From 5b9f36292525ab9567460b4ba156c24b3435aeb2 Mon Sep 17 00:00:00 2001 From: John Leehey Date: Sat, 30 Nov 2019 18:37:52 -0800 Subject: [PATCH 001/675] Add support for margins in Webtoon view On larger tablets, matching the page width to the screen width in webtoon mode causes eye strain due to the image looking so magnified. Adding a page margin to the image can resolve this by effectively scaling the image down. --- .../data/preference/PreferenceKeys.kt | 2 ++ .../data/preference/PreferencesHelper.kt | 2 ++ .../ui/reader/ReaderSettingsSheet.kt | 1 + .../ui/reader/viewer/webtoon/WebtoonConfig.kt | 20 ++++++++++++++ .../viewer/webtoon/WebtoonPageHolder.kt | 13 +++++++++- .../ui/reader/viewer/webtoon/WebtoonViewer.kt | 2 ++ .../tachiyomi/ui/setting/PreferenceDSL.kt | 5 ++++ .../ui/setting/SettingsReaderController.kt | 10 +++++++ .../widget/preference/FloatListPreference.kt | 26 +++++++++++++++++++ .../main/res/layout/reader_settings_sheet.xml | 21 ++++++++++++++- app/src/main/res/values/arrays.xml | 6 +++++ app/src/main/res/values/strings.xml | 4 +++ 12 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 58388547c..26e25aa29 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -51,6 +51,8 @@ object PreferenceKeys { const val readWithVolumeKeysInverted = "reader_volume_keys_inverted" + const val webtoonMarginRatio = "margin_ratio" + const val portraitColumns = "pref_library_columns_portrait_key" const val landscapeColumns = "pref_library_columns_landscape_key" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 68e6371ee..bb5be1c25 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -79,6 +79,8 @@ class PreferencesHelper(val context: Context) { fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false) + fun marginRatio() = rxPrefs.getInteger(Keys.webtoonMarginRatio, 0) + fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0) fun landscapeColumns() = rxPrefs.getInteger(Keys.landscapeColumns, 0) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt index b798f3b49..5f88b26fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt @@ -82,6 +82,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia private fun initWebtoonPreferences() { webtoon_prefs_group.visible() crop_borders_webtoon.bindToPreference(preferences.cropBordersWebtoon()) + margin_ratio_webtoon.bindToPreference(preferences.marginRatio()) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt index 7ac8a220a..31ca89323 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt @@ -34,6 +34,9 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) { var doubleTapAnimDuration = 500 private set + var marginRatio = 0f + private set + init { preferences.readWithTapping() .register({ tappingEnabled = it }) @@ -52,6 +55,23 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) { preferences.readWithVolumeKeysInverted() .register({ volumeKeysInverted = it }) + + preferences.marginRatio() + .register({ marginFromPreference(it) }, { imagePropertyChangedListener?.invoke() }) + } + + private fun marginFromPreference(position: Int) { + marginRatio = when (position) { + 1 -> PageMargin.TEN_PERCENT + 2 -> PageMargin.TWENTY_FIVE_PERCENT + else -> PageMargin.NO_MARGIN + } + } + + object PageMargin { + const val NO_MARGIN = 0f + const val TEN_PERCENT = 0.1f + const val TWENTY_FIVE_PERCENT = 0.25f } fun unsubscribe() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 754dbb7e1..34fb025d4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -2,11 +2,13 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon import android.annotation.SuppressLint import android.content.Intent +import android.content.res.Resources import android.graphics.drawable.Drawable import android.net.Uri import android.support.v7.widget.AppCompatButton import android.support.v7.widget.AppCompatImageView import android.view.Gravity +import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT @@ -111,7 +113,7 @@ class WebtoonPageHolder( private var readImageHeaderSubscription: Subscription? = null init { - frame.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + refreshLayoutParams() } /** @@ -120,6 +122,15 @@ class WebtoonPageHolder( fun bind(page: ReaderPage) { this.page = page observeStatus() + refreshLayoutParams() + } + + private fun refreshLayoutParams() { + frame.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { + val margin = Resources.getSystem().displayMetrics.widthPixels * viewer.config.marginRatio + marginEnd = margin.toInt() + marginStart = margin.toInt() + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index 6adee83c2..a2cb85597 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -110,6 +110,8 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer { frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) frame.addView(recycler) + + config.imagePropertyChangedListener = { adapter.notifyDataSetChanged() } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt index 6fc05d1af..138ebf76e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting import android.support.graphics.drawable.VectorDrawableCompat import android.support.v4.graphics.drawable.DrawableCompat import android.support.v7.preference.* +import eu.kanade.tachiyomi.widget.preference.FloatListPreference import eu.kanade.tachiyomi.widget.preference.IntListPreference @DslMarker @@ -37,6 +38,10 @@ inline fun PreferenceGroup.intListPreference(block: (@DSL IntListPreference).() return initThenAdd(IntListPreference(context), block).also(::initDialog) } +inline fun PreferenceGroup.floatListPreference(block: (@DSL FloatListPreference).() -> Unit): FloatListPreference { + return initThenAdd(FloatListPreference(context), block).also(::initDialog) +} + inline fun PreferenceGroup.multiSelectListPreference(block: (@DSL MultiSelectListPreference).() -> Unit): MultiSelectListPreference { return initThenAdd(MultiSelectListPreference(context), block).also(::initDialog) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index ae59d13f1..e5958fdac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -112,6 +112,16 @@ class SettingsReaderController : SettingsController() { titleRes = R.string.pref_crop_borders defaultValue = false } + + floatListPreference { + key = Keys.webtoonMarginRatio + titleRes = R.string.pref_reader_theme + entriesRes = arrayOf(R.string.webtoon_margin_ratio_0, + R.string.webtoon_margin_ratio_10, R.string.webtoon_margin_ratio_25) + entryValues = arrayOf("0", "1", "2") + defaultValue = "0" + summary = "%s" + } } preferenceCategory { titleRes = R.string.pref_reader_navigation diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt new file mode 100644 index 000000000..fb7d66e63 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.widget.preference + +import android.content.Context +import android.support.v7.preference.ListPreference +import android.util.AttributeSet + +class FloatListPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + ListPreference(context, attrs) { + + override fun persistString(value: String?): Boolean { + return value != null && persistFloat(value.toFloat()) + } + + override fun getPersistedString(defaultReturnValue: String?): String? { + // When the underlying preference is using a PreferenceDataStore, there's no way (for now) + // to check if a value is in the store, so we use a most likely unused value as workaround + val defaultIntValue = Float.NEGATIVE_INFINITY + + val value = getPersistedFloat(defaultIntValue) + return if (value != defaultIntValue) { + value.toString() + } else { + defaultReturnValue + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/reader_settings_sheet.xml b/app/src/main/res/layout/reader_settings_sheet.xml index d28155d70..12435ba18 100644 --- a/app/src/main/res/layout/reader_settings_sheet.xml +++ b/app/src/main/res/layout/reader_settings_sheet.xml @@ -240,6 +240,25 @@ android:textColor="?android:attr/textColorSecondary" app:layout_constraintTop_toBottomOf="@id/webtoon_prefs" /> + + + + + app:constraint_referenced_ids="webtoon_prefs,crop_borders_webtoon,margin_ratio_text,margin_ratio_webtoon" /> @string/scale_type_smart_fit + + @string/webtoon_margin_ratio_0 + @string/webtoon_margin_ratio_10 + @string/webtoon_margin_ratio_25 + + 1 2 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f72686b4f..a42e9d4ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,6 +196,7 @@ Tapping Long tap dialog Background color + Margin ratio White Black Default viewer @@ -230,6 +231,9 @@ G B A + No margin + 10% + 25% From 19993199db400e0276966dacba5511115e907170 Mon Sep 17 00:00:00 2001 From: John Leehey Date: Sat, 30 Nov 2019 18:41:52 -0800 Subject: [PATCH 002/675] Remove no-longer-needed FloatListPreference --- .../tachiyomi/ui/setting/PreferenceDSL.kt | 5 ---- .../ui/setting/SettingsReaderController.kt | 2 +- .../widget/preference/FloatListPreference.kt | 26 ------------------- 3 files changed, 1 insertion(+), 32 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt index 138ebf76e..6fc05d1af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.setting import android.support.graphics.drawable.VectorDrawableCompat import android.support.v4.graphics.drawable.DrawableCompat import android.support.v7.preference.* -import eu.kanade.tachiyomi.widget.preference.FloatListPreference import eu.kanade.tachiyomi.widget.preference.IntListPreference @DslMarker @@ -38,10 +37,6 @@ inline fun PreferenceGroup.intListPreference(block: (@DSL IntListPreference).() return initThenAdd(IntListPreference(context), block).also(::initDialog) } -inline fun PreferenceGroup.floatListPreference(block: (@DSL FloatListPreference).() -> Unit): FloatListPreference { - return initThenAdd(FloatListPreference(context), block).also(::initDialog) -} - inline fun PreferenceGroup.multiSelectListPreference(block: (@DSL MultiSelectListPreference).() -> Unit): MultiSelectListPreference { return initThenAdd(MultiSelectListPreference(context), block).also(::initDialog) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index e5958fdac..901b6c358 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -113,7 +113,7 @@ class SettingsReaderController : SettingsController() { defaultValue = false } - floatListPreference { + intListPreference { key = Keys.webtoonMarginRatio titleRes = R.string.pref_reader_theme entriesRes = arrayOf(R.string.webtoon_margin_ratio_0, diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt deleted file mode 100644 index fb7d66e63..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt +++ /dev/null @@ -1,26 +0,0 @@ -package eu.kanade.tachiyomi.widget.preference - -import android.content.Context -import android.support.v7.preference.ListPreference -import android.util.AttributeSet - -class FloatListPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - ListPreference(context, attrs) { - - override fun persistString(value: String?): Boolean { - return value != null && persistFloat(value.toFloat()) - } - - override fun getPersistedString(defaultReturnValue: String?): String? { - // When the underlying preference is using a PreferenceDataStore, there's no way (for now) - // to check if a value is in the store, so we use a most likely unused value as workaround - val defaultIntValue = Float.NEGATIVE_INFINITY - - val value = getPersistedFloat(defaultIntValue) - return if (value != defaultIntValue) { - value.toString() - } else { - defaultReturnValue - } - } -} \ No newline at end of file From 4014c48c626da812d9e40135941f067ecf08ef03 Mon Sep 17 00:00:00 2001 From: John Leehey Date: Mon, 2 Dec 2019 13:38:33 -0800 Subject: [PATCH 003/675] Revert "Remove no-longer-needed FloatListPreference" This reverts commit 19993199db400e0276966dacba5511115e907170. --- .../tachiyomi/ui/setting/PreferenceDSL.kt | 5 ++++ .../ui/setting/SettingsReaderController.kt | 2 +- .../widget/preference/FloatListPreference.kt | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt index 6fc05d1af..138ebf76e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting import android.support.graphics.drawable.VectorDrawableCompat import android.support.v4.graphics.drawable.DrawableCompat import android.support.v7.preference.* +import eu.kanade.tachiyomi.widget.preference.FloatListPreference import eu.kanade.tachiyomi.widget.preference.IntListPreference @DslMarker @@ -37,6 +38,10 @@ inline fun PreferenceGroup.intListPreference(block: (@DSL IntListPreference).() return initThenAdd(IntListPreference(context), block).also(::initDialog) } +inline fun PreferenceGroup.floatListPreference(block: (@DSL FloatListPreference).() -> Unit): FloatListPreference { + return initThenAdd(FloatListPreference(context), block).also(::initDialog) +} + inline fun PreferenceGroup.multiSelectListPreference(block: (@DSL MultiSelectListPreference).() -> Unit): MultiSelectListPreference { return initThenAdd(MultiSelectListPreference(context), block).also(::initDialog) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index 901b6c358..e5958fdac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -113,7 +113,7 @@ class SettingsReaderController : SettingsController() { defaultValue = false } - intListPreference { + floatListPreference { key = Keys.webtoonMarginRatio titleRes = R.string.pref_reader_theme entriesRes = arrayOf(R.string.webtoon_margin_ratio_0, diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt new file mode 100644 index 000000000..fb7d66e63 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/FloatListPreference.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.widget.preference + +import android.content.Context +import android.support.v7.preference.ListPreference +import android.util.AttributeSet + +class FloatListPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + ListPreference(context, attrs) { + + override fun persistString(value: String?): Boolean { + return value != null && persistFloat(value.toFloat()) + } + + override fun getPersistedString(defaultReturnValue: String?): String? { + // When the underlying preference is using a PreferenceDataStore, there's no way (for now) + // to check if a value is in the store, so we use a most likely unused value as workaround + val defaultIntValue = Float.NEGATIVE_INFINITY + + val value = getPersistedFloat(defaultIntValue) + return if (value != defaultIntValue) { + value.toString() + } else { + defaultReturnValue + } + } +} \ No newline at end of file From f14af7cf832a4ac7900897cd882ecb298e2d9546 Mon Sep 17 00:00:00 2001 From: John Leehey Date: Mon, 2 Dec 2019 14:19:22 -0800 Subject: [PATCH 004/675] Bind the margin ratio as a float preference and rename variables - Fixed the floatListPreference that was used in the SettingsReaderController - Created new extension for binding float preferences in the ReaderSettingsSheet - Renamed the ratio variables to include the `webtoon` naming --- .../data/preference/PreferenceKeys.kt | 2 +- .../data/preference/PreferencesHelper.kt | 4 ++-- .../tachiyomi/ui/reader/ReaderSettingsSheet.kt | 16 +++++++++++++++- .../ui/reader/viewer/webtoon/WebtoonConfig.kt | 18 ++---------------- .../ui/setting/SettingsReaderController.kt | 9 +++++---- .../main/res/layout/reader_settings_sheet.xml | 2 +- app/src/main/res/values/arrays.xml | 12 +++++++++++- app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 39 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 26e25aa29..74f9c5a6e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -51,7 +51,7 @@ object PreferenceKeys { const val readWithVolumeKeysInverted = "reader_volume_keys_inverted" - const val webtoonMarginRatio = "margin_ratio" + const val marginRatioWebtoon = "margin_ratio_webtoon" const val portraitColumns = "pref_library_columns_portrait_key" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index bb5be1c25..75bef0c5f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -71,6 +71,8 @@ class PreferencesHelper(val context: Context) { fun cropBordersWebtoon() = rxPrefs.getBoolean(Keys.cropBordersWebtoon, false) + fun marginRatioWebtoon() = rxPrefs.getFloat(Keys.marginRatioWebtoon, 0f) + fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true) fun readWithLongTap() = rxPrefs.getBoolean(Keys.readWithLongTap, true) @@ -79,8 +81,6 @@ class PreferencesHelper(val context: Context) { fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false) - fun marginRatio() = rxPrefs.getInteger(Keys.webtoonMarginRatio, 0) - fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0) fun landscapeColumns() = rxPrefs.getInteger(Keys.landscapeColumns, 0) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt index 5f88b26fa..595668473 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.reader import android.os.Bundle +import android.support.annotation.ArrayRes import android.support.design.widget.BottomSheetDialog import android.support.v4.widget.NestedScrollView import android.widget.CompoundButton @@ -82,7 +83,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia private fun initWebtoonPreferences() { webtoon_prefs_group.visible() crop_borders_webtoon.bindToPreference(preferences.cropBordersWebtoon()) - margin_ratio_webtoon.bindToPreference(preferences.marginRatio()) + margin_ratio_webtoon.bindToFloatPreference(preferences.marginRatioWebtoon(), R.array.webtoon_margin_ratio_values) } /** @@ -103,4 +104,17 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia setSelection(pref.getOrDefault() - offset, false) } + /** + * Binds a spinner to a float preference. The position of the spinner item must + * correlate with the [floatValues] resource item (in arrays.xml), which is a + * of float values that will be parsed here and applied to the preference. + */ + private fun Spinner.bindToFloatPreference(pref: Preference, @ArrayRes floatValuesResource: Int) { + val floatValues = resources.getStringArray(floatValuesResource).map { it.toFloatOrNull() } + onItemSelectedListener = IgnoreFirstSpinnerListener { position -> + pref.set(floatValues[position]) + } + setSelection(floatValues.indexOf(pref.getOrDefault()), false) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt index 31ca89323..b26ea5144 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt @@ -56,22 +56,8 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) { preferences.readWithVolumeKeysInverted() .register({ volumeKeysInverted = it }) - preferences.marginRatio() - .register({ marginFromPreference(it) }, { imagePropertyChangedListener?.invoke() }) - } - - private fun marginFromPreference(position: Int) { - marginRatio = when (position) { - 1 -> PageMargin.TEN_PERCENT - 2 -> PageMargin.TWENTY_FIVE_PERCENT - else -> PageMargin.NO_MARGIN - } - } - - object PageMargin { - const val NO_MARGIN = 0f - const val TEN_PERCENT = 0.1f - const val TWENTY_FIVE_PERCENT = 0.25f + preferences.marginRatioWebtoon() + .register({ marginRatio = it }, { imagePropertyChangedListener?.invoke() }) } fun unsubscribe() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index e5958fdac..ceb9ee64c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -114,11 +114,12 @@ class SettingsReaderController : SettingsController() { } floatListPreference { - key = Keys.webtoonMarginRatio - titleRes = R.string.pref_reader_theme + key = Keys.marginRatioWebtoon + titleRes = R.string.pref_reader_margin entriesRes = arrayOf(R.string.webtoon_margin_ratio_0, - R.string.webtoon_margin_ratio_10, R.string.webtoon_margin_ratio_25) - entryValues = arrayOf("0", "1", "2") + R.string.webtoon_margin_ratio_10, R.string.webtoon_margin_ratio_15, + R.string.webtoon_margin_ratio_20, R.string.webtoon_margin_ratio_25) + entryValues = arrayOf("0", "0.1", "0.15", "0.2", "0.25") defaultValue = "0" summary = "%s" } diff --git a/app/src/main/res/layout/reader_settings_sheet.xml b/app/src/main/res/layout/reader_settings_sheet.xml index 12435ba18..04ebe2909 100644 --- a/app/src/main/res/layout/reader_settings_sheet.xml +++ b/app/src/main/res/layout/reader_settings_sheet.xml @@ -254,7 +254,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:entries="@array/margin_ratio" + android:entries="@array/webtoon_margin_ratio" app:layout_constraintLeft_toRightOf="@id/verticalcenter" app:layout_constraintRight_toRightOf="@id/spinner_end" app:layout_constraintTop_toBottomOf="@id/crop_borders_webtoon"/> diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 87e5710ff..feefd81b3 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -53,12 +53,22 @@ @string/scale_type_smart_fit - + @string/webtoon_margin_ratio_0 @string/webtoon_margin_ratio_10 + @string/webtoon_margin_ratio_15 + @string/webtoon_margin_ratio_20 @string/webtoon_margin_ratio_25 + + 0.0 + 0.1 + 0.15 + 0.2 + 0.25 + + 1 2 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a42e9d4ed..6a261e6fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -233,6 +233,8 @@ A No margin 10% + 15% + 20% 25% From dd1e6402c97acfb66b911a1d496aa90828df8542 Mon Sep 17 00:00:00 2001 From: MCAxiaz Date: Sun, 5 Jan 2020 09:18:02 -0800 Subject: [PATCH 005/675] Improve Loading Speed When Skipping Pages in a Chapter (#2426) * cancel queued loads when the page that requested the queue is destroyed * use page.status for optimizing removal --- .../ui/reader/loader/HttpPageLoader.kt | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index 223e9811e..f7b30d442 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -17,6 +17,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.PriorityBlockingQueue import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.min /** * Loader used to load chapters from an online source. @@ -37,18 +38,20 @@ class HttpPageLoader( */ private val subscriptions = CompositeSubscription() + private val preloadSize = 4 + init { subscriptions += Observable.defer { Observable.just(queue.take().page) } - .filter { it.status == Page.QUEUE } - .concatMap { source.fetchImageFromCacheThenNet(it) } - .repeat() - .subscribeOn(Schedulers.io()) - .subscribe({ - }, { error -> - if (error !is InterruptedException) { - Timber.e(error) - } - }) + .filter { it.status == Page.QUEUE } + .concatMap { source.fetchImageFromCacheThenNet(it) } + .repeat() + .subscribeOn(Schedulers.io()) + .subscribe({ + }, { error -> + if (error !is InterruptedException) { + Timber.e(error) + } + }) } /** @@ -80,13 +83,13 @@ class HttpPageLoader( */ override fun getPages(): Observable> { return chapterCache - .getPageListFromCache(chapter.chapter) - .onErrorResumeNext { source.fetchPageList(chapter.chapter) } - .map { pages -> - pages.mapIndexed { index, page -> // Don't trust sources and use our own indexing - ReaderPage(index, page.url, page.imageUrl) + .getPageListFromCache(chapter.chapter) + .onErrorResumeNext { source.fetchPageList(chapter.chapter) } + .map { pages -> + pages.mapIndexed { index, page -> // Don't trust sources and use our own indexing + ReaderPage(index, page.url, page.imageUrl) + } } - } } /** @@ -110,29 +113,41 @@ class HttpPageLoader( val statusSubject = SerializedSubject(PublishSubject.create()) page.setStatusSubject(statusSubject) + val queuedPages = mutableListOf() if (page.status == Page.QUEUE) { - queue.offer(PriorityPage(page, 1)) + queuedPages += PriorityPage(page, 1).also { queue.offer(it) } } - - preloadNextPages(page, 4) + queuedPages += preloadNextPages(page, preloadSize) statusSubject.startWith(page.status) + .doOnUnsubscribe { + queuedPages.forEach { + if (it.page.status == Page.QUEUE) { + queue.remove(it) + } + } + } } + .subscribeOn(Schedulers.io()) + .unsubscribeOn(Schedulers.io()) } /** * Preloads the given [amount] of pages after the [currentPage] with a lower priority. + * @return a list of [PriorityPage] that were added to the [queue] */ - private fun preloadNextPages(currentPage: ReaderPage, amount: Int) { + private fun preloadNextPages(currentPage: ReaderPage, amount: Int): List { val pageIndex = currentPage.index - val pages = currentPage.chapter.pages ?: return - if (pageIndex == pages.lastIndex) return - val nextPages = pages.subList(pageIndex + 1, Math.min(pageIndex + 1 + amount, pages.size)) - for (nextPage in nextPages) { - if (nextPage.status == Page.QUEUE) { - queue.offer(PriorityPage(nextPage, 0)) - } - } + val pages = currentPage.chapter.pages ?: return emptyList() + if (pageIndex == pages.lastIndex) return emptyList() + + return pages + .subList(pageIndex + 1, min(pageIndex + 1 + amount, pages.size)) + .mapNotNull { + if (it.status == Page.QUEUE) { + PriorityPage(it, 0).apply { queue.offer(this) } + } else null + } } /** @@ -148,7 +163,7 @@ class HttpPageLoader( /** * Data class used to keep ordering of pages in order to maintain priority. */ - private data class PriorityPage( + private class PriorityPage( val page: ReaderPage, val priority: Int ): Comparable { From ed7ebf2da1c52389c5872dc5783e266f5ec797e8 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 5 Jan 2020 12:25:48 -0500 Subject: [PATCH 006/675] Update conductor-support-preference for AndroidX preference v1.1.0 compat (fixes #2431) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 14d2e9c40..0a1866f6c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -212,7 +212,7 @@ dependencies { implementation ("com.bluelinelabs:conductor-support:2.1.5") { exclude group: "com.android.support" } - implementation 'com.github.inorichi:conductor-support-preference:78e2344' + implementation 'com.github.inorichi:conductor-support-preference:a32c357' // RxBindings final rxbindings_version = '1.0.1' From b4aedb5f843e959b2e432374f67f37919db0c37d Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 5 Jan 2020 14:38:38 -0500 Subject: [PATCH 007/675] Enforce unix line endings --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..735361404 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +* text eol=lf From 600fbb2ef81bfd3c2ea88aa8e88161b432f4364f Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 5 Jan 2020 14:43:07 -0500 Subject: [PATCH 008/675] Update files to use unix line endings cmd: `find . -type f -print0 | xargs -0 dos2unix` --- .../tachiyomi/data/backup/models/Backup.kt | 44 +- .../data/database/queries/TrackQueries.kt | 66 +- .../data/preference/PreferenceKeys.kt | 264 ++-- .../tachiyomi/data/track/TrackManager.kt | 72 +- .../tachiyomi/data/track/TrackService.kt | 140 +- .../tachiyomi/data/track/anilist/Anilist.kt | 428 +++--- .../data/track/anilist/AnilistApi.kt | 582 ++++----- .../data/track/anilist/AnilistInterceptor.kt | 114 +- .../tachiyomi/data/track/anilist/OAuth.kt | 18 +- .../tachiyomi/data/track/bangumi/Bangumi.kt | 288 ++-- .../tachiyomi/data/track/bangumi/OAuth.kt | 32 +- .../tachiyomi/data/track/kitsu/Kitsu.kt | 288 ++-- .../tachiyomi/data/track/kitsu/OAuth.kt | 20 +- .../data/track/myanimelist/MyAnimeList.kt | 326 ++--- .../tachiyomi/data/track/shikimori/OAuth.kt | 26 +- .../data/track/shikimori/Shikimori.kt | 278 ++-- .../network/CloudflareInterceptor.kt | 308 ++--- .../kanade/tachiyomi/network/NetworkHelper.kt | 234 ++-- .../tachiyomi/network/OkHttpExtensions.kt | 140 +- .../tachiyomi/network/ProgressListener.kt | 8 +- .../tachiyomi/network/ProgressResponseBody.kt | 80 +- .../eu/kanade/tachiyomi/network/Requests.kt | 64 +- .../tachiyomi/source/CatalogueSource.kt | 90 +- .../java/eu/kanade/tachiyomi/source/Source.kt | 86 +- .../kanade/tachiyomi/source/SourceManager.kt | 148 +-- .../kanade/tachiyomi/source/model/Filter.kt | 78 +- .../tachiyomi/source/model/FilterList.kt | 12 +- .../tachiyomi/source/model/MangasPage.kt | 4 +- .../eu/kanade/tachiyomi/source/model/Page.kt | 96 +- .../kanade/tachiyomi/source/model/SChapter.kt | 60 +- .../tachiyomi/source/model/SChapterImpl.kt | 28 +- .../kanade/tachiyomi/source/model/SManga.kt | 114 +- .../tachiyomi/source/model/SMangaImpl.kt | 44 +- .../tachiyomi/source/online/HttpSource.kt | 734 +++++------ .../source/online/HttpSourceFetcher.kt | 50 +- .../tachiyomi/source/online/LoginSource.kt | 28 +- .../source/online/ParsedHttpSource.kt | 400 +++--- .../ui/base/controller/NucleusController.kt | 42 +- .../presenter/NucleusConductorDelegate.java | 122 +- .../NucleusConductorLifecycleListener.java | 88 +- .../ui/catalogue/filter/SectionItems.kt | 176 +-- .../ui/catalogue/filter/SortGroup.kt | 108 +- .../CatalogueSearchCardAdapter.kt | 54 +- .../CatalogueSearchCardHolder.kt | 102 +- .../global_search/CatalogueSearchCardItem.kt | 74 +- .../ui/download/DownloadController.kt | 494 +++---- .../ui/library/ChangeMangaCategoriesDialog.kt | 94 +- .../ui/library/DeleteLibraryMangasDialog.kt | 84 +- .../tachiyomi/ui/library/LibraryAdapter.kt | 204 +-- .../ui/library/LibraryCategoryAdapter.kt | 96 +- .../ui/library/LibraryCategoryView.kt | 496 +++---- .../tachiyomi/ui/library/LibraryController.kt | 1048 +++++++-------- .../tachiyomi/ui/library/LibraryGridHolder.kt | 114 +- .../tachiyomi/ui/library/LibraryHolder.kt | 54 +- .../tachiyomi/ui/library/LibraryItem.kt | 150 +-- .../tachiyomi/ui/library/LibraryListHolder.kt | 130 +- .../ui/library/LibraryNavigationView.kt | 432 +++--- .../tachiyomi/ui/library/LibraryPresenter.kt | 742 +++++------ .../tachiyomi/ui/library/LibrarySort.kt | 20 +- .../ui/main/ChangelogDialogController.kt | 62 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 564 ++++---- .../tachiyomi/ui/manga/MangaController.kt | 386 +++--- .../ui/manga/chapter/ChapterHolder.kt | 244 ++-- .../tachiyomi/ui/manga/chapter/ChapterItem.kt | 110 +- .../ui/manga/chapter/ChaptersAdapter.kt | 90 +- .../ui/manga/chapter/ChaptersController.kt | 972 +++++++------- .../ui/manga/chapter/ChaptersPresenter.kt | 836 ++++++------ .../ui/manga/chapter/DeleteChaptersDialog.kt | 62 +- .../manga/chapter/DeletingChaptersDialog.kt | 52 +- .../manga/chapter/DownloadChaptersDialog.kt | 82 +- .../ui/manga/chapter/SetDisplayModeDialog.kt | 84 +- .../ui/manga/chapter/SetSortingDialog.kt | 84 +- .../ui/manga/info/MangaInfoController.kt | 1154 ++++++++--------- .../ui/manga/info/MangaInfoPresenter.kt | 346 ++--- .../ui/manga/track/SetTrackChaptersDialog.kt | 146 +-- .../ui/manga/track/SetTrackScoreDialog.kt | 158 +-- .../ui/manga/track/SetTrackStatusDialog.kt | 114 +- .../tachiyomi/ui/manga/track/TrackAdapter.kt | 90 +- .../ui/manga/track/TrackController.kt | 284 ++-- .../tachiyomi/ui/manga/track/TrackHolder.kt | 84 +- .../tachiyomi/ui/manga/track/TrackItem.kt | 12 +- .../ui/manga/track/TrackPresenter.kt | 258 ++-- .../ui/manga/track/TrackSearchAdapter.kt | 156 +-- .../ui/manga/track/TrackSearchDialog.kt | 288 ++-- .../RecentChaptersController.kt | 666 +++++----- .../ui/setting/SettingsController.kt | 174 +-- .../ui/setting/SettingsMainController.kt | 122 +- .../widget/ExtendedNavigationView.kt | 478 +++---- .../main/res/drawable/empty_drawable_32dp.xml | 14 +- .../main/res/drawable/ic_done_white_18dp.xml | 18 +- .../drawable/ic_watch_later_black_24dp.xml | 18 +- .../res/layout/navigation_view_checkbox.xml | 46 +- .../main/res/layout/navigation_view_group.xml | 58 +- app/src/main/res/layout/pref_item_source.xml | 122 +- app/src/main/res/layout/track_item.xml | 382 +++--- 95 files changed, 9766 insertions(+), 9766 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt index 3a5e2d343..dd50553c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt @@ -1,23 +1,23 @@ -package eu.kanade.tachiyomi.data.backup.models - -import java.text.SimpleDateFormat -import java.util.* - -/** - * Json values - */ -object Backup { - const val CURRENT_VERSION = 2 - const val MANGA = "manga" - const val MANGAS = "mangas" - const val TRACK = "track" - const val CHAPTERS = "chapters" - const val CATEGORIES = "categories" - const val HISTORY = "history" - const val VERSION = "version" - - fun getDefaultFilename(): String { - val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) - return "tachiyomi_$date.json" - } +package eu.kanade.tachiyomi.data.backup.models + +import java.text.SimpleDateFormat +import java.util.* + +/** + * Json values + */ +object Backup { + const val CURRENT_VERSION = 2 + const val MANGA = "manga" + const val MANGAS = "mangas" + const val TRACK = "track" + const val CHAPTERS = "chapters" + const val CATEGORIES = "categories" + const val HISTORY = "history" + const val VERSION = "version" + + fun getDefaultFilename(): String { + val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) + return "tachiyomi_$date.json" + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt index e215e72ea..a93877faf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt @@ -1,34 +1,34 @@ -package eu.kanade.tachiyomi.data.database.queries - -import com.pushtorefresh.storio.sqlite.queries.DeleteQuery -import com.pushtorefresh.storio.sqlite.queries.Query -import eu.kanade.tachiyomi.data.database.DbProvider -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.database.tables.TrackTable -import eu.kanade.tachiyomi.data.track.TrackService - -interface TrackQueries : DbProvider { - - fun getTracks(manga: Manga) = db.get() - .listOfObjects(Track::class.java) - .withQuery(Query.builder() - .table(TrackTable.TABLE) - .where("${TrackTable.COL_MANGA_ID} = ?") - .whereArgs(manga.id) - .build()) - .prepare() - - fun insertTrack(track: Track) = db.put().`object`(track).prepare() - - fun insertTracks(tracks: List) = db.put().objects(tracks).prepare() - - fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete() - .byQuery(DeleteQuery.builder() - .table(TrackTable.TABLE) - .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") - .whereArgs(manga.id, sync.id) - .build()) - .prepare() - +package eu.kanade.tachiyomi.data.database.queries + +import com.pushtorefresh.storio.sqlite.queries.DeleteQuery +import com.pushtorefresh.storio.sqlite.queries.Query +import eu.kanade.tachiyomi.data.database.DbProvider +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.database.tables.TrackTable +import eu.kanade.tachiyomi.data.track.TrackService + +interface TrackQueries : DbProvider { + + fun getTracks(manga: Manga) = db.get() + .listOfObjects(Track::class.java) + .withQuery(Query.builder() + .table(TrackTable.TABLE) + .where("${TrackTable.COL_MANGA_ID} = ?") + .whereArgs(manga.id) + .build()) + .prepare() + + fun insertTrack(track: Track) = db.put().`object`(track).prepare() + + fun insertTracks(tracks: List) = db.put().objects(tracks).prepare() + + fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete() + .byQuery(DeleteQuery.builder() + .table(TrackTable.TABLE) + .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") + .whereArgs(manga.id, sync.id) + .build()) + .prepare() + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 58388547c..057def523 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -1,132 +1,132 @@ -package eu.kanade.tachiyomi.data.preference - -/** - * This class stores the keys for the preferences in the application. - */ -object PreferenceKeys { - - const val theme = "pref_theme_key" - - const val rotation = "pref_rotation_type_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 trueColor = "pref_true_color_key" - - const val fullscreen = "fullscreen" - - const val keepScreenOn = "pref_keep_screen_on_key" - - const val customBrightness = "pref_custom_brightness_key" - - const val customBrightnessValue = "custom_brightness_value" - - const val colorFilter = "pref_color_filter_key" - - const val colorFilterValue = "color_filter_value" - - const val colorFilterMode = "color_filter_mode" - - const val defaultViewer = "pref_default_viewer_key" - - const val imageScaleType = "pref_image_scale_type_key" - - const val zoomStart = "pref_zoom_start_key" - - const val readerTheme = "pref_reader_theme_key" - - const val cropBorders = "crop_borders" - - const val cropBordersWebtoon = "crop_borders_webtoon" - - const val readWithTapping = "reader_tap" - - const val readWithLongTap = "reader_long_tap" - - const val readWithVolumeKeys = "reader_volume_keys" - - const val readWithVolumeKeysInverted = "reader_volume_keys_inverted" - - const val portraitColumns = "pref_library_columns_portrait_key" - - const val landscapeColumns = "pref_library_columns_landscape_key" - - const val updateOnlyNonCompleted = "pref_update_only_non_completed_key" - - const val autoUpdateTrack = "pref_auto_update_manga_sync_key" - - const val lastUsedCatalogueSource = "last_catalogue_source" - - const val lastUsedCategory = "last_used_category" - - const val catalogueAsList = "pref_display_catalogue_as_list" - - const val enabledLanguages = "source_languages" - - const val backupDirectory = "backup_directory" - - const val downloadsDirectory = "download_directory" - - const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key" - - const val numberOfBackups = "backup_slots" - - const val backupInterval = "backup_interval" - - const val removeAfterReadSlots = "remove_after_read_slots" - - const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key" - - const val libraryUpdateInterval = "pref_library_update_interval_key" - - const val libraryUpdateRestriction = "library_update_restriction" - - const val libraryUpdateCategories = "library_update_categories" - - const val libraryUpdatePrioritization = "library_update_prioritization" - - const val filterDownloaded = "pref_filter_downloaded_key" - - const val filterUnread = "pref_filter_unread_key" - - const val filterCompleted = "pref_filter_completed_key" - - const val librarySortingMode = "library_sorting_mode" - - const val automaticUpdates = "automatic_updates" - - const val startScreen = "start_screen" - - const val downloadNew = "download_new" - - const val downloadNewCategories = "download_new_categories" - - const val libraryAsList = "pref_display_library_as_list" - - const val lang = "app_language" - - const val defaultCategory = "default_category" - - const val skipRead = "skip_read" - - const val downloadBadge = "display_download_badge" - - @Deprecated("Use the preferences of the source") - fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" - - @Deprecated("Use the preferences of the source") - fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId" - - fun sourceSharedPref(sourceId: Long) = "source_$sourceId" - - fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" - - fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" - - fun trackToken(syncId: Int) = "track_token_$syncId" - -} +package eu.kanade.tachiyomi.data.preference + +/** + * This class stores the keys for the preferences in the application. + */ +object PreferenceKeys { + + const val theme = "pref_theme_key" + + const val rotation = "pref_rotation_type_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 trueColor = "pref_true_color_key" + + const val fullscreen = "fullscreen" + + const val keepScreenOn = "pref_keep_screen_on_key" + + const val customBrightness = "pref_custom_brightness_key" + + const val customBrightnessValue = "custom_brightness_value" + + const val colorFilter = "pref_color_filter_key" + + const val colorFilterValue = "color_filter_value" + + const val colorFilterMode = "color_filter_mode" + + const val defaultViewer = "pref_default_viewer_key" + + const val imageScaleType = "pref_image_scale_type_key" + + const val zoomStart = "pref_zoom_start_key" + + const val readerTheme = "pref_reader_theme_key" + + const val cropBorders = "crop_borders" + + const val cropBordersWebtoon = "crop_borders_webtoon" + + const val readWithTapping = "reader_tap" + + const val readWithLongTap = "reader_long_tap" + + const val readWithVolumeKeys = "reader_volume_keys" + + const val readWithVolumeKeysInverted = "reader_volume_keys_inverted" + + const val portraitColumns = "pref_library_columns_portrait_key" + + const val landscapeColumns = "pref_library_columns_landscape_key" + + const val updateOnlyNonCompleted = "pref_update_only_non_completed_key" + + const val autoUpdateTrack = "pref_auto_update_manga_sync_key" + + const val lastUsedCatalogueSource = "last_catalogue_source" + + const val lastUsedCategory = "last_used_category" + + const val catalogueAsList = "pref_display_catalogue_as_list" + + const val enabledLanguages = "source_languages" + + const val backupDirectory = "backup_directory" + + const val downloadsDirectory = "download_directory" + + const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key" + + const val numberOfBackups = "backup_slots" + + const val backupInterval = "backup_interval" + + const val removeAfterReadSlots = "remove_after_read_slots" + + const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key" + + const val libraryUpdateInterval = "pref_library_update_interval_key" + + const val libraryUpdateRestriction = "library_update_restriction" + + const val libraryUpdateCategories = "library_update_categories" + + const val libraryUpdatePrioritization = "library_update_prioritization" + + const val filterDownloaded = "pref_filter_downloaded_key" + + const val filterUnread = "pref_filter_unread_key" + + const val filterCompleted = "pref_filter_completed_key" + + const val librarySortingMode = "library_sorting_mode" + + const val automaticUpdates = "automatic_updates" + + const val startScreen = "start_screen" + + const val downloadNew = "download_new" + + const val downloadNewCategories = "download_new_categories" + + const val libraryAsList = "pref_display_library_as_list" + + const val lang = "app_language" + + const val defaultCategory = "default_category" + + const val skipRead = "skip_read" + + const val downloadBadge = "display_download_badge" + + @Deprecated("Use the preferences of the source") + fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" + + @Deprecated("Use the preferences of the source") + fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId" + + fun sourceSharedPref(sourceId: Long) = "source_$sourceId" + + fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" + + fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" + + fun trackToken(syncId: Int) = "track_token_$syncId" + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt index 854afa03d..62c34d422 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -1,36 +1,36 @@ -package eu.kanade.tachiyomi.data.track - -import android.content.Context -import eu.kanade.tachiyomi.data.track.anilist.Anilist -import eu.kanade.tachiyomi.data.track.kitsu.Kitsu -import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist -import eu.kanade.tachiyomi.data.track.shikimori.Shikimori -import eu.kanade.tachiyomi.data.track.bangumi.Bangumi - -class TrackManager(private val context: Context) { - - companion object { - const val MYANIMELIST = 1 - const val ANILIST = 2 - const val KITSU = 3 - const val SHIKIMORI = 4 - const val BANGUMI = 5 - } - - val myAnimeList = Myanimelist(context, MYANIMELIST) - - val aniList = Anilist(context, ANILIST) - - val kitsu = Kitsu(context, KITSU) - - val shikimori = Shikimori(context, SHIKIMORI) - - val bangumi = Bangumi(context, BANGUMI) - - val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi) - - fun getService(id: Int) = services.find { it.id == id } - - fun hasLoggedServices() = services.any { it.isLogged } - -} +package eu.kanade.tachiyomi.data.track + +import android.content.Context +import eu.kanade.tachiyomi.data.track.anilist.Anilist +import eu.kanade.tachiyomi.data.track.kitsu.Kitsu +import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist +import eu.kanade.tachiyomi.data.track.shikimori.Shikimori +import eu.kanade.tachiyomi.data.track.bangumi.Bangumi + +class TrackManager(private val context: Context) { + + companion object { + const val MYANIMELIST = 1 + const val ANILIST = 2 + const val KITSU = 3 + const val SHIKIMORI = 4 + const val BANGUMI = 5 + } + + val myAnimeList = Myanimelist(context, MYANIMELIST) + + val aniList = Anilist(context, ANILIST) + + val kitsu = Kitsu(context, KITSU) + + val shikimori = Shikimori(context, SHIKIMORI) + + val bangumi = Bangumi(context, BANGUMI) + + val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi) + + fun getService(id: Int) = services.find { it.id == id } + + fun hasLoggedServices() = services.any { it.isLogged } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt index 417e8ba5c..736b33cb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt @@ -1,70 +1,70 @@ -package eu.kanade.tachiyomi.data.track - -import androidx.annotation.CallSuper -import androidx.annotation.DrawableRes -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.network.NetworkHelper -import okhttp3.OkHttpClient -import rx.Completable -import rx.Observable -import uy.kohesive.injekt.injectLazy - -abstract class TrackService(val id: Int) { - - val preferences: PreferencesHelper by injectLazy() - val networkService: NetworkHelper by injectLazy() - - open val client: OkHttpClient - get() = networkService.client - - // Name of the manga sync service to display - abstract val name: String - - @DrawableRes - abstract fun getLogo(): Int - - abstract fun getLogoColor(): Int - - abstract fun getStatusList(): List - - abstract fun getStatus(status: Int): String - - abstract fun getScoreList(): List - - open fun indexToScore(index: Int): Float { - return index.toFloat() - } - - abstract fun displayScore(track: Track): String - - abstract fun add(track: Track): Observable - - abstract fun update(track: Track): Observable - - abstract fun bind(track: Track): Observable - - abstract fun search(query: String): Observable> - - abstract fun refresh(track: Track): Observable - - abstract fun login(username: String, password: String): Completable - - @CallSuper - open fun logout() { - preferences.setTrackCredentials(this, "", "") - } - - open val isLogged: Boolean - get() = !getUsername().isEmpty() && - !getPassword().isEmpty() - - fun getUsername() = preferences.trackUsername(this)!! - - fun getPassword() = preferences.trackPassword(this)!! - - fun saveCredentials(username: String, password: String) { - preferences.setTrackCredentials(this, username, password) - } -} +package eu.kanade.tachiyomi.data.track + +import androidx.annotation.CallSuper +import androidx.annotation.DrawableRes +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.network.NetworkHelper +import okhttp3.OkHttpClient +import rx.Completable +import rx.Observable +import uy.kohesive.injekt.injectLazy + +abstract class TrackService(val id: Int) { + + val preferences: PreferencesHelper by injectLazy() + val networkService: NetworkHelper by injectLazy() + + open val client: OkHttpClient + get() = networkService.client + + // Name of the manga sync service to display + abstract val name: String + + @DrawableRes + abstract fun getLogo(): Int + + abstract fun getLogoColor(): Int + + abstract fun getStatusList(): List + + abstract fun getStatus(status: Int): String + + abstract fun getScoreList(): List + + open fun indexToScore(index: Int): Float { + return index.toFloat() + } + + abstract fun displayScore(track: Track): String + + abstract fun add(track: Track): Observable + + abstract fun update(track: Track): Observable + + abstract fun bind(track: Track): Observable + + abstract fun search(query: String): Observable> + + abstract fun refresh(track: Track): Observable + + abstract fun login(username: String, password: String): Completable + + @CallSuper + open fun logout() { + preferences.setTrackCredentials(this, "", "") + } + + open val isLogged: Boolean + get() = !getUsername().isEmpty() && + !getPassword().isEmpty() + + fun getUsername() = preferences.trackUsername(this)!! + + fun getPassword() = preferences.trackPassword(this)!! + + fun saveCredentials(username: String, password: String) { + preferences.setTrackCredentials(this, username, password) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index 95c4f6461..1f862cfef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -1,214 +1,214 @@ -package eu.kanade.tachiyomi.data.track.anilist - -import android.content.Context -import android.graphics.Color -import com.google.gson.Gson -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import rx.Completable -import rx.Observable -import uy.kohesive.injekt.injectLazy - -class Anilist(private val context: Context, id: Int) : TrackService(id) { - - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val ON_HOLD = 3 - const val DROPPED = 4 - const val PLANNING = 5 - const val REPEATING = 6 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - - const val POINT_100 = "POINT_100" - const val POINT_10 = "POINT_10" - const val POINT_10_DECIMAL = "POINT_10_DECIMAL" - const val POINT_5 = "POINT_5" - const val POINT_3 = "POINT_3" - } - - override val name = "AniList" - - private val gson: Gson by injectLazy() - - private val interceptor by lazy { AnilistInterceptor(this, getPassword()) } - - private val api by lazy { AnilistApi(client, interceptor) } - - private val scorePreference = preferences.anilistScoreType() - - init { - // If the preference is an int from APIv1, logout user to force using APIv2 - try { - scorePreference.get() - } catch (e: ClassCastException) { - logout() - scorePreference.delete() - } - } - - override fun getLogo() = R.drawable.al - - override fun getLogoColor() = Color.rgb(18, 25, 35) - - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) - } - - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLANNING -> getString(R.string.plan_to_read) - REPEATING -> getString(R.string.repeating) - else -> "" - } - } - - override fun getScoreList(): List { - return when (scorePreference.getOrDefault()) { - // 10 point - POINT_10 -> IntRange(0, 10).map(Int::toString) - // 100 point - POINT_100 -> IntRange(0, 100).map(Int::toString) - // 5 stars - POINT_5 -> IntRange(0, 5).map { "$it ★" } - // Smiley - POINT_3 -> listOf("-", "😦", "😐", "😊") - // 10 point decimal - POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() } - else -> throw Exception("Unknown score type") - } - } - - override fun indexToScore(index: Int): Float { - return when (scorePreference.getOrDefault()) { - // 10 point - POINT_10 -> index * 10f - // 100 point - POINT_100 -> index.toFloat() - // 5 stars - POINT_5 -> when { - index == 0 -> 0f - else -> index * 20f - 10f - } - // Smiley - POINT_3 -> when { - index == 0 -> 0f - else -> index * 25f + 10f - } - // 10 point decimal - POINT_10_DECIMAL -> index.toFloat() - else -> throw Exception("Unknown score type") - } - } - - override fun displayScore(track: Track): String { - val score = track.score - - return when (scorePreference.getOrDefault()) { - POINT_5 -> when { - score == 0f -> "0 ★" - else -> "${((score + 10) / 20).toInt()} ★" - } - POINT_3 -> when { - score == 0f -> "0" - score <= 35 -> "😦" - score <= 60 -> "😐" - else -> "😊" - } - else -> track.toAnilistScore() - } - } - - override fun add(track: Track): Observable { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { - if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { - track.status = COMPLETED - } - // If user was using API v1 fetch library_id - if (track.library_id == null || track.library_id!! == 0L){ - return api.findLibManga(track, getUsername().toInt()).flatMap { - if (it == null) { - throw Exception("$track not found on user library") - } - track.library_id = it.library_id - api.updateLibManga(track) - } - } - - return api.updateLibManga(track) - } - - override fun bind(track: Track): Observable { - return api.findLibManga(track, getUsername().toInt()) - .flatMap { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - track.library_id = remoteTrack.library_id - update(track) - } else { - // Set default fields if it's not found in the list - track.score = DEFAULT_SCORE.toFloat() - track.status = DEFAULT_STATUS - add(track) - } - } - } - - override fun search(query: String): Observable> { - return api.search(query) - } - - override fun refresh(track: Track): Observable { - return api.getLibManga(track, getUsername().toInt()) - .map { remoteTrack -> - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - track - } - } - - override fun login(username: String, password: String) = login(password) - - fun login(token: String): Completable { - val oauth = api.createOAuth(token) - interceptor.setAuth(oauth) - return api.getCurrentUser().map { (username, scoreType) -> - scorePreference.set(scoreType) - saveCredentials(username.toString(), oauth.access_token) - }.doOnError{ - logout() - }.toCompletable() - } - - override fun logout() { - super.logout() - preferences.trackToken(this).set(null) - interceptor.setAuth(null) - } - - fun saveOAuth(oAuth: OAuth?) { - preferences.trackToken(this).set(gson.toJson(oAuth)) - } - - fun loadOAuth(): OAuth? { - return try { - gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) - } catch (e: Exception) { - null - } - } - -} - +package eu.kanade.tachiyomi.data.track.anilist + +import android.content.Context +import android.graphics.Color +import com.google.gson.Gson +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import rx.Completable +import rx.Observable +import uy.kohesive.injekt.injectLazy + +class Anilist(private val context: Context, id: Int) : TrackService(id) { + + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val ON_HOLD = 3 + const val DROPPED = 4 + const val PLANNING = 5 + const val REPEATING = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + + const val POINT_100 = "POINT_100" + const val POINT_10 = "POINT_10" + const val POINT_10_DECIMAL = "POINT_10_DECIMAL" + const val POINT_5 = "POINT_5" + const val POINT_3 = "POINT_3" + } + + override val name = "AniList" + + private val gson: Gson by injectLazy() + + private val interceptor by lazy { AnilistInterceptor(this, getPassword()) } + + private val api by lazy { AnilistApi(client, interceptor) } + + private val scorePreference = preferences.anilistScoreType() + + init { + // If the preference is an int from APIv1, logout user to force using APIv2 + try { + scorePreference.get() + } catch (e: ClassCastException) { + logout() + scorePreference.delete() + } + } + + override fun getLogo() = R.drawable.al + + override fun getLogoColor() = Color.rgb(18, 25, 35) + + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) + } + + override fun getStatus(status: Int): String = with(context) { + when (status) { + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + ON_HOLD -> getString(R.string.on_hold) + DROPPED -> getString(R.string.dropped) + PLANNING -> getString(R.string.plan_to_read) + REPEATING -> getString(R.string.repeating) + else -> "" + } + } + + override fun getScoreList(): List { + return when (scorePreference.getOrDefault()) { + // 10 point + POINT_10 -> IntRange(0, 10).map(Int::toString) + // 100 point + POINT_100 -> IntRange(0, 100).map(Int::toString) + // 5 stars + POINT_5 -> IntRange(0, 5).map { "$it ★" } + // Smiley + POINT_3 -> listOf("-", "😦", "😐", "😊") + // 10 point decimal + POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() } + else -> throw Exception("Unknown score type") + } + } + + override fun indexToScore(index: Int): Float { + return when (scorePreference.getOrDefault()) { + // 10 point + POINT_10 -> index * 10f + // 100 point + POINT_100 -> index.toFloat() + // 5 stars + POINT_5 -> when { + index == 0 -> 0f + else -> index * 20f - 10f + } + // Smiley + POINT_3 -> when { + index == 0 -> 0f + else -> index * 25f + 10f + } + // 10 point decimal + POINT_10_DECIMAL -> index.toFloat() + else -> throw Exception("Unknown score type") + } + } + + override fun displayScore(track: Track): String { + val score = track.score + + return when (scorePreference.getOrDefault()) { + POINT_5 -> when { + score == 0f -> "0 ★" + else -> "${((score + 10) / 20).toInt()} ★" + } + POINT_3 -> when { + score == 0f -> "0" + score <= 35 -> "😦" + score <= 60 -> "😐" + else -> "😊" + } + else -> track.toAnilistScore() + } + } + + override fun add(track: Track): Observable { + return api.addLibManga(track) + } + + override fun update(track: Track): Observable { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + // If user was using API v1 fetch library_id + if (track.library_id == null || track.library_id!! == 0L){ + return api.findLibManga(track, getUsername().toInt()).flatMap { + if (it == null) { + throw Exception("$track not found on user library") + } + track.library_id = it.library_id + api.updateLibManga(track) + } + } + + return api.updateLibManga(track) + } + + override fun bind(track: Track): Observable { + return api.findLibManga(track, getUsername().toInt()) + .flatMap { remoteTrack -> + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.library_id = remoteTrack.library_id + update(track) + } else { + // Set default fields if it's not found in the list + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + add(track) + } + } + } + + override fun search(query: String): Observable> { + return api.search(query) + } + + override fun refresh(track: Track): Observable { + return api.getLibManga(track, getUsername().toInt()) + .map { remoteTrack -> + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + track + } + } + + override fun login(username: String, password: String) = login(password) + + fun login(token: String): Completable { + val oauth = api.createOAuth(token) + interceptor.setAuth(oauth) + return api.getCurrentUser().map { (username, scoreType) -> + scorePreference.set(scoreType) + saveCredentials(username.toString(), oauth.access_token) + }.doOnError{ + logout() + }.toCompletable() + } + + override fun logout() { + super.logout() + preferences.trackToken(this).set(null) + interceptor.setAuth(null) + } + + fun saveOAuth(oAuth: OAuth?) { + preferences.trackToken(this).set(gson.toJson(oAuth)) + } + + fun loadOAuth(): OAuth? { + return try { + gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) + } catch (e: Exception) { + null + } + } + +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 6698b1cde..11ef51952 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -1,291 +1,291 @@ -package eu.kanade.tachiyomi.data.track.anilist - -import android.net.Uri -import com.github.salomonbrys.kotson.array -import com.github.salomonbrys.kotson.get -import com.github.salomonbrys.kotson.jsonObject -import com.github.salomonbrys.kotson.nullInt -import com.github.salomonbrys.kotson.nullString -import com.github.salomonbrys.kotson.obj -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.network.asObservableSuccess -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody -import rx.Observable -import java.util.Calendar - - -class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { - - private val parser = JsonParser() - private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull() - private val authClient = client.newBuilder().addInterceptor(interceptor).build() - - fun addLibManga(track: Track): Observable { - val query = """ - |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { - |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { - | id - | status - |} - |} - |""".trimMargin() - val variables = jsonObject( - "mangaId" to track.media_id, - "progress" to track.last_chapter_read, - "status" to track.toAnilistStatus() - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = RequestBody.create(jsonMime, payload.toString()) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - netResponse.close() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).obj - track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong - track - } - } - - fun updateLibManga(track: Track): Observable { - val query = """ - |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { - |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { - |id - |status - |progress - |} - |} - |""".trimMargin() - val variables = jsonObject( - "listId" to track.library_id, - "progress" to track.last_chapter_read, - "status" to track.toAnilistStatus(), - "score" to track.score.toInt() - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = RequestBody.create(jsonMime, payload.toString()) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { - track - } - } - - fun search(search: String): Observable> { - val query = """ - |query Search(${'$'}query: String) { - |Page (perPage: 50) { - |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { - |id - |title { - |romaji - |} - |coverImage { - |large - |} - |type - |status - |chapters - |description - |startDate { - |year - |month - |day - |} - |} - |} - |} - |""".trimMargin() - val variables = jsonObject( - "query" to search - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = RequestBody.create(jsonMime, payload.toString()) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).obj - val data = response["data"]!!.obj - val page = data["Page"].obj - val media = page["media"].array - val entries = media.map { jsonToALManga(it.obj) } - entries.map { it.toTrack() } - } - } - - - fun findLibManga(track: Track, userid: Int): Observable { - val query = """ - |query (${'$'}id: Int!, ${'$'}manga_id: Int!) { - |Page { - |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { - |id - |status - |scoreRaw: score(format: POINT_100) - |progress - |media { - |id - |title { - |romaji - |} - |coverImage { - |large - |} - |type - |status - |chapters - |description - |startDate { - |year - |month - |day - |} - |} - |} - |} - |} - |""".trimMargin() - val variables = jsonObject( - "id" to userid, - "manga_id" to track.media_id - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = RequestBody.create(jsonMime, payload.toString()) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).obj - val data = response["data"]!!.obj - val page = data["Page"].obj - val media = page["mediaList"].array - val entries = media.map { jsonToALUserManga(it.obj) } - entries.firstOrNull()?.toTrack() - - } - } - - fun getLibManga(track: Track, userid: Int): Observable { - return findLibManga(track, userid) - .map { it ?: throw Exception("Could not find manga") } - } - - fun createOAuth(token: String): OAuth { - return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) - } - - fun getCurrentUser(): Observable> { - val query = """ - |query User { - |Viewer { - |id - |mediaListOptions { - |scoreFormat - |} - |} - |} - |""".trimMargin() - val payload = jsonObject( - "query" to query - ) - val body = RequestBody.create(jsonMime, payload.toString()) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).obj - val data = response["data"]!!.obj - val viewer = data["Viewer"].obj - Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) - } - } - - private fun jsonToALManga(struct: JsonObject): ALManga { - val date = try { - val date = Calendar.getInstance() - date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1, - struct["startDate"]["day"].nullInt ?: 0) - date.timeInMillis - } catch (_: Exception) { - 0L - } - - return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, - struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString, - date, struct["chapters"].nullInt ?: 0) - } - - private fun jsonToALUserManga(struct: JsonObject): ALUserManga { - return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj)) - } - - companion object { - private const val clientId = "385" - private const val clientUrl = "tachiyomi://anilist-auth" - private const val apiUrl = "https://graphql.anilist.co/" - private const val baseUrl = "https://anilist.co/api/v2/" - private const val baseMangaUrl = "https://anilist.co/manga/" - - fun mangaUrl(mediaId: Int): String { - return baseMangaUrl + mediaId - } - - fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("response_type", "token") - .build() - } - -} +package eu.kanade.tachiyomi.data.track.anilist + +import android.net.Uri +import com.github.salomonbrys.kotson.array +import com.github.salomonbrys.kotson.get +import com.github.salomonbrys.kotson.jsonObject +import com.github.salomonbrys.kotson.nullInt +import com.github.salomonbrys.kotson.nullString +import com.github.salomonbrys.kotson.obj +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.network.asObservableSuccess +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import rx.Observable +import java.util.Calendar + + +class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { + + private val parser = JsonParser() + private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull() + private val authClient = client.newBuilder().addInterceptor(interceptor).build() + + fun addLibManga(track: Track): Observable { + val query = """ + |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { + |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { + | id + | status + |} + |} + |""".trimMargin() + val variables = jsonObject( + "mangaId" to track.media_id, + "progress" to track.last_chapter_read, + "status" to track.toAnilistStatus() + ) + val payload = jsonObject( + "query" to query, + "variables" to variables + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) + .build() + return authClient.newCall(request) + .asObservableSuccess() + .map { netResponse -> + val responseBody = netResponse.body?.string().orEmpty() + netResponse.close() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = parser.parse(responseBody).obj + track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong + track + } + } + + fun updateLibManga(track: Track): Observable { + val query = """ + |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { + |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { + |id + |status + |progress + |} + |} + |""".trimMargin() + val variables = jsonObject( + "listId" to track.library_id, + "progress" to track.last_chapter_read, + "status" to track.toAnilistStatus(), + "score" to track.score.toInt() + ) + val payload = jsonObject( + "query" to query, + "variables" to variables + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) + .build() + return authClient.newCall(request) + .asObservableSuccess() + .map { + track + } + } + + fun search(search: String): Observable> { + val query = """ + |query Search(${'$'}query: String) { + |Page (perPage: 50) { + |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { + |id + |title { + |romaji + |} + |coverImage { + |large + |} + |type + |status + |chapters + |description + |startDate { + |year + |month + |day + |} + |} + |} + |} + |""".trimMargin() + val variables = jsonObject( + "query" to search + ) + val payload = jsonObject( + "query" to query, + "variables" to variables + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) + .build() + return authClient.newCall(request) + .asObservableSuccess() + .map { netResponse -> + val responseBody = netResponse.body?.string().orEmpty() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = parser.parse(responseBody).obj + val data = response["data"]!!.obj + val page = data["Page"].obj + val media = page["media"].array + val entries = media.map { jsonToALManga(it.obj) } + entries.map { it.toTrack() } + } + } + + + fun findLibManga(track: Track, userid: Int): Observable { + val query = """ + |query (${'$'}id: Int!, ${'$'}manga_id: Int!) { + |Page { + |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { + |id + |status + |scoreRaw: score(format: POINT_100) + |progress + |media { + |id + |title { + |romaji + |} + |coverImage { + |large + |} + |type + |status + |chapters + |description + |startDate { + |year + |month + |day + |} + |} + |} + |} + |} + |""".trimMargin() + val variables = jsonObject( + "id" to userid, + "manga_id" to track.media_id + ) + val payload = jsonObject( + "query" to query, + "variables" to variables + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) + .build() + return authClient.newCall(request) + .asObservableSuccess() + .map { netResponse -> + val responseBody = netResponse.body?.string().orEmpty() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = parser.parse(responseBody).obj + val data = response["data"]!!.obj + val page = data["Page"].obj + val media = page["mediaList"].array + val entries = media.map { jsonToALUserManga(it.obj) } + entries.firstOrNull()?.toTrack() + + } + } + + fun getLibManga(track: Track, userid: Int): Observable { + return findLibManga(track, userid) + .map { it ?: throw Exception("Could not find manga") } + } + + fun createOAuth(token: String): OAuth { + return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) + } + + fun getCurrentUser(): Observable> { + val query = """ + |query User { + |Viewer { + |id + |mediaListOptions { + |scoreFormat + |} + |} + |} + |""".trimMargin() + val payload = jsonObject( + "query" to query + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) + .build() + return authClient.newCall(request) + .asObservableSuccess() + .map { netResponse -> + val responseBody = netResponse.body?.string().orEmpty() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = parser.parse(responseBody).obj + val data = response["data"]!!.obj + val viewer = data["Viewer"].obj + Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) + } + } + + private fun jsonToALManga(struct: JsonObject): ALManga { + val date = try { + val date = Calendar.getInstance() + date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1, + struct["startDate"]["day"].nullInt ?: 0) + date.timeInMillis + } catch (_: Exception) { + 0L + } + + return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, + struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString, + date, struct["chapters"].nullInt ?: 0) + } + + private fun jsonToALUserManga(struct: JsonObject): ALUserManga { + return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj)) + } + + companion object { + private const val clientId = "385" + private const val clientUrl = "tachiyomi://anilist-auth" + private const val apiUrl = "https://graphql.anilist.co/" + private const val baseUrl = "https://anilist.co/api/v2/" + private const val baseMangaUrl = "https://anilist.co/manga/" + + fun mangaUrl(mediaId: Int): String { + return baseMangaUrl + mediaId + } + + fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() + .appendQueryParameter("client_id", clientId) + .appendQueryParameter("response_type", "token") + .build() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index 427b0acfe..ff416a1c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -1,58 +1,58 @@ -package eu.kanade.tachiyomi.data.track.anilist - -import okhttp3.Interceptor -import okhttp3.Response - - -class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor { - - /** - * OAuth object used for authenticated requests. - * - * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute - * before its original expiration date. - */ - private var oauth: OAuth? = null - set(value) { - field = value?.copy(expires = value.expires * 1000 - 60 * 1000) - } - - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() - - if (token.isNullOrEmpty()) { - throw Exception("Not authenticated with Anilist") - } - if (oauth == null){ - oauth = anilist.loadOAuth() - } - // Refresh access token if null or expired. - if (oauth!!.isExpired()) { - anilist.logout() - throw Exception("Token expired") - } - - // Throw on null auth. - if (oauth == null) { - throw Exception("No authentication token") - } - - // Add the authorization header to the original request. - val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") - .build() - - return chain.proceed(authRequest) - } - - /** - * Called when the user authenticates with Anilist for the first time. Sets the refresh token - * and the oauth object. - */ - fun setAuth(oauth: OAuth?) { - token = oauth?.access_token - this.oauth = oauth - anilist.saveOAuth(oauth) - } - +package eu.kanade.tachiyomi.data.track.anilist + +import okhttp3.Interceptor +import okhttp3.Response + + +class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor { + + /** + * OAuth object used for authenticated requests. + * + * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute + * before its original expiration date. + */ + private var oauth: OAuth? = null + set(value) { + field = value?.copy(expires = value.expires * 1000 - 60 * 1000) + } + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + if (token.isNullOrEmpty()) { + throw Exception("Not authenticated with Anilist") + } + if (oauth == null){ + oauth = anilist.loadOAuth() + } + // Refresh access token if null or expired. + if (oauth!!.isExpired()) { + anilist.logout() + throw Exception("Token expired") + } + + // Throw on null auth. + if (oauth == null) { + throw Exception("No authentication token") + } + + // Add the authorization header to the original request. + val authRequest = originalRequest.newBuilder() + .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .build() + + return chain.proceed(authRequest) + } + + /** + * Called when the user authenticates with Anilist for the first time. Sets the refresh token + * and the oauth object. + */ + fun setAuth(oauth: OAuth?) { + token = oauth?.access_token + this.oauth = oauth + anilist.saveOAuth(oauth) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt index 1d7a31ac5..a53760ba5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt @@ -1,10 +1,10 @@ -package eu.kanade.tachiyomi.data.track.anilist - -data class OAuth( - val access_token: String, - val token_type: String, - val expires: Long, - val expires_in: Long) { - - fun isExpired() = System.currentTimeMillis() > expires +package eu.kanade.tachiyomi.data.track.anilist + +data class OAuth( + val access_token: String, + val token_type: String, + val expires: Long, + val expires_in: Long) { + + fun isExpired() = System.currentTimeMillis() > expires } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index 1eb6fff59..0d93e1fb7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -1,144 +1,144 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -import android.content.Context -import android.graphics.Color -import com.google.gson.Gson -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import rx.Completable -import rx.Observable -import uy.kohesive.injekt.injectLazy - -class Bangumi(private val context: Context, id: Int) : TrackService(id) { - - override fun getScoreList(): List { - return IntRange(0, 10).map(Int::toString) - } - - override fun displayScore(track: Track): String { - return track.score.toInt().toString() - } - - override fun add(track: Track): Observable { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { - if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { - track.status = COMPLETED - } - return api.updateLibManga(track) - } - - override fun bind(track: Track): Observable { - return api.statusLibManga(track) - .flatMap { - api.findLibManga(track).flatMap { remoteTrack -> - if (remoteTrack != null && it != null) { - track.copyPersonalFrom(remoteTrack) - track.library_id = remoteTrack.library_id - track.status = remoteTrack.status - track.last_chapter_read = remoteTrack.last_chapter_read - update(track) - } else { - // Set default fields if it's not found in the list - track.score = DEFAULT_SCORE.toFloat() - track.status = DEFAULT_STATUS - add(track) - update(track) - } - } - } - } - - override fun search(query: String): Observable> { - return api.search(query) - } - - override fun refresh(track: Track): Observable { - return api.statusLibManga(track) - .flatMap { - track.copyPersonalFrom(it!!) - api.findLibManga(track) - .map { remoteTrack -> - if (remoteTrack != null) { - track.total_chapters = remoteTrack.total_chapters - track.status = remoteTrack.status - } - track - } - } - } - - companion object { - const val READING = 3 - const val COMPLETED = 2 - const val ON_HOLD = 4 - const val DROPPED = 5 - const val PLANNING = 1 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - } - - override val name = "Bangumi" - - private val gson: Gson by injectLazy() - - private val interceptor by lazy { BangumiInterceptor(this, gson) } - - private val api by lazy { BangumiApi(client, interceptor) } - - override fun getLogo() = R.drawable.bangumi - - override fun getLogoColor() = Color.rgb(0xF0, 0x91, 0x99) - - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING) - } - - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLANNING -> getString(R.string.plan_to_read) - else -> "" - } - } - - override fun login(username: String, password: String) = login(password) - - fun login(code: String): Completable { - return api.accessToken(code).map { oauth: OAuth? -> - interceptor.newAuth(oauth) - if (oauth != null) { - saveCredentials(oauth.user_id.toString(), oauth.access_token) - } - }.doOnError { - logout() - }.toCompletable() - } - - fun saveToken(oauth: OAuth?) { - val json = gson.toJson(oauth) - preferences.trackToken(this).set(json) - } - - fun restoreToken(): OAuth? { - return try { - gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) - } catch (e: Exception) { - null - } - } - - override fun logout() { - super.logout() - preferences.trackToken(this).set(null) - interceptor.newAuth(null) - } -} +package eu.kanade.tachiyomi.data.track.bangumi + +import android.content.Context +import android.graphics.Color +import com.google.gson.Gson +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import rx.Completable +import rx.Observable +import uy.kohesive.injekt.injectLazy + +class Bangumi(private val context: Context, id: Int) : TrackService(id) { + + override fun getScoreList(): List { + return IntRange(0, 10).map(Int::toString) + } + + override fun displayScore(track: Track): String { + return track.score.toInt().toString() + } + + override fun add(track: Track): Observable { + return api.addLibManga(track) + } + + override fun update(track: Track): Observable { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + return api.updateLibManga(track) + } + + override fun bind(track: Track): Observable { + return api.statusLibManga(track) + .flatMap { + api.findLibManga(track).flatMap { remoteTrack -> + if (remoteTrack != null && it != null) { + track.copyPersonalFrom(remoteTrack) + track.library_id = remoteTrack.library_id + track.status = remoteTrack.status + track.last_chapter_read = remoteTrack.last_chapter_read + update(track) + } else { + // Set default fields if it's not found in the list + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + add(track) + update(track) + } + } + } + } + + override fun search(query: String): Observable> { + return api.search(query) + } + + override fun refresh(track: Track): Observable { + return api.statusLibManga(track) + .flatMap { + track.copyPersonalFrom(it!!) + api.findLibManga(track) + .map { remoteTrack -> + if (remoteTrack != null) { + track.total_chapters = remoteTrack.total_chapters + track.status = remoteTrack.status + } + track + } + } + } + + companion object { + const val READING = 3 + const val COMPLETED = 2 + const val ON_HOLD = 4 + const val DROPPED = 5 + const val PLANNING = 1 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + } + + override val name = "Bangumi" + + private val gson: Gson by injectLazy() + + private val interceptor by lazy { BangumiInterceptor(this, gson) } + + private val api by lazy { BangumiApi(client, interceptor) } + + override fun getLogo() = R.drawable.bangumi + + override fun getLogoColor() = Color.rgb(0xF0, 0x91, 0x99) + + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING) + } + + override fun getStatus(status: Int): String = with(context) { + when (status) { + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + ON_HOLD -> getString(R.string.on_hold) + DROPPED -> getString(R.string.dropped) + PLANNING -> getString(R.string.plan_to_read) + else -> "" + } + } + + override fun login(username: String, password: String) = login(password) + + fun login(code: String): Completable { + return api.accessToken(code).map { oauth: OAuth? -> + interceptor.newAuth(oauth) + if (oauth != null) { + saveCredentials(oauth.user_id.toString(), oauth.access_token) + } + }.doOnError { + logout() + }.toCompletable() + } + + fun saveToken(oauth: OAuth?) { + val json = gson.toJson(oauth) + preferences.trackToken(this).set(json) + } + + fun restoreToken(): OAuth? { + return try { + gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) + } catch (e: Exception) { + null + } + } + + override fun logout() { + super.logout() + preferences.trackToken(this).set(null) + interceptor.newAuth(null) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt index 68dc7e5c4..8674b6134 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt @@ -1,16 +1,16 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?, - val user_id: Long? -) { - - // Access token refersh before expired - fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) - -} - +package eu.kanade.tachiyomi.data.track.bangumi + +data class OAuth( + val access_token: String, + val token_type: String, + val created_at: Long, + val expires_in: Long, + val refresh_token: String?, + val user_id: Long? +) { + + // Access token refersh before expired + fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) + +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index 14be0ddb7..97741fd54 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -1,144 +1,144 @@ -package eu.kanade.tachiyomi.data.track.kitsu - -import android.content.Context -import android.graphics.Color -import com.google.gson.Gson -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import rx.Completable -import rx.Observable -import uy.kohesive.injekt.injectLazy -import java.text.DecimalFormat - -class Kitsu(private val context: Context, id: Int) : TrackService(id) { - - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val ON_HOLD = 3 - const val DROPPED = 4 - const val PLAN_TO_READ = 5 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0f - } - - override val name = "Kitsu" - - private val gson: Gson by injectLazy() - - private val interceptor by lazy { KitsuInterceptor(this, gson) } - - private val api by lazy { KitsuApi(client, interceptor) } - - override fun getLogo(): Int { - return R.drawable.kitsu - } - - override fun getLogoColor(): Int { - return Color.rgb(51, 37, 50) - } - - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) - } - - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLAN_TO_READ -> getString(R.string.plan_to_read) - else -> "" - } - } - - override fun getScoreList(): List { - val df = DecimalFormat("0.#") - return listOf("0") + IntRange(2, 20).map { df.format(it / 2f) } - } - - override fun indexToScore(index: Int): Float { - return if (index > 0) (index + 1) / 2f else 0f - } - - override fun displayScore(track: Track): String { - val df = DecimalFormat("0.#") - return df.format(track.score) - } - - override fun add(track: Track): Observable { - return api.addLibManga(track, getUserId()) - } - - override fun update(track: Track): Observable { - if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { - track.status = COMPLETED - } - - return api.updateLibManga(track) - } - - override fun bind(track: Track): Observable { - return api.findLibManga(track, getUserId()) - .flatMap { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - track.media_id = remoteTrack.media_id - update(track) - } else { - track.score = DEFAULT_SCORE - track.status = DEFAULT_STATUS - add(track) - } - } - } - - override fun search(query: String): Observable> { - return api.search(query) - } - - override fun refresh(track: Track): Observable { - return api.getLibManga(track) - .map { remoteTrack -> - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - track - } - } - - override fun login(username: String, password: String): Completable { - return api.login(username, password) - .doOnNext { interceptor.newAuth(it) } - .flatMap { api.getCurrentUser() } - .doOnNext { userId -> saveCredentials(username, userId) } - .doOnError { logout() } - .toCompletable() - } - - override fun logout() { - super.logout() - interceptor.newAuth(null) - } - - private fun getUserId(): String { - return getPassword() - } - - fun saveToken(oauth: OAuth?) { - val json = gson.toJson(oauth) - preferences.trackToken(this).set(json) - } - - fun restoreToken(): OAuth? { - return try { - gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) - } catch (e: Exception) { - null - } - } - -} +package eu.kanade.tachiyomi.data.track.kitsu + +import android.content.Context +import android.graphics.Color +import com.google.gson.Gson +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import rx.Completable +import rx.Observable +import uy.kohesive.injekt.injectLazy +import java.text.DecimalFormat + +class Kitsu(private val context: Context, id: Int) : TrackService(id) { + + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val ON_HOLD = 3 + const val DROPPED = 4 + const val PLAN_TO_READ = 5 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0f + } + + override val name = "Kitsu" + + private val gson: Gson by injectLazy() + + private val interceptor by lazy { KitsuInterceptor(this, gson) } + + private val api by lazy { KitsuApi(client, interceptor) } + + override fun getLogo(): Int { + return R.drawable.kitsu + } + + override fun getLogoColor(): Int { + return Color.rgb(51, 37, 50) + } + + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) + } + + override fun getStatus(status: Int): String = with(context) { + when (status) { + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + ON_HOLD -> getString(R.string.on_hold) + DROPPED -> getString(R.string.dropped) + PLAN_TO_READ -> getString(R.string.plan_to_read) + else -> "" + } + } + + override fun getScoreList(): List { + val df = DecimalFormat("0.#") + return listOf("0") + IntRange(2, 20).map { df.format(it / 2f) } + } + + override fun indexToScore(index: Int): Float { + return if (index > 0) (index + 1) / 2f else 0f + } + + override fun displayScore(track: Track): String { + val df = DecimalFormat("0.#") + return df.format(track.score) + } + + override fun add(track: Track): Observable { + return api.addLibManga(track, getUserId()) + } + + override fun update(track: Track): Observable { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + + return api.updateLibManga(track) + } + + override fun bind(track: Track): Observable { + return api.findLibManga(track, getUserId()) + .flatMap { remoteTrack -> + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.media_id = remoteTrack.media_id + update(track) + } else { + track.score = DEFAULT_SCORE + track.status = DEFAULT_STATUS + add(track) + } + } + } + + override fun search(query: String): Observable> { + return api.search(query) + } + + override fun refresh(track: Track): Observable { + return api.getLibManga(track) + .map { remoteTrack -> + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + track + } + } + + override fun login(username: String, password: String): Completable { + return api.login(username, password) + .doOnNext { interceptor.newAuth(it) } + .flatMap { api.getCurrentUser() } + .doOnNext { userId -> saveCredentials(username, userId) } + .doOnError { logout() } + .toCompletable() + } + + override fun logout() { + super.logout() + interceptor.newAuth(null) + } + + private fun getUserId(): String { + return getPassword() + } + + fun saveToken(oauth: OAuth?) { + val json = gson.toJson(oauth) + preferences.trackToken(this).set(json) + } + + fun restoreToken(): OAuth? { + return try { + gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) + } catch (e: Exception) { + null + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt index e9f2ae401..678567ce9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt @@ -1,11 +1,11 @@ -package eu.kanade.tachiyomi.data.track.kitsu - -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?) { - - fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) +package eu.kanade.tachiyomi.data.track.kitsu + +data class OAuth( + val access_token: String, + val token_type: String, + val created_at: Long, + val expires_in: Long, + val refresh_token: String?) { + + fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index a57012447..083060016 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -1,163 +1,163 @@ -package eu.kanade.tachiyomi.data.track.myanimelist - -import android.content.Context -import android.graphics.Color -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import rx.Completable -import rx.Observable - -class Myanimelist(private val context: Context, id: Int) : TrackService(id) { - - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val ON_HOLD = 3 - const val DROPPED = 4 - const val PLAN_TO_READ = 6 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - - const val BASE_URL = "https://myanimelist.net" - const val USER_SESSION_COOKIE = "MALSESSIONID" - const val LOGGED_IN_COOKIE = "is_logged_in" - } - - private val interceptor by lazy { MyAnimeListInterceptor(this) } - private val api by lazy { MyanimelistApi(client, interceptor) } - - override val name: String - get() = "MyAnimeList" - - override fun getLogo() = R.drawable.mal - - override fun getLogoColor() = Color.rgb(46, 81, 162) - - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLAN_TO_READ -> getString(R.string.plan_to_read) - else -> "" - } - } - - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) - } - - override fun getScoreList(): List { - return IntRange(0, 10).map(Int::toString) - } - - override fun displayScore(track: Track): String { - return track.score.toInt().toString() - } - - override fun add(track: Track): Observable { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { - if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { - track.status = COMPLETED - } - - return api.updateLibManga(track) - } - - override fun bind(track: Track): Observable { - return api.findLibManga(track) - .flatMap { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - update(track) - } else { - // Set default fields if it's not found in the list - track.score = DEFAULT_SCORE.toFloat() - track.status = DEFAULT_STATUS - add(track) - } - } - } - - override fun search(query: String): Observable> { - return api.search(query) - } - - override fun refresh(track: Track): Observable { - return api.getLibManga(track) - .map { remoteTrack -> - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - track - } - } - - override fun login(username: String, password: String): Completable { - logout() - - return Observable.fromCallable { api.login(username, password) } - .doOnNext { csrf -> saveCSRF(csrf) } - .doOnNext { saveCredentials(username, password) } - .doOnError { logout() } - .toCompletable() - } - - fun refreshLogin() { - val username = getUsername() - val password = getPassword() - logout() - - try { - val csrf = api.login(username, password) - saveCSRF(csrf) - saveCredentials(username, password) - } catch (e: Exception) { - logout() - throw e - } - } - - // Attempt to login again if cookies have been cleared but credentials are still filled - fun ensureLoggedIn() { - if (isAuthorized) return - if (!isLogged) throw Exception("MAL Login Credentials not found") - - refreshLogin() - } - - override fun logout() { - super.logout() - preferences.trackToken(this).delete() - networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!) - } - - val isAuthorized: Boolean - get() = super.isLogged && - getCSRF().isNotEmpty() && - checkCookies() - - fun getCSRF(): String = preferences.trackToken(this).getOrDefault() - - private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf) - - private fun checkCookies(): Boolean { - var ckCount = 0 - val url = BASE_URL.toHttpUrlOrNull()!! - for (ck in networkService.cookieManager.get(url)) { - if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE) - ckCount++ - } - - return ckCount == 2 - } - -} +package eu.kanade.tachiyomi.data.track.myanimelist + +import android.content.Context +import android.graphics.Color +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import rx.Completable +import rx.Observable + +class Myanimelist(private val context: Context, id: Int) : TrackService(id) { + + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val ON_HOLD = 3 + const val DROPPED = 4 + const val PLAN_TO_READ = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + + const val BASE_URL = "https://myanimelist.net" + const val USER_SESSION_COOKIE = "MALSESSIONID" + const val LOGGED_IN_COOKIE = "is_logged_in" + } + + private val interceptor by lazy { MyAnimeListInterceptor(this) } + private val api by lazy { MyanimelistApi(client, interceptor) } + + override val name: String + get() = "MyAnimeList" + + override fun getLogo() = R.drawable.mal + + override fun getLogoColor() = Color.rgb(46, 81, 162) + + override fun getStatus(status: Int): String = with(context) { + when (status) { + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + ON_HOLD -> getString(R.string.on_hold) + DROPPED -> getString(R.string.dropped) + PLAN_TO_READ -> getString(R.string.plan_to_read) + else -> "" + } + } + + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) + } + + override fun getScoreList(): List { + return IntRange(0, 10).map(Int::toString) + } + + override fun displayScore(track: Track): String { + return track.score.toInt().toString() + } + + override fun add(track: Track): Observable { + return api.addLibManga(track) + } + + override fun update(track: Track): Observable { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + + return api.updateLibManga(track) + } + + override fun bind(track: Track): Observable { + return api.findLibManga(track) + .flatMap { remoteTrack -> + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + update(track) + } else { + // Set default fields if it's not found in the list + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + add(track) + } + } + } + + override fun search(query: String): Observable> { + return api.search(query) + } + + override fun refresh(track: Track): Observable { + return api.getLibManga(track) + .map { remoteTrack -> + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + track + } + } + + override fun login(username: String, password: String): Completable { + logout() + + return Observable.fromCallable { api.login(username, password) } + .doOnNext { csrf -> saveCSRF(csrf) } + .doOnNext { saveCredentials(username, password) } + .doOnError { logout() } + .toCompletable() + } + + fun refreshLogin() { + val username = getUsername() + val password = getPassword() + logout() + + try { + val csrf = api.login(username, password) + saveCSRF(csrf) + saveCredentials(username, password) + } catch (e: Exception) { + logout() + throw e + } + } + + // Attempt to login again if cookies have been cleared but credentials are still filled + fun ensureLoggedIn() { + if (isAuthorized) return + if (!isLogged) throw Exception("MAL Login Credentials not found") + + refreshLogin() + } + + override fun logout() { + super.logout() + preferences.trackToken(this).delete() + networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!) + } + + val isAuthorized: Boolean + get() = super.isLogged && + getCSRF().isNotEmpty() && + checkCookies() + + fun getCSRF(): String = preferences.trackToken(this).getOrDefault() + + private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf) + + private fun checkCookies(): Boolean { + var ckCount = 0 + val url = BASE_URL.toHttpUrlOrNull()!! + for (ck in networkService.cookieManager.get(url)) { + if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE) + ckCount++ + } + + return ckCount == 2 + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt index 118e584e7..1f6a38b47 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt @@ -1,13 +1,13 @@ -package eu.kanade.tachiyomi.data.track.shikimori - -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?) { - - // Access token lives 1 day - fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) -} - +package eu.kanade.tachiyomi.data.track.shikimori + +data class OAuth( + val access_token: String, + val token_type: String, + val created_at: Long, + val expires_in: Long, + val refresh_token: String?) { + + // Access token lives 1 day + fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index 8068e6d55..4c818d5fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -1,139 +1,139 @@ -package eu.kanade.tachiyomi.data.track.shikimori - -import android.content.Context -import android.graphics.Color -import android.util.Log -import com.google.gson.Gson -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import rx.Completable -import rx.Observable -import uy.kohesive.injekt.injectLazy - -class Shikimori(private val context: Context, id: Int) : TrackService(id) { - - override fun getScoreList(): List { - return IntRange(0, 10).map(Int::toString) - } - - override fun displayScore(track: Track): String { - return track.score.toInt().toString() - } - - override fun add(track: Track): Observable { - return api.addLibManga(track, getUsername()) - } - - override fun update(track: Track): Observable { - if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { - track.status = COMPLETED - } - return api.updateLibManga(track, getUsername()) - } - - override fun bind(track: Track): Observable { - return api.findLibManga(track, getUsername()) - .flatMap { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - track.library_id = remoteTrack.library_id - update(track) - } else { - // Set default fields if it's not found in the list - track.score = DEFAULT_SCORE.toFloat() - track.status = DEFAULT_STATUS - add(track) - } - } - } - - override fun search(query: String): Observable> { - return api.search(query) - } - - override fun refresh(track: Track): Observable { - return api.findLibManga(track, getUsername()) - .map { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - } - track - } - } - - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val ON_HOLD = 3 - const val DROPPED = 4 - const val PLANNING = 5 - const val REPEATING = 6 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - } - - override val name = "Shikimori" - - private val gson: Gson by injectLazy() - - private val interceptor by lazy { ShikimoriInterceptor(this, gson) } - - private val api by lazy { ShikimoriApi(client, interceptor) } - - override fun getLogo() = R.drawable.shikimori - - override fun getLogoColor() = Color.rgb(40, 40, 40) - - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) - } - - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLANNING -> getString(R.string.plan_to_read) - REPEATING -> getString(R.string.repeating) - else -> "" - } - } - - override fun login(username: String, password: String) = login(password) - - fun login(code: String): Completable { - return api.accessToken(code).map { oauth: OAuth? -> - interceptor.newAuth(oauth) - if (oauth != null) { - val user = api.getCurrentUser() - saveCredentials(user.toString(), oauth.access_token) - } - }.doOnError { - logout() - }.toCompletable() - } - - fun saveToken(oauth: OAuth?) { - val json = gson.toJson(oauth) - preferences.trackToken(this).set(json) - } - - fun restoreToken(): OAuth? { - return try { - gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) - } catch (e: Exception) { - null - } - } - - override fun logout() { - super.logout() - preferences.trackToken(this).set(null) - interceptor.newAuth(null) - } -} +package eu.kanade.tachiyomi.data.track.shikimori + +import android.content.Context +import android.graphics.Color +import android.util.Log +import com.google.gson.Gson +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import rx.Completable +import rx.Observable +import uy.kohesive.injekt.injectLazy + +class Shikimori(private val context: Context, id: Int) : TrackService(id) { + + override fun getScoreList(): List { + return IntRange(0, 10).map(Int::toString) + } + + override fun displayScore(track: Track): String { + return track.score.toInt().toString() + } + + override fun add(track: Track): Observable { + return api.addLibManga(track, getUsername()) + } + + override fun update(track: Track): Observable { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + return api.updateLibManga(track, getUsername()) + } + + override fun bind(track: Track): Observable { + return api.findLibManga(track, getUsername()) + .flatMap { remoteTrack -> + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.library_id = remoteTrack.library_id + update(track) + } else { + // Set default fields if it's not found in the list + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + add(track) + } + } + } + + override fun search(query: String): Observable> { + return api.search(query) + } + + override fun refresh(track: Track): Observable { + return api.findLibManga(track, getUsername()) + .map { remoteTrack -> + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + } + track + } + } + + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val ON_HOLD = 3 + const val DROPPED = 4 + const val PLANNING = 5 + const val REPEATING = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + } + + override val name = "Shikimori" + + private val gson: Gson by injectLazy() + + private val interceptor by lazy { ShikimoriInterceptor(this, gson) } + + private val api by lazy { ShikimoriApi(client, interceptor) } + + override fun getLogo() = R.drawable.shikimori + + override fun getLogoColor() = Color.rgb(40, 40, 40) + + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) + } + + override fun getStatus(status: Int): String = with(context) { + when (status) { + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + ON_HOLD -> getString(R.string.on_hold) + DROPPED -> getString(R.string.dropped) + PLANNING -> getString(R.string.plan_to_read) + REPEATING -> getString(R.string.repeating) + else -> "" + } + } + + override fun login(username: String, password: String) = login(password) + + fun login(code: String): Completable { + return api.accessToken(code).map { oauth: OAuth? -> + interceptor.newAuth(oauth) + if (oauth != null) { + val user = api.getCurrentUser() + saveCredentials(user.toString(), oauth.access_token) + } + }.doOnError { + logout() + }.toCompletable() + } + + fun saveToken(oauth: OAuth?) { + val json = gson.toJson(oauth) + preferences.trackToken(this).set(json) + } + + fun restoreToken(): OAuth? { + return try { + gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) + } catch (e: Exception) { + null + } + } + + override fun logout() { + super.logout() + preferences.trackToken(this).set(null) + interceptor.newAuth(null) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt index a34475d4c..7a320ba49 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -1,154 +1,154 @@ -package eu.kanade.tachiyomi.network - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.webkit.WebResourceResponse -import android.webkit.WebSettings -import android.webkit.WebView -import eu.kanade.tachiyomi.util.WebViewClientCompat -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import java.io.IOException -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -class CloudflareInterceptor(private val context: Context) : Interceptor { - - private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare") - - private val handler = Handler(Looper.getMainLooper()) - - /** - * When this is called, it initializes the WebView if it wasn't already. We use this to avoid - * blocking the main thread too much. If used too often we could consider moving it to the - * Application class. - */ - private val initWebView by lazy { - if (Build.VERSION.SDK_INT >= 17) { - WebSettings.getDefaultUserAgent(context) - } else { - null - } - } - - @Synchronized - override fun intercept(chain: Interceptor.Chain): Response { - initWebView - - val response = chain.proceed(chain.request()) - - // Check if Cloudflare anti-bot is on - if (response.code == 503 && response.header("Server") in serverCheck) { - try { - response.close() - val solutionRequest = resolveWithWebView(chain.request()) - return chain.proceed(solutionRequest) - } catch (e: Exception) { - // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that - // we don't crash the entire app - throw IOException(e) - } - } - - return response - } - - private fun isChallengeSolutionUrl(url: String): Boolean { - return "chk_jschl" in url - } - - @SuppressLint("SetJavaScriptEnabled") - private fun resolveWithWebView(request: Request): Request { - // We need to lock this thread until the WebView finds the challenge solution url, because - // OkHttp doesn't support asynchronous interceptors. - val latch = CountDownLatch(1) - - var webView: WebView? = null - var solutionUrl: String? = null - var challengeFound = false - - val origRequestUrl = request.url.toString() - val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } - - handler.post { - val view = WebView(context) - webView = view - view.settings.javaScriptEnabled = true - view.settings.userAgentString = request.header("User-Agent") - view.webViewClient = object : WebViewClientCompat() { - - override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { - if (isChallengeSolutionUrl(url)) { - solutionUrl = url - latch.countDown() - } - return solutionUrl != null - } - - override fun shouldInterceptRequestCompat( - view: WebView, - url: String - ): WebResourceResponse? { - if (solutionUrl != null) { - // Intercept any request when we have the solution. - return WebResourceResponse("text/plain", "UTF-8", null) - } - return null - } - - override fun onPageFinished(view: WebView, url: String) { - // Http error codes are only received since M - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - url == origRequestUrl && !challengeFound - ) { - // The first request didn't return the challenge, abort. - latch.countDown() - } - } - - override fun onReceivedErrorCompat( - view: WebView, - errorCode: Int, - description: String?, - failingUrl: String, - isMainFrame: Boolean - ) { - if (isMainFrame) { - if (errorCode == 503) { - // Found the cloudflare challenge page. - challengeFound = true - } else { - // Unlock thread, the challenge wasn't found. - latch.countDown() - } - } - } - } - webView?.loadUrl(origRequestUrl, headers) - } - - // Wait a reasonable amount of time to retrieve the solution. The minimum should be - // around 4 seconds but it can take more due to slow networks or server issues. - latch.await(12, TimeUnit.SECONDS) - - handler.post { - webView?.stopLoading() - webView?.destroy() - } - - val solution = solutionUrl ?: throw Exception("Challenge not found") - - return Request.Builder().get() - .url(solution) - .headers(request.headers) - .addHeader("Referer", origRequestUrl) - .addHeader("Accept", "text/html,application/xhtml+xml,application/xml") - .addHeader("Accept-Language", "en") - .build() - } - -} +package eu.kanade.tachiyomi.network + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import eu.kanade.tachiyomi.util.WebViewClientCompat +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class CloudflareInterceptor(private val context: Context) : Interceptor { + + private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare") + + private val handler = Handler(Looper.getMainLooper()) + + /** + * When this is called, it initializes the WebView if it wasn't already. We use this to avoid + * blocking the main thread too much. If used too often we could consider moving it to the + * Application class. + */ + private val initWebView by lazy { + if (Build.VERSION.SDK_INT >= 17) { + WebSettings.getDefaultUserAgent(context) + } else { + null + } + } + + @Synchronized + override fun intercept(chain: Interceptor.Chain): Response { + initWebView + + val response = chain.proceed(chain.request()) + + // Check if Cloudflare anti-bot is on + if (response.code == 503 && response.header("Server") in serverCheck) { + try { + response.close() + val solutionRequest = resolveWithWebView(chain.request()) + return chain.proceed(solutionRequest) + } catch (e: Exception) { + // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that + // we don't crash the entire app + throw IOException(e) + } + } + + return response + } + + private fun isChallengeSolutionUrl(url: String): Boolean { + return "chk_jschl" in url + } + + @SuppressLint("SetJavaScriptEnabled") + private fun resolveWithWebView(request: Request): Request { + // We need to lock this thread until the WebView finds the challenge solution url, because + // OkHttp doesn't support asynchronous interceptors. + val latch = CountDownLatch(1) + + var webView: WebView? = null + var solutionUrl: String? = null + var challengeFound = false + + val origRequestUrl = request.url.toString() + val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } + + handler.post { + val view = WebView(context) + webView = view + view.settings.javaScriptEnabled = true + view.settings.userAgentString = request.header("User-Agent") + view.webViewClient = object : WebViewClientCompat() { + + override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { + if (isChallengeSolutionUrl(url)) { + solutionUrl = url + latch.countDown() + } + return solutionUrl != null + } + + override fun shouldInterceptRequestCompat( + view: WebView, + url: String + ): WebResourceResponse? { + if (solutionUrl != null) { + // Intercept any request when we have the solution. + return WebResourceResponse("text/plain", "UTF-8", null) + } + return null + } + + override fun onPageFinished(view: WebView, url: String) { + // Http error codes are only received since M + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + url == origRequestUrl && !challengeFound + ) { + // The first request didn't return the challenge, abort. + latch.countDown() + } + } + + override fun onReceivedErrorCompat( + view: WebView, + errorCode: Int, + description: String?, + failingUrl: String, + isMainFrame: Boolean + ) { + if (isMainFrame) { + if (errorCode == 503) { + // Found the cloudflare challenge page. + challengeFound = true + } else { + // Unlock thread, the challenge wasn't found. + latch.countDown() + } + } + } + } + webView?.loadUrl(origRequestUrl, headers) + } + + // Wait a reasonable amount of time to retrieve the solution. The minimum should be + // around 4 seconds but it can take more due to slow networks or server issues. + latch.await(12, TimeUnit.SECONDS) + + handler.post { + webView?.stopLoading() + webView?.destroy() + } + + val solution = solutionUrl ?: throw Exception("Challenge not found") + + return Request.Builder().get() + .url(solution) + .headers(request.headers) + .addHeader("Referer", origRequestUrl) + .addHeader("Accept", "text/html,application/xhtml+xml,application/xml") + .addHeader("Accept-Language", "en") + .build() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 60893a7e3..fa0b70660 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -1,117 +1,117 @@ -package eu.kanade.tachiyomi.network - -import android.content.Context -import android.os.Build -import okhttp3.* -import java.io.File -import java.io.IOException -import java.net.InetAddress -import java.net.Socket -import java.net.UnknownHostException -import java.security.KeyManagementException -import java.security.KeyStore -import java.security.NoSuchAlgorithmException -import javax.net.ssl.* - -class NetworkHelper(context: Context) { - - private val cacheDir = File(context.cacheDir, "network_cache") - - private val cacheSize = 5L * 1024 * 1024 // 5 MiB - - val cookieManager = AndroidCookieJar(context) - - val client = OkHttpClient.Builder() - .cookieJar(cookieManager) - .cache(Cache(cacheDir, cacheSize)) - .enableTLS12() - .build() - - val cloudflareClient = client.newBuilder() - .addInterceptor(CloudflareInterceptor(context)) - .build() - - private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - return this - } - - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - trustManagerFactory.init(null as KeyStore?) - val trustManagers = trustManagerFactory.trustManagers - if (trustManagers.size == 1 && trustManagers[0] is X509TrustManager) { - class TLSSocketFactory @Throws(KeyManagementException::class, NoSuchAlgorithmException::class) - constructor() : SSLSocketFactory() { - - private val internalSSLSocketFactory: SSLSocketFactory - - init { - val context = SSLContext.getInstance("TLS") - context.init(null, null, null) - internalSSLSocketFactory = context.socketFactory - } - - override fun getDefaultCipherSuites(): Array { - return internalSSLSocketFactory.defaultCipherSuites - } - - override fun getSupportedCipherSuites(): Array { - return internalSSLSocketFactory.supportedCipherSuites - } - - @Throws(IOException::class) - override fun createSocket(): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket()) - } - - @Throws(IOException::class) - override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)) - } - - @Throws(IOException::class, UnknownHostException::class) - override fun createSocket(host: String, port: Int): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) - } - - @Throws(IOException::class, UnknownHostException::class) - override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)) - } - - @Throws(IOException::class) - override fun createSocket(host: InetAddress, port: Int): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) - } - - @Throws(IOException::class) - override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)) - } - - private fun enableTLSOnSocket(socket: Socket?): Socket? { - if (socket != null && socket is SSLSocket) { - socket.enabledProtocols = socket.supportedProtocols - } - return socket - } - } - - sslSocketFactory(TLSSocketFactory(), trustManagers[0] as X509TrustManager) - } - - val specCompat = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) - .cipherSuites( - *ConnectionSpec.MODERN_TLS.cipherSuites.orEmpty().toTypedArray(), - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA - ) - .build() - - val specs = listOf(specCompat, ConnectionSpec.CLEARTEXT) - connectionSpecs(specs) - - return this - } -} +package eu.kanade.tachiyomi.network + +import android.content.Context +import android.os.Build +import okhttp3.* +import java.io.File +import java.io.IOException +import java.net.InetAddress +import java.net.Socket +import java.net.UnknownHostException +import java.security.KeyManagementException +import java.security.KeyStore +import java.security.NoSuchAlgorithmException +import javax.net.ssl.* + +class NetworkHelper(context: Context) { + + private val cacheDir = File(context.cacheDir, "network_cache") + + private val cacheSize = 5L * 1024 * 1024 // 5 MiB + + val cookieManager = AndroidCookieJar(context) + + val client = OkHttpClient.Builder() + .cookieJar(cookieManager) + .cache(Cache(cacheDir, cacheSize)) + .enableTLS12() + .build() + + val cloudflareClient = client.newBuilder() + .addInterceptor(CloudflareInterceptor(context)) + .build() + + private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + return this + } + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + trustManagerFactory.init(null as KeyStore?) + val trustManagers = trustManagerFactory.trustManagers + if (trustManagers.size == 1 && trustManagers[0] is X509TrustManager) { + class TLSSocketFactory @Throws(KeyManagementException::class, NoSuchAlgorithmException::class) + constructor() : SSLSocketFactory() { + + private val internalSSLSocketFactory: SSLSocketFactory + + init { + val context = SSLContext.getInstance("TLS") + context.init(null, null, null) + internalSSLSocketFactory = context.socketFactory + } + + override fun getDefaultCipherSuites(): Array { + return internalSSLSocketFactory.defaultCipherSuites + } + + override fun getSupportedCipherSuites(): Array { + return internalSSLSocketFactory.supportedCipherSuites + } + + @Throws(IOException::class) + override fun createSocket(): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket()) + } + + @Throws(IOException::class) + override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)) + } + + @Throws(IOException::class, UnknownHostException::class) + override fun createSocket(host: String, port: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) + } + + @Throws(IOException::class, UnknownHostException::class) + override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)) + } + + @Throws(IOException::class) + override fun createSocket(host: InetAddress, port: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) + } + + @Throws(IOException::class) + override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)) + } + + private fun enableTLSOnSocket(socket: Socket?): Socket? { + if (socket != null && socket is SSLSocket) { + socket.enabledProtocols = socket.supportedProtocols + } + return socket + } + } + + sslSocketFactory(TLSSocketFactory(), trustManagers[0] as X509TrustManager) + } + + val specCompat = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) + .cipherSuites( + *ConnectionSpec.MODERN_TLS.cipherSuites.orEmpty().toTypedArray(), + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + ) + .build() + + val specs = listOf(specCompat, ConnectionSpec.CLEARTEXT) + connectionSpecs(specs) + + return this + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index 905113352..6f0bb5f08 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -1,70 +1,70 @@ -package eu.kanade.tachiyomi.network - -import okhttp3.Call -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import rx.Observable -import rx.Producer -import rx.Subscription -import java.util.concurrent.atomic.AtomicBoolean - -fun Call.asObservable(): Observable { - return Observable.unsafeCreate { subscriber -> - // Since Call is a one-shot type, clone it for each new subscriber. - val call = clone() - - // Wrap the call in a helper which handles both unsubscription and backpressure. - val requestArbiter = object : AtomicBoolean(), Producer, Subscription { - override fun request(n: Long) { - if (n == 0L || !compareAndSet(false, true)) return - - try { - val response = call.execute() - if (!subscriber.isUnsubscribed) { - subscriber.onNext(response) - subscriber.onCompleted() - } - } catch (error: Exception) { - if (!subscriber.isUnsubscribed) { - subscriber.onError(error) - } - } - } - - override fun unsubscribe() { - call.cancel() - } - - override fun isUnsubscribed(): Boolean { - return call.isCanceled() - } - } - - subscriber.add(requestArbiter) - subscriber.setProducer(requestArbiter) - } -} - -fun Call.asObservableSuccess(): Observable { - return asObservable().doOnNext { response -> - if (!response.isSuccessful) { - response.close() - throw Exception("HTTP error ${response.code}") - } - } -} - -fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { - val progressClient = newBuilder() - .cache(null) - .addNetworkInterceptor { chain -> - val originalResponse = chain.proceed(chain.request()) - originalResponse.newBuilder() - .body(ProgressResponseBody(originalResponse.body!!, listener)) - .build() - } - .build() - - return progressClient.newCall(request) -} +package eu.kanade.tachiyomi.network + +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import rx.Producer +import rx.Subscription +import java.util.concurrent.atomic.AtomicBoolean + +fun Call.asObservable(): Observable { + return Observable.unsafeCreate { subscriber -> + // Since Call is a one-shot type, clone it for each new subscriber. + val call = clone() + + // Wrap the call in a helper which handles both unsubscription and backpressure. + val requestArbiter = object : AtomicBoolean(), Producer, Subscription { + override fun request(n: Long) { + if (n == 0L || !compareAndSet(false, true)) return + + try { + val response = call.execute() + if (!subscriber.isUnsubscribed) { + subscriber.onNext(response) + subscriber.onCompleted() + } + } catch (error: Exception) { + if (!subscriber.isUnsubscribed) { + subscriber.onError(error) + } + } + } + + override fun unsubscribe() { + call.cancel() + } + + override fun isUnsubscribed(): Boolean { + return call.isCanceled() + } + } + + subscriber.add(requestArbiter) + subscriber.setProducer(requestArbiter) + } +} + +fun Call.asObservableSuccess(): Observable { + return asObservable().doOnNext { response -> + if (!response.isSuccessful) { + response.close() + throw Exception("HTTP error ${response.code}") + } + } +} + +fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { + val progressClient = newBuilder() + .cache(null) + .addNetworkInterceptor { chain -> + val originalResponse = chain.proceed(chain.request()) + originalResponse.newBuilder() + .body(ProgressResponseBody(originalResponse.body!!, listener)) + .build() + } + .build() + + return progressClient.newCall(request) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt index 113f99763..4bebcf87d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt @@ -1,5 +1,5 @@ -package eu.kanade.tachiyomi.network - -interface ProgressListener { - fun update(bytesRead: Long, contentLength: Long, done: Boolean) +package eu.kanade.tachiyomi.network + +interface ProgressListener { + fun update(bytesRead: Long, contentLength: Long, done: Boolean) } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt index a90a04576..8308acc1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt @@ -1,40 +1,40 @@ -package eu.kanade.tachiyomi.network - -import okhttp3.MediaType -import okhttp3.ResponseBody -import okio.* -import java.io.IOException - -class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { - - private val bufferedSource: BufferedSource by lazy { - source(responseBody.source()).buffer() - } - - override fun contentType(): MediaType { - return responseBody.contentType()!! - } - - override fun contentLength(): Long { - return responseBody.contentLength() - } - - override fun source(): BufferedSource { - return bufferedSource - } - - private fun source(source: Source): Source { - return object : ForwardingSource(source) { - var totalBytesRead = 0L - - @Throws(IOException::class) - override fun read(sink: Buffer, byteCount: Long): Long { - val bytesRead = super.read(sink, byteCount) - // read() returns the number of bytes read, or -1 if this source is exhausted. - totalBytesRead += if (bytesRead != -1L) bytesRead else 0 - progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L) - return bytesRead - } - } - } -} +package eu.kanade.tachiyomi.network + +import okhttp3.MediaType +import okhttp3.ResponseBody +import okio.* +import java.io.IOException + +class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { + + private val bufferedSource: BufferedSource by lazy { + source(responseBody.source()).buffer() + } + + override fun contentType(): MediaType { + return responseBody.contentType()!! + } + + override fun contentLength(): Long { + return responseBody.contentLength() + } + + override fun source(): BufferedSource { + return bufferedSource + } + + private fun source(source: Source): Source { + return object : ForwardingSource(source) { + var totalBytesRead = 0L + + @Throws(IOException::class) + override fun read(sink: Buffer, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += if (bytesRead != -1L) bytesRead else 0 + progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L) + return bytesRead + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt b/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt index 3b89d0d88..9b2697a51 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt @@ -1,32 +1,32 @@ -package eu.kanade.tachiyomi.network - -import okhttp3.* -import java.util.concurrent.TimeUnit.MINUTES - -private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() -private val DEFAULT_HEADERS = Headers.Builder().build() -private val DEFAULT_BODY: RequestBody = FormBody.Builder().build() - -fun GET(url: String, - headers: Headers = DEFAULT_HEADERS, - cache: CacheControl = DEFAULT_CACHE_CONTROL): Request { - - return Request.Builder() - .url(url) - .headers(headers) - .cacheControl(cache) - .build() -} - -fun POST(url: String, - headers: Headers = DEFAULT_HEADERS, - body: RequestBody = DEFAULT_BODY, - cache: CacheControl = DEFAULT_CACHE_CONTROL): Request { - - return Request.Builder() - .url(url) - .post(body) - .headers(headers) - .cacheControl(cache) - .build() -} +package eu.kanade.tachiyomi.network + +import okhttp3.* +import java.util.concurrent.TimeUnit.MINUTES + +private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() +private val DEFAULT_HEADERS = Headers.Builder().build() +private val DEFAULT_BODY: RequestBody = FormBody.Builder().build() + +fun GET(url: String, + headers: Headers = DEFAULT_HEADERS, + cache: CacheControl = DEFAULT_CACHE_CONTROL): Request { + + return Request.Builder() + .url(url) + .headers(headers) + .cacheControl(cache) + .build() +} + +fun POST(url: String, + headers: Headers = DEFAULT_HEADERS, + body: RequestBody = DEFAULT_BODY, + cache: CacheControl = DEFAULT_CACHE_CONTROL): Request { + + return Request.Builder() + .url(url) + .post(body) + .headers(headers) + .cacheControl(cache) + .build() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt index f8d0ea464..f5f11a00b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -1,46 +1,46 @@ -package eu.kanade.tachiyomi.source - -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import rx.Observable - -interface CatalogueSource : Source { - - /** - * An ISO 639-1 compliant language code (two letters in lower case). - */ - val lang: String - - /** - * Whether the source has support for latest updates. - */ - val supportsLatest: Boolean - - /** - * Returns an observable containing a page with a list of manga. - * - * @param page the page number to retrieve. - */ - fun fetchPopularManga(page: Int): Observable - - /** - * Returns an observable containing a page with a list of manga. - * - * @param page the page number to retrieve. - * @param query the search query. - * @param filters the list of filters to apply. - */ - fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable - - /** - * Returns an observable containing a page with a list of latest manga updates. - * - * @param page the page number to retrieve. - */ - fun fetchLatestUpdates(page: Int): Observable - - /** - * Returns the list of filters for the source. - */ - fun getFilterList(): FilterList +package eu.kanade.tachiyomi.source + +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import rx.Observable + +interface CatalogueSource : Source { + + /** + * An ISO 639-1 compliant language code (two letters in lower case). + */ + val lang: String + + /** + * Whether the source has support for latest updates. + */ + val supportsLatest: Boolean + + /** + * Returns an observable containing a page with a list of manga. + * + * @param page the page number to retrieve. + */ + fun fetchPopularManga(page: Int): Observable + + /** + * Returns an observable containing a page with a list of manga. + * + * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. + */ + fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable + + /** + * Returns an observable containing a page with a list of latest manga updates. + * + * @param page the page number to retrieve. + */ + fun fetchLatestUpdates(page: Int): Observable + + /** + * Returns the list of filters for the source. + */ + fun getFilterList(): FilterList } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt index 666621bb4..7a5f43a84 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt @@ -1,44 +1,44 @@ -package eu.kanade.tachiyomi.source - -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import rx.Observable - -/** - * A basic interface for creating a source. It could be an online source, a local source, etc... - */ -interface Source { - - /** - * Id for the source. Must be unique. - */ - val id: Long - - /** - * Name of the source. - */ - val name: String - - /** - * Returns an observable with the updated details for a manga. - * - * @param manga the manga to update. - */ - fun fetchMangaDetails(manga: SManga): Observable - - /** - * Returns an observable with all the available chapters for a manga. - * - * @param manga the manga to update. - */ - fun fetchChapterList(manga: SManga): Observable> - - /** - * Returns an observable with the list of pages a chapter has. - * - * @param chapter the chapter. - */ - fun fetchPageList(chapter: SChapter): Observable> - +package eu.kanade.tachiyomi.source + +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import rx.Observable + +/** + * A basic interface for creating a source. It could be an online source, a local source, etc... + */ +interface Source { + + /** + * Id for the source. Must be unique. + */ + val id: Long + + /** + * Name of the source. + */ + val name: String + + /** + * Returns an observable with the updated details for a manga. + * + * @param manga the manga to update. + */ + fun fetchMangaDetails(manga: SManga): Observable + + /** + * Returns an observable with all the available chapters for a manga. + * + * @param manga the manga to update. + */ + fun fetchChapterList(manga: SManga): Observable> + + /** + * Returns an observable with the list of pages a chapter has. + * + * @param chapter the chapter. + */ + fun fetchPageList(chapter: SChapter): Observable> + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index a8d0fed16..969f789ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -1,74 +1,74 @@ -package eu.kanade.tachiyomi.source - -import android.content.Context -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import rx.Observable - -open class SourceManager(private val context: Context) { - - private val sourcesMap = mutableMapOf() - - private val stubSourcesMap = mutableMapOf() - - init { - createInternalSources().forEach { registerSource(it) } - } - - open fun get(sourceKey: Long): Source? { - return sourcesMap[sourceKey] - } - - fun getOrStub(sourceKey: Long): Source { - return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { - StubSource(sourceKey) - } - } - - fun getOnlineSources() = sourcesMap.values.filterIsInstance() - - fun getCatalogueSources() = sourcesMap.values.filterIsInstance() - - internal fun registerSource(source: Source, overwrite: Boolean = false) { - if (overwrite || !sourcesMap.containsKey(source.id)) { - sourcesMap[source.id] = source - } - } - - internal fun unregisterSource(source: Source) { - sourcesMap.remove(source.id) - } - - private fun createInternalSources(): List = listOf( - LocalSource(context) - ) - - private inner class StubSource(override val id: Long) : Source { - - override val name: String - get() = id.toString() - - override fun fetchMangaDetails(manga: SManga): Observable { - return Observable.error(getSourceNotInstalledException()) - } - - override fun fetchChapterList(manga: SManga): Observable> { - return Observable.error(getSourceNotInstalledException()) - } - - override fun fetchPageList(chapter: SChapter): Observable> { - return Observable.error(getSourceNotInstalledException()) - } - - override fun toString(): String { - return name - } - - private fun getSourceNotInstalledException(): Exception { - return Exception(context.getString(R.string.source_not_installed, id.toString())) - } - } -} +package eu.kanade.tachiyomi.source + +import android.content.Context +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import rx.Observable + +open class SourceManager(private val context: Context) { + + private val sourcesMap = mutableMapOf() + + private val stubSourcesMap = mutableMapOf() + + init { + createInternalSources().forEach { registerSource(it) } + } + + open fun get(sourceKey: Long): Source? { + return sourcesMap[sourceKey] + } + + fun getOrStub(sourceKey: Long): Source { + return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { + StubSource(sourceKey) + } + } + + fun getOnlineSources() = sourcesMap.values.filterIsInstance() + + fun getCatalogueSources() = sourcesMap.values.filterIsInstance() + + internal fun registerSource(source: Source, overwrite: Boolean = false) { + if (overwrite || !sourcesMap.containsKey(source.id)) { + sourcesMap[source.id] = source + } + } + + internal fun unregisterSource(source: Source) { + sourcesMap.remove(source.id) + } + + private fun createInternalSources(): List = listOf( + LocalSource(context) + ) + + private inner class StubSource(override val id: Long) : Source { + + override val name: String + get() = id.toString() + + override fun fetchMangaDetails(manga: SManga): Observable { + return Observable.error(getSourceNotInstalledException()) + } + + override fun fetchChapterList(manga: SManga): Observable> { + return Observable.error(getSourceNotInstalledException()) + } + + override fun fetchPageList(chapter: SChapter): Observable> { + return Observable.error(getSourceNotInstalledException()) + } + + override fun toString(): String { + return name + } + + private fun getSourceNotInstalledException(): Exception { + return Exception(context.getString(R.string.source_not_installed, id.toString())) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt index 1664d67eb..8cd520d22 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt @@ -1,40 +1,40 @@ -package eu.kanade.tachiyomi.source.model - -sealed class Filter(val name: String, var state: T) { - open class Header(name: String) : Filter(name, 0) - open class Separator(name: String = "") : Filter(name, 0) - abstract class Select(name: String, val values: Array, state: Int = 0) : Filter(name, state) - abstract class Text(name: String, state: String = "") : Filter(name, state) - abstract class CheckBox(name: String, state: Boolean = false) : Filter(name, state) - abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter(name, state) { - fun isIgnored() = state == STATE_IGNORE - fun isIncluded() = state == STATE_INCLUDE - fun isExcluded() = state == STATE_EXCLUDE - - companion object { - const val STATE_IGNORE = 0 - const val STATE_INCLUDE = 1 - const val STATE_EXCLUDE = 2 - } - } - abstract class Group(name: String, state: List): Filter>(name, state) - - abstract class Sort(name: String, val values: Array, state: Selection? = null) - : Filter(name, state) { - data class Selection(val index: Int, val ascending: Boolean) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Filter<*>) return false - - return name == other.name && state == other.state - } - - override fun hashCode(): Int { - var result = name.hashCode() - result = 31 * result + (state?.hashCode() ?: 0) - return result - } - +package eu.kanade.tachiyomi.source.model + +sealed class Filter(val name: String, var state: T) { + open class Header(name: String) : Filter(name, 0) + open class Separator(name: String = "") : Filter(name, 0) + abstract class Select(name: String, val values: Array, state: Int = 0) : Filter(name, state) + abstract class Text(name: String, state: String = "") : Filter(name, state) + abstract class CheckBox(name: String, state: Boolean = false) : Filter(name, state) + abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter(name, state) { + fun isIgnored() = state == STATE_IGNORE + fun isIncluded() = state == STATE_INCLUDE + fun isExcluded() = state == STATE_EXCLUDE + + companion object { + const val STATE_IGNORE = 0 + const val STATE_INCLUDE = 1 + const val STATE_EXCLUDE = 2 + } + } + abstract class Group(name: String, state: List): Filter>(name, state) + + abstract class Sort(name: String, val values: Array, state: Selection? = null) + : Filter(name, state) { + data class Selection(val index: Int, val ascending: Boolean) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Filter<*>) return false + + return name == other.name && state == other.state + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + (state?.hashCode() ?: 0) + return result + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt index 36d8e144a..e24db65b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt @@ -1,7 +1,7 @@ -package eu.kanade.tachiyomi.source.model - -data class FilterList(val list: List>) : List> by list { - - constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) - +package eu.kanade.tachiyomi.source.model + +data class FilterList(val list: List>) : List> by list { + + constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt index e359619fb..12dd172a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt @@ -1,3 +1,3 @@ -package eu.kanade.tachiyomi.source.model - +package eu.kanade.tachiyomi.source.model + data class MangasPage(val mangas: List, val hasNextPage: Boolean) \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt index 00cb40e55..c06a59a88 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt @@ -1,48 +1,48 @@ -package eu.kanade.tachiyomi.source.model - -import android.net.Uri -import eu.kanade.tachiyomi.network.ProgressListener -import rx.subjects.Subject - -open class Page( - val index: Int, - val url: String = "", - var imageUrl: String? = null, - @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions -) : ProgressListener { - - val number: Int - get() = index + 1 - - @Transient @Volatile var status: Int = 0 - set(value) { - field = value - statusSubject?.onNext(value) - } - - @Transient @Volatile var progress: Int = 0 - - @Transient private var statusSubject: Subject? = null - - override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { - progress = if (contentLength > 0) { - (100 * bytesRead / contentLength).toInt() - } else { - -1 - } - } - - fun setStatusSubject(subject: Subject?) { - this.statusSubject = subject - } - - companion object { - - const val QUEUE = 0 - const val LOAD_PAGE = 1 - const val DOWNLOAD_IMAGE = 2 - const val READY = 3 - const val ERROR = 4 - } - -} +package eu.kanade.tachiyomi.source.model + +import android.net.Uri +import eu.kanade.tachiyomi.network.ProgressListener +import rx.subjects.Subject + +open class Page( + val index: Int, + val url: String = "", + var imageUrl: String? = null, + @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions +) : ProgressListener { + + val number: Int + get() = index + 1 + + @Transient @Volatile var status: Int = 0 + set(value) { + field = value + statusSubject?.onNext(value) + } + + @Transient @Volatile var progress: Int = 0 + + @Transient private var statusSubject: Subject? = null + + override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { + progress = if (contentLength > 0) { + (100 * bytesRead / contentLength).toInt() + } else { + -1 + } + } + + fun setStatusSubject(subject: Subject?) { + this.statusSubject = subject + } + + companion object { + + const val QUEUE = 0 + const val LOAD_PAGE = 1 + const val DOWNLOAD_IMAGE = 2 + const val READY = 3 + const val ERROR = 4 + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt index 991d24d41..0017e51d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt @@ -1,31 +1,31 @@ -package eu.kanade.tachiyomi.source.model - -import java.io.Serializable - -interface SChapter : Serializable { - - var url: String - - var name: String - - var date_upload: Long - - var chapter_number: Float - - var scanlator: String? - - fun copyFrom(other: SChapter) { - name = other.name - url = other.url - date_upload = other.date_upload - chapter_number = other.chapter_number - scanlator = other.scanlator - } - - companion object { - fun create(): SChapter { - return SChapterImpl() - } - } - +package eu.kanade.tachiyomi.source.model + +import java.io.Serializable + +interface SChapter : Serializable { + + var url: String + + var name: String + + var date_upload: Long + + var chapter_number: Float + + var scanlator: String? + + fun copyFrom(other: SChapter) { + name = other.name + url = other.url + date_upload = other.date_upload + chapter_number = other.chapter_number + scanlator = other.scanlator + } + + companion object { + fun create(): SChapter { + return SChapterImpl() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt index cfc4c3999..4fa55141f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt @@ -1,15 +1,15 @@ -package eu.kanade.tachiyomi.source.model - -class SChapterImpl : SChapter { - - override lateinit var url: String - - override lateinit var name: String - - override var date_upload: Long = 0 - - override var chapter_number: Float = -1f - - override var scanlator: String? = null - +package eu.kanade.tachiyomi.source.model + +class SChapterImpl : SChapter { + + override lateinit var url: String + + override lateinit var name: String + + override var date_upload: Long = 0 + + override var chapter_number: Float = -1f + + override var scanlator: String? = null + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt index 8a1ba1af0..3e3ef8206 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt @@ -1,58 +1,58 @@ -package eu.kanade.tachiyomi.source.model - -import java.io.Serializable - -interface SManga : Serializable { - - var url: String - - var title: String - - var artist: String? - - var author: String? - - var description: String? - - var genre: String? - - var status: Int - - var thumbnail_url: String? - - var initialized: Boolean - - fun copyFrom(other: SManga) { - if (other.author != null) - author = other.author - - if (other.artist != null) - artist = other.artist - - if (other.description != null) - description = other.description - - if (other.genre != null) - genre = other.genre - - if (other.thumbnail_url != null) - thumbnail_url = other.thumbnail_url - - status = other.status - - if (!initialized) - initialized = other.initialized - } - - companion object { - const val UNKNOWN = 0 - const val ONGOING = 1 - const val COMPLETED = 2 - const val LICENSED = 3 - - fun create(): SManga { - return SMangaImpl() - } - } - +package eu.kanade.tachiyomi.source.model + +import java.io.Serializable + +interface SManga : Serializable { + + var url: String + + var title: String + + var artist: String? + + var author: String? + + var description: String? + + var genre: String? + + var status: Int + + var thumbnail_url: String? + + var initialized: Boolean + + fun copyFrom(other: SManga) { + if (other.author != null) + author = other.author + + if (other.artist != null) + artist = other.artist + + if (other.description != null) + description = other.description + + if (other.genre != null) + genre = other.genre + + if (other.thumbnail_url != null) + thumbnail_url = other.thumbnail_url + + status = other.status + + if (!initialized) + initialized = other.initialized + } + + companion object { + const val UNKNOWN = 0 + const val ONGOING = 1 + const val COMPLETED = 2 + const val LICENSED = 3 + + fun create(): SManga { + return SMangaImpl() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt index 30635897b..3dbba4b99 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt @@ -1,23 +1,23 @@ -package eu.kanade.tachiyomi.source.model - -class SMangaImpl : SManga { - - override lateinit var url: String - - override lateinit var title: String - - override var artist: String? = null - - override var author: String? = null - - override var description: String? = null - - override var genre: String? = null - - override var status: Int = 0 - - override var thumbnail_url: String? = null - - override var initialized: Boolean = false - +package eu.kanade.tachiyomi.source.model + +class SMangaImpl : SManga { + + override lateinit var url: String + + override lateinit var title: String + + override var artist: String? = null + + override var author: String? = null + + override var description: String? = null + + override var genre: String? = null + + override var status: Int = 0 + + override var thumbnail_url: String? = null + + override var initialized: Boolean = false + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt index cb76f1162..86b020be3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -1,367 +1,367 @@ -package eu.kanade.tachiyomi.source.online - -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.network.newCallWithProgress -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.model.* -import okhttp3.Headers -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import rx.Observable -import uy.kohesive.injekt.injectLazy -import java.lang.Exception -import java.net.URI -import java.net.URISyntaxException -import java.security.MessageDigest - -/** - * A simple implementation for sources from a website. - */ -abstract class HttpSource : CatalogueSource { - - /** - * Network service. - */ - protected val network: NetworkHelper by injectLazy() - -// /** -// * Preferences that a source may need. -// */ -// val preferences: SharedPreferences by lazy { -// Injekt.get().getSharedPreferences("source_$id", Context.MODE_PRIVATE) -// } - - /** - * Base url of the website without the trailing slash, like: http://mysite.com - */ - abstract val baseUrl: String - - /** - * Version id used to generate the source id. If the site completely changes and urls are - * incompatible, you may increase this value and it'll be considered as a new source. - */ - open val versionId = 1 - - /** - * Id of the source. By default it uses a generated id using the first 16 characters (64 bits) - * of the MD5 of the string: sourcename/language/versionId - * Note the generated id sets the sign bit to 0. - */ - override val id by lazy { - val key = "${name.toLowerCase()}/$lang/$versionId" - val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) - (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE - } - - /** - * Headers used for requests. - */ - val headers: Headers by lazy { headersBuilder().build() } - - /** - * Default network client for doing requests. - */ - open val client: OkHttpClient - get() = network.client - - /** - * Headers builder for requests. Implementations can override this method for custom headers. - */ - open protected fun headersBuilder() = Headers.Builder().apply { - add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)") - } - - /** - * Visible name of the source. - */ - override fun toString() = "$name (${lang.toUpperCase()})" - - /** - * Returns an observable containing a page with a list of manga. Normally it's not needed to - * override this method. - * - * @param page the page number to retrieve. - */ - override fun fetchPopularManga(page: Int): Observable { - return client.newCall(popularMangaRequest(page)) - .asObservableSuccess() - .map { response -> - popularMangaParse(response) - } - } - - /** - * Returns the request for the popular manga given the page. - * - * @param page the page number to retrieve. - */ - abstract protected fun popularMangaRequest(page: Int): Request - - /** - * Parses the response from the site and returns a [MangasPage] object. - * - * @param response the response from the site. - */ - abstract protected fun popularMangaParse(response: Response): MangasPage - - /** - * Returns an observable containing a page with a list of manga. Normally it's not needed to - * override this method. - * - * @param page the page number to retrieve. - * @param query the search query. - * @param filters the list of filters to apply. - */ - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return client.newCall(searchMangaRequest(page, query, filters)) - .asObservableSuccess() - .map { response -> - searchMangaParse(response) - } - } - - /** - * Returns the request for the search manga given the page. - * - * @param page the page number to retrieve. - * @param query the search query. - * @param filters the list of filters to apply. - */ - abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request - - /** - * Parses the response from the site and returns a [MangasPage] object. - * - * @param response the response from the site. - */ - abstract protected fun searchMangaParse(response: Response): MangasPage - - /** - * Returns an observable containing a page with a list of latest manga updates. - * - * @param page the page number to retrieve. - */ - override fun fetchLatestUpdates(page: Int): Observable { - return client.newCall(latestUpdatesRequest(page)) - .asObservableSuccess() - .map { response -> - latestUpdatesParse(response) - } - } - - /** - * Returns the request for latest manga given the page. - * - * @param page the page number to retrieve. - */ - abstract protected fun latestUpdatesRequest(page: Int): Request - - /** - * Parses the response from the site and returns a [MangasPage] object. - * - * @param response the response from the site. - */ - abstract protected fun latestUpdatesParse(response: Response): MangasPage - - /** - * Returns an observable with the updated details for a manga. Normally it's not needed to - * override this method. - * - * @param manga the manga to be updated. - */ - override fun fetchMangaDetails(manga: SManga): Observable { - return client.newCall(mangaDetailsRequest(manga)) - .asObservableSuccess() - .map { response -> - mangaDetailsParse(response).apply { initialized = true } - } - } - - /** - * Returns the request for the details of a manga. Override only if it's needed to change the - * url, send different headers or request method like POST. - * - * @param manga the manga to be updated. - */ - open fun mangaDetailsRequest(manga: SManga): Request { - return GET(baseUrl + manga.url, headers) - } - - /** - * Parses the response from the site and returns the details of a manga. - * - * @param response the response from the site. - */ - abstract protected fun mangaDetailsParse(response: Response): SManga - - /** - * Returns an observable with the updated chapter list for a manga. Normally it's not needed to - * override this method. If a manga is licensed an empty chapter list observable is returned - * - * @param manga the manga to look for chapters. - */ - override fun fetchChapterList(manga: SManga): Observable> { - if (manga.status != SManga.LICENSED) { - return client.newCall(chapterListRequest(manga)) - .asObservableSuccess() - .map { response -> - chapterListParse(response) - } - } else { - return Observable.error(Exception("Licensed - No chapters to show")) - } - } - - /** - * Returns the request for updating the chapter list. Override only if it's needed to override - * the url, send different headers or request method like POST. - * - * @param manga the manga to look for chapters. - */ - open protected fun chapterListRequest(manga: SManga): Request { - return GET(baseUrl + manga.url, headers) - } - - /** - * Parses the response from the site and returns a list of chapters. - * - * @param response the response from the site. - */ - abstract protected fun chapterListParse(response: Response): List - - /** - * Returns an observable with the page list for a chapter. - * - * @param chapter the chapter whose page list has to be fetched. - */ - override fun fetchPageList(chapter: SChapter): Observable> { - return client.newCall(pageListRequest(chapter)) - .asObservableSuccess() - .map { response -> - pageListParse(response) - } - } - - /** - * Returns the request for getting the page list. Override only if it's needed to override the - * url, send different headers or request method like POST. - * - * @param chapter the chapter whose page list has to be fetched. - */ - open protected fun pageListRequest(chapter: SChapter): Request { - return GET(baseUrl + chapter.url, headers) - } - - /** - * Parses the response from the site and returns a list of pages. - * - * @param response the response from the site. - */ - abstract protected fun pageListParse(response: Response): List - - /** - * Returns an observable with the page containing the source url of the image. If there's any - * error, it will return null instead of throwing an exception. - * - * @param page the page whose source image has to be fetched. - */ - open fun fetchImageUrl(page: Page): Observable { - return client.newCall(imageUrlRequest(page)) - .asObservableSuccess() - .map { imageUrlParse(it) } - } - - /** - * Returns the request for getting the url to the source image. Override only if it's needed to - * override the url, send different headers or request method like POST. - * - * @param page the chapter whose page list has to be fetched - */ - open protected fun imageUrlRequest(page: Page): Request { - return GET(page.url, headers) - } - - /** - * Parses the response from the site and returns the absolute url to the source image. - * - * @param response the response from the site. - */ - abstract protected fun imageUrlParse(response: Response): String - - /** - * Returns an observable with the response of the source image. - * - * @param page the page whose source image has to be downloaded. - */ - fun fetchImage(page: Page): Observable { - return client.newCallWithProgress(imageRequest(page), page) - .asObservableSuccess() - } - - /** - * Returns the request for getting the source image. Override only if it's needed to override - * the url, send different headers or request method like POST. - * - * @param page the chapter whose page list has to be fetched - */ - open protected fun imageRequest(page: Page): Request { - return GET(page.imageUrl!!, headers) - } - - /** - * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from - * database and the urls could still work after a domain change. - * - * @param url the full url to the chapter. - */ - fun SChapter.setUrlWithoutDomain(url: String) { - this.url = getUrlWithoutDomain(url) - } - - /** - * Assigns the url of the manga without the scheme and domain. It saves some redundancy from - * database and the urls could still work after a domain change. - * - * @param url the full url to the manga. - */ - fun SManga.setUrlWithoutDomain(url: String) { - this.url = getUrlWithoutDomain(url) - } - - /** - * Returns the url of the given string without the scheme and domain. - * - * @param orig the full url. - */ - private fun getUrlWithoutDomain(orig: String): String { - try { - val uri = URI(orig) - var out = uri.path - if (uri.query != null) - out += "?" + uri.query - if (uri.fragment != null) - out += "#" + uri.fragment - return out - } catch (e: URISyntaxException) { - return orig - } - } - - /** - * Called before inserting a new chapter into database. Use it if you need to override chapter - * fields, like the title or the chapter number. Do not change anything to [manga]. - * - * @param chapter the chapter to be added. - * @param manga the manga of the chapter. - */ - open fun prepareNewChapter(chapter: SChapter, manga: SManga) { - } - - /** - * Returns the list of filters for the source. - */ - override fun getFilterList() = FilterList() -} +package eu.kanade.tachiyomi.source.online + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.newCallWithProgress +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.model.* +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.injectLazy +import java.lang.Exception +import java.net.URI +import java.net.URISyntaxException +import java.security.MessageDigest + +/** + * A simple implementation for sources from a website. + */ +abstract class HttpSource : CatalogueSource { + + /** + * Network service. + */ + protected val network: NetworkHelper by injectLazy() + +// /** +// * Preferences that a source may need. +// */ +// val preferences: SharedPreferences by lazy { +// Injekt.get().getSharedPreferences("source_$id", Context.MODE_PRIVATE) +// } + + /** + * Base url of the website without the trailing slash, like: http://mysite.com + */ + abstract val baseUrl: String + + /** + * Version id used to generate the source id. If the site completely changes and urls are + * incompatible, you may increase this value and it'll be considered as a new source. + */ + open val versionId = 1 + + /** + * Id of the source. By default it uses a generated id using the first 16 characters (64 bits) + * of the MD5 of the string: sourcename/language/versionId + * Note the generated id sets the sign bit to 0. + */ + override val id by lazy { + val key = "${name.toLowerCase()}/$lang/$versionId" + val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) + (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE + } + + /** + * Headers used for requests. + */ + val headers: Headers by lazy { headersBuilder().build() } + + /** + * Default network client for doing requests. + */ + open val client: OkHttpClient + get() = network.client + + /** + * Headers builder for requests. Implementations can override this method for custom headers. + */ + open protected fun headersBuilder() = Headers.Builder().apply { + add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)") + } + + /** + * Visible name of the source. + */ + override fun toString() = "$name (${lang.toUpperCase()})" + + /** + * Returns an observable containing a page with a list of manga. Normally it's not needed to + * override this method. + * + * @param page the page number to retrieve. + */ + override fun fetchPopularManga(page: Int): Observable { + return client.newCall(popularMangaRequest(page)) + .asObservableSuccess() + .map { response -> + popularMangaParse(response) + } + } + + /** + * Returns the request for the popular manga given the page. + * + * @param page the page number to retrieve. + */ + abstract protected fun popularMangaRequest(page: Int): Request + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + abstract protected fun popularMangaParse(response: Response): MangasPage + + /** + * Returns an observable containing a page with a list of manga. Normally it's not needed to + * override this method. + * + * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. + */ + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() + .map { response -> + searchMangaParse(response) + } + } + + /** + * Returns the request for the search manga given the page. + * + * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. + */ + abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + abstract protected fun searchMangaParse(response: Response): MangasPage + + /** + * Returns an observable containing a page with a list of latest manga updates. + * + * @param page the page number to retrieve. + */ + override fun fetchLatestUpdates(page: Int): Observable { + return client.newCall(latestUpdatesRequest(page)) + .asObservableSuccess() + .map { response -> + latestUpdatesParse(response) + } + } + + /** + * Returns the request for latest manga given the page. + * + * @param page the page number to retrieve. + */ + abstract protected fun latestUpdatesRequest(page: Int): Request + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + abstract protected fun latestUpdatesParse(response: Response): MangasPage + + /** + * Returns an observable with the updated details for a manga. Normally it's not needed to + * override this method. + * + * @param manga the manga to be updated. + */ + override fun fetchMangaDetails(manga: SManga): Observable { + return client.newCall(mangaDetailsRequest(manga)) + .asObservableSuccess() + .map { response -> + mangaDetailsParse(response).apply { initialized = true } + } + } + + /** + * Returns the request for the details of a manga. Override only if it's needed to change the + * url, send different headers or request method like POST. + * + * @param manga the manga to be updated. + */ + open fun mangaDetailsRequest(manga: SManga): Request { + return GET(baseUrl + manga.url, headers) + } + + /** + * Parses the response from the site and returns the details of a manga. + * + * @param response the response from the site. + */ + abstract protected fun mangaDetailsParse(response: Response): SManga + + /** + * Returns an observable with the updated chapter list for a manga. Normally it's not needed to + * override this method. If a manga is licensed an empty chapter list observable is returned + * + * @param manga the manga to look for chapters. + */ + override fun fetchChapterList(manga: SManga): Observable> { + if (manga.status != SManga.LICENSED) { + return client.newCall(chapterListRequest(manga)) + .asObservableSuccess() + .map { response -> + chapterListParse(response) + } + } else { + return Observable.error(Exception("Licensed - No chapters to show")) + } + } + + /** + * Returns the request for updating the chapter list. Override only if it's needed to override + * the url, send different headers or request method like POST. + * + * @param manga the manga to look for chapters. + */ + open protected fun chapterListRequest(manga: SManga): Request { + return GET(baseUrl + manga.url, headers) + } + + /** + * Parses the response from the site and returns a list of chapters. + * + * @param response the response from the site. + */ + abstract protected fun chapterListParse(response: Response): List + + /** + * Returns an observable with the page list for a chapter. + * + * @param chapter the chapter whose page list has to be fetched. + */ + override fun fetchPageList(chapter: SChapter): Observable> { + return client.newCall(pageListRequest(chapter)) + .asObservableSuccess() + .map { response -> + pageListParse(response) + } + } + + /** + * Returns the request for getting the page list. Override only if it's needed to override the + * url, send different headers or request method like POST. + * + * @param chapter the chapter whose page list has to be fetched. + */ + open protected fun pageListRequest(chapter: SChapter): Request { + return GET(baseUrl + chapter.url, headers) + } + + /** + * Parses the response from the site and returns a list of pages. + * + * @param response the response from the site. + */ + abstract protected fun pageListParse(response: Response): List + + /** + * Returns an observable with the page containing the source url of the image. If there's any + * error, it will return null instead of throwing an exception. + * + * @param page the page whose source image has to be fetched. + */ + open fun fetchImageUrl(page: Page): Observable { + return client.newCall(imageUrlRequest(page)) + .asObservableSuccess() + .map { imageUrlParse(it) } + } + + /** + * Returns the request for getting the url to the source image. Override only if it's needed to + * override the url, send different headers or request method like POST. + * + * @param page the chapter whose page list has to be fetched + */ + open protected fun imageUrlRequest(page: Page): Request { + return GET(page.url, headers) + } + + /** + * Parses the response from the site and returns the absolute url to the source image. + * + * @param response the response from the site. + */ + abstract protected fun imageUrlParse(response: Response): String + + /** + * Returns an observable with the response of the source image. + * + * @param page the page whose source image has to be downloaded. + */ + fun fetchImage(page: Page): Observable { + return client.newCallWithProgress(imageRequest(page), page) + .asObservableSuccess() + } + + /** + * Returns the request for getting the source image. Override only if it's needed to override + * the url, send different headers or request method like POST. + * + * @param page the chapter whose page list has to be fetched + */ + open protected fun imageRequest(page: Page): Request { + return GET(page.imageUrl!!, headers) + } + + /** + * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from + * database and the urls could still work after a domain change. + * + * @param url the full url to the chapter. + */ + fun SChapter.setUrlWithoutDomain(url: String) { + this.url = getUrlWithoutDomain(url) + } + + /** + * Assigns the url of the manga without the scheme and domain. It saves some redundancy from + * database and the urls could still work after a domain change. + * + * @param url the full url to the manga. + */ + fun SManga.setUrlWithoutDomain(url: String) { + this.url = getUrlWithoutDomain(url) + } + + /** + * Returns the url of the given string without the scheme and domain. + * + * @param orig the full url. + */ + private fun getUrlWithoutDomain(orig: String): String { + try { + val uri = URI(orig) + var out = uri.path + if (uri.query != null) + out += "?" + uri.query + if (uri.fragment != null) + out += "#" + uri.fragment + return out + } catch (e: URISyntaxException) { + return orig + } + } + + /** + * Called before inserting a new chapter into database. Use it if you need to override chapter + * fields, like the title or the chapter number. Do not change anything to [manga]. + * + * @param chapter the chapter to be added. + * @param manga the manga of the chapter. + */ + open fun prepareNewChapter(chapter: SChapter, manga: SManga) { + } + + /** + * Returns the list of filters for the source. + */ + override fun getFilterList() = FilterList() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt index e69581df3..2c8f2d0b8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt @@ -1,25 +1,25 @@ -package eu.kanade.tachiyomi.source.online - -import eu.kanade.tachiyomi.source.model.Page -import rx.Observable - -fun HttpSource.getImageUrl(page: Page): Observable { - page.status = Page.LOAD_PAGE - return fetchImageUrl(page) - .doOnError { page.status = Page.ERROR } - .onErrorReturn { null } - .doOnNext { page.imageUrl = it } - .map { page } -} - -fun HttpSource.fetchAllImageUrlsFromPageList(pages: List): Observable { - return Observable.from(pages) - .filter { !it.imageUrl.isNullOrEmpty() } - .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) -} - -fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List): Observable { - return Observable.from(pages) - .filter { it.imageUrl.isNullOrEmpty() } - .concatMap { getImageUrl(it) } -} +package eu.kanade.tachiyomi.source.online + +import eu.kanade.tachiyomi.source.model.Page +import rx.Observable + +fun HttpSource.getImageUrl(page: Page): Observable { + page.status = Page.LOAD_PAGE + return fetchImageUrl(page) + .doOnError { page.status = Page.ERROR } + .onErrorReturn { null } + .doOnNext { page.imageUrl = it } + .map { page } +} + +fun HttpSource.fetchAllImageUrlsFromPageList(pages: List): Observable { + return Observable.from(pages) + .filter { !it.imageUrl.isNullOrEmpty() } + .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) +} + +fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List): Observable { + return Observable.from(pages) + .filter { it.imageUrl.isNullOrEmpty() } + .concatMap { getImageUrl(it) } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt index 61ec4fd35..8aae073e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt @@ -1,15 +1,15 @@ -package eu.kanade.tachiyomi.source.online - -import eu.kanade.tachiyomi.source.Source -import okhttp3.Response -import rx.Observable - -interface LoginSource : Source { - - fun isLogged(): Boolean - - fun login(username: String, password: String): Observable - - fun isAuthenticationSuccessful(response: Response): Boolean - +package eu.kanade.tachiyomi.source.online + +import eu.kanade.tachiyomi.source.Source +import okhttp3.Response +import rx.Observable + +interface LoginSource : Source { + + fun isLogged(): Boolean + + fun login(username: String, password: String): Observable + + fun isAuthenticationSuccessful(response: Response): Boolean + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt index 6053fc2b6..03d58d56a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt @@ -1,200 +1,200 @@ -package eu.kanade.tachiyomi.source.online - -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.util.asJsoup -import okhttp3.Response -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element - -/** - * A simple implementation for sources from a website using Jsoup, an HTML parser. - */ -abstract class ParsedHttpSource : HttpSource() { - - /** - * Parses the response from the site and returns a [MangasPage] object. - * - * @param response the response from the site. - */ - override fun popularMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - - val mangas = document.select(popularMangaSelector()).map { element -> - popularMangaFromElement(element) - } - - val hasNextPage = popularMangaNextPageSelector()?.let { selector -> - document.select(selector).first() - } != null - - return MangasPage(mangas, hasNextPage) - } - - /** - * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. - */ - abstract protected fun popularMangaSelector(): String - - /** - * Returns a manga from the given [element]. Most sites only show the title and the url, it's - * totally fine to fill only those two values. - * - * @param element an element obtained from [popularMangaSelector]. - */ - abstract protected fun popularMangaFromElement(element: Element): SManga - - /** - * Returns the Jsoup selector that returns the tag linking to the next page, or null if - * there's no next page. - */ - abstract protected fun popularMangaNextPageSelector(): String? - - /** - * Parses the response from the site and returns a [MangasPage] object. - * - * @param response the response from the site. - */ - override fun searchMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - - val mangas = document.select(searchMangaSelector()).map { element -> - searchMangaFromElement(element) - } - - val hasNextPage = searchMangaNextPageSelector()?.let { selector -> - document.select(selector).first() - } != null - - return MangasPage(mangas, hasNextPage) - } - - /** - * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. - */ - abstract protected fun searchMangaSelector(): String - - /** - * Returns a manga from the given [element]. Most sites only show the title and the url, it's - * totally fine to fill only those two values. - * - * @param element an element obtained from [searchMangaSelector]. - */ - abstract protected fun searchMangaFromElement(element: Element): SManga - - /** - * Returns the Jsoup selector that returns the tag linking to the next page, or null if - * there's no next page. - */ - abstract protected fun searchMangaNextPageSelector(): String? - - /** - * Parses the response from the site and returns a [MangasPage] object. - * - * @param response the response from the site. - */ - override fun latestUpdatesParse(response: Response): MangasPage { - val document = response.asJsoup() - - val mangas = document.select(latestUpdatesSelector()).map { element -> - latestUpdatesFromElement(element) - } - - val hasNextPage = latestUpdatesNextPageSelector()?.let { selector -> - document.select(selector).first() - } != null - - return MangasPage(mangas, hasNextPage) - } - - /** - * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. - */ - abstract protected fun latestUpdatesSelector(): String - - /** - * Returns a manga from the given [element]. Most sites only show the title and the url, it's - * totally fine to fill only those two values. - * - * @param element an element obtained from [latestUpdatesSelector]. - */ - abstract protected fun latestUpdatesFromElement(element: Element): SManga - - /** - * Returns the Jsoup selector that returns the tag linking to the next page, or null if - * there's no next page. - */ - abstract protected fun latestUpdatesNextPageSelector(): String? - - /** - * Parses the response from the site and returns the details of a manga. - * - * @param response the response from the site. - */ - override fun mangaDetailsParse(response: Response): SManga { - return mangaDetailsParse(response.asJsoup()) - } - - /** - * Returns the details of the manga from the given [document]. - * - * @param document the parsed document. - */ - abstract protected fun mangaDetailsParse(document: Document): SManga - - /** - * Parses the response from the site and returns a list of chapters. - * - * @param response the response from the site. - */ - override fun chapterListParse(response: Response): List { - val document = response.asJsoup() - return document.select(chapterListSelector()).map { chapterFromElement(it) } - } - - /** - * Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter. - */ - abstract protected fun chapterListSelector(): String - - /** - * Returns a chapter from the given element. - * - * @param element an element obtained from [chapterListSelector]. - */ - abstract protected fun chapterFromElement(element: Element): SChapter - - /** - * Parses the response from the site and returns the page list. - * - * @param response the response from the site. - */ - override fun pageListParse(response: Response): List { - return pageListParse(response.asJsoup()) - } - - /** - * Returns a page list from the given document. - * - * @param document the parsed document. - */ - abstract protected fun pageListParse(document: Document): List - - /** - * Parse the response from the site and returns the absolute url to the source image. - * - * @param response the response from the site. - */ - override fun imageUrlParse(response: Response): String { - return imageUrlParse(response.asJsoup()) - } - - /** - * Returns the absolute url to the source image from the document. - * - * @param document the parsed document. - */ - abstract protected fun imageUrlParse(document: Document): String -} +package eu.kanade.tachiyomi.source.online + +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +/** + * A simple implementation for sources from a website using Jsoup, an HTML parser. + */ +abstract class ParsedHttpSource : HttpSource() { + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + + val mangas = document.select(popularMangaSelector()).map { element -> + popularMangaFromElement(element) + } + + val hasNextPage = popularMangaNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return MangasPage(mangas, hasNextPage) + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. + */ + abstract protected fun popularMangaSelector(): String + + /** + * Returns a manga from the given [element]. Most sites only show the title and the url, it's + * totally fine to fill only those two values. + * + * @param element an element obtained from [popularMangaSelector]. + */ + abstract protected fun popularMangaFromElement(element: Element): SManga + + /** + * Returns the Jsoup selector that returns the tag linking to the next page, or null if + * there's no next page. + */ + abstract protected fun popularMangaNextPageSelector(): String? + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + override fun searchMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + + val mangas = document.select(searchMangaSelector()).map { element -> + searchMangaFromElement(element) + } + + val hasNextPage = searchMangaNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return MangasPage(mangas, hasNextPage) + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. + */ + abstract protected fun searchMangaSelector(): String + + /** + * Returns a manga from the given [element]. Most sites only show the title and the url, it's + * totally fine to fill only those two values. + * + * @param element an element obtained from [searchMangaSelector]. + */ + abstract protected fun searchMangaFromElement(element: Element): SManga + + /** + * Returns the Jsoup selector that returns the tag linking to the next page, or null if + * there's no next page. + */ + abstract protected fun searchMangaNextPageSelector(): String? + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + override fun latestUpdatesParse(response: Response): MangasPage { + val document = response.asJsoup() + + val mangas = document.select(latestUpdatesSelector()).map { element -> + latestUpdatesFromElement(element) + } + + val hasNextPage = latestUpdatesNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return MangasPage(mangas, hasNextPage) + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. + */ + abstract protected fun latestUpdatesSelector(): String + + /** + * Returns a manga from the given [element]. Most sites only show the title and the url, it's + * totally fine to fill only those two values. + * + * @param element an element obtained from [latestUpdatesSelector]. + */ + abstract protected fun latestUpdatesFromElement(element: Element): SManga + + /** + * Returns the Jsoup selector that returns the tag linking to the next page, or null if + * there's no next page. + */ + abstract protected fun latestUpdatesNextPageSelector(): String? + + /** + * Parses the response from the site and returns the details of a manga. + * + * @param response the response from the site. + */ + override fun mangaDetailsParse(response: Response): SManga { + return mangaDetailsParse(response.asJsoup()) + } + + /** + * Returns the details of the manga from the given [document]. + * + * @param document the parsed document. + */ + abstract protected fun mangaDetailsParse(document: Document): SManga + + /** + * Parses the response from the site and returns a list of chapters. + * + * @param response the response from the site. + */ + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + return document.select(chapterListSelector()).map { chapterFromElement(it) } + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter. + */ + abstract protected fun chapterListSelector(): String + + /** + * Returns a chapter from the given element. + * + * @param element an element obtained from [chapterListSelector]. + */ + abstract protected fun chapterFromElement(element: Element): SChapter + + /** + * Parses the response from the site and returns the page list. + * + * @param response the response from the site. + */ + override fun pageListParse(response: Response): List { + return pageListParse(response.asJsoup()) + } + + /** + * Returns a page list from the given document. + * + * @param document the parsed document. + */ + abstract protected fun pageListParse(document: Document): List + + /** + * Parse the response from the site and returns the absolute url to the source image. + * + * @param response the response from the site. + */ + override fun imageUrlParse(response: Response): String { + return imageUrlParse(response.asJsoup()) + } + + /** + * Returns the absolute url to the source image from the document. + * + * @param document the parsed document. + */ + abstract protected fun imageUrlParse(document: Document): String +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt index 3f252409c..4df8dbd3f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt @@ -1,21 +1,21 @@ -package eu.kanade.tachiyomi.ui.base.controller - -import android.os.Bundle -import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate -import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener -import nucleus.factory.PresenterFactory -import nucleus.presenter.Presenter - -@Suppress("LeakingThis") -abstract class NucleusController

>(val bundle: Bundle? = null) : RxController(bundle), - PresenterFactory

{ - - private val delegate = NucleusConductorDelegate(this) - - val presenter: P - get() = delegate.presenter - - init { - addLifecycleListener(NucleusConductorLifecycleListener(delegate)) - } -} +package eu.kanade.tachiyomi.ui.base.controller + +import android.os.Bundle +import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate +import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener +import nucleus.factory.PresenterFactory +import nucleus.presenter.Presenter + +@Suppress("LeakingThis") +abstract class NucleusController

>(val bundle: Bundle? = null) : RxController(bundle), + PresenterFactory

{ + + private val delegate = NucleusConductorDelegate(this) + + val presenter: P + get() = delegate.presenter + + init { + addLifecycleListener(NucleusConductorLifecycleListener(delegate)) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java index 99642a501..46034fbf8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java @@ -1,61 +1,61 @@ -package eu.kanade.tachiyomi.ui.base.presenter; - -import android.os.Bundle; -import androidx.annotation.Nullable; - -import nucleus.factory.PresenterFactory; -import nucleus.presenter.Presenter; - -public class NucleusConductorDelegate

{ - - @Nullable private P presenter; - @Nullable private Bundle bundle; - - private PresenterFactory

factory; - - public NucleusConductorDelegate(PresenterFactory

creator) { - this.factory = creator; - } - - public P getPresenter() { - if (presenter == null) { - presenter = factory.createPresenter(); - presenter.create(bundle); - bundle = null; - } - return presenter; - } - - Bundle onSaveInstanceState() { - Bundle bundle = new Bundle(); -// getPresenter(); // Workaround a crash related to saving instance state with child routers - if (presenter != null) { - presenter.save(bundle); - } - return bundle; - } - - void onRestoreInstanceState(Bundle presenterState) { - bundle = presenterState; - } - - void onTakeView(Object view) { - getPresenter(); - if (presenter != null) { - //noinspection unchecked - presenter.takeView(view); - } - } - - void onDropView() { - if (presenter != null) { - presenter.dropView(); - } - } - - void onDestroy() { - if (presenter != null) { - presenter.destroy(); - } - } -} +package eu.kanade.tachiyomi.ui.base.presenter; + +import android.os.Bundle; +import androidx.annotation.Nullable; + +import nucleus.factory.PresenterFactory; +import nucleus.presenter.Presenter; + +public class NucleusConductorDelegate

{ + + @Nullable private P presenter; + @Nullable private Bundle bundle; + + private PresenterFactory

factory; + + public NucleusConductorDelegate(PresenterFactory

creator) { + this.factory = creator; + } + + public P getPresenter() { + if (presenter == null) { + presenter = factory.createPresenter(); + presenter.create(bundle); + bundle = null; + } + return presenter; + } + + Bundle onSaveInstanceState() { + Bundle bundle = new Bundle(); +// getPresenter(); // Workaround a crash related to saving instance state with child routers + if (presenter != null) { + presenter.save(bundle); + } + return bundle; + } + + void onRestoreInstanceState(Bundle presenterState) { + bundle = presenterState; + } + + void onTakeView(Object view) { + getPresenter(); + if (presenter != null) { + //noinspection unchecked + presenter.takeView(view); + } + } + + void onDropView() { + if (presenter != null) { + presenter.dropView(); + } + } + + void onDestroy() { + if (presenter != null) { + presenter.destroy(); + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java index 36890cd1b..90d94e5d4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java @@ -1,44 +1,44 @@ -package eu.kanade.tachiyomi.ui.base.presenter; - -import android.os.Bundle; -import androidx.annotation.NonNull; -import android.view.View; - -import com.bluelinelabs.conductor.Controller; - -public class NucleusConductorLifecycleListener extends Controller.LifecycleListener { - - private static final String PRESENTER_STATE_KEY = "presenter_state"; - - private NucleusConductorDelegate delegate; - - public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) { - this.delegate = delegate; - } - - @Override - public void postCreateView(@NonNull Controller controller, @NonNull View view) { - delegate.onTakeView(controller); - } - - @Override - public void preDestroyView(@NonNull Controller controller, @NonNull View view) { - delegate.onDropView(); - } - - @Override - public void preDestroy(@NonNull Controller controller) { - delegate.onDestroy(); - } - - @Override - public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) { - outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState()); - } - - @Override - public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) { - delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY)); - } - -} +package eu.kanade.tachiyomi.ui.base.presenter; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import android.view.View; + +import com.bluelinelabs.conductor.Controller; + +public class NucleusConductorLifecycleListener extends Controller.LifecycleListener { + + private static final String PRESENTER_STATE_KEY = "presenter_state"; + + private NucleusConductorDelegate delegate; + + public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) { + this.delegate = delegate; + } + + @Override + public void postCreateView(@NonNull Controller controller, @NonNull View view) { + delegate.onTakeView(controller); + } + + @Override + public void preDestroyView(@NonNull Controller controller, @NonNull View view) { + delegate.onDropView(); + } + + @Override + public void preDestroy(@NonNull Controller controller) { + delegate.onDestroy(); + } + + @Override + public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) { + outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState()); + } + + @Override + public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) { + delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY)); + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt index c55113c16..27e1efa5e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt @@ -1,88 +1,88 @@ -package eu.kanade.tachiyomi.ui.catalogue.filter - -import eu.davidea.flexibleadapter.items.ISectionable -import eu.kanade.tachiyomi.source.model.Filter - -class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable { - - private var head: GroupItem? = null - - override fun getHeader(): GroupItem? = head - - override fun setHeader(header: GroupItem?) { - head = header - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - return filter == (other as TriStateSectionItem).filter - } - - override fun hashCode(): Int { - return filter.hashCode() - } -} - -class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable { - - private var head: GroupItem? = null - - override fun getHeader(): GroupItem? = head - - override fun setHeader(header: GroupItem?) { - head = header - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - return filter == (other as TextSectionItem).filter - } - - override fun hashCode(): Int { - return filter.hashCode() - } -} - -class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISectionable { - - private var head: GroupItem? = null - - override fun getHeader(): GroupItem? = head - - override fun setHeader(header: GroupItem?) { - head = header - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - return filter == (other as CheckboxSectionItem).filter - } - - override fun hashCode(): Int { - return filter.hashCode() - } -} - -class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISectionable { - - private var head: GroupItem? = null - - override fun getHeader(): GroupItem? = head - - override fun setHeader(header: GroupItem?) { - head = header - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - return filter == (other as SelectSectionItem).filter - } - - override fun hashCode(): Int { - return filter.hashCode() - } -} +package eu.kanade.tachiyomi.ui.catalogue.filter + +import eu.davidea.flexibleadapter.items.ISectionable +import eu.kanade.tachiyomi.source.model.Filter + +class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable { + + private var head: GroupItem? = null + + override fun getHeader(): GroupItem? = head + + override fun setHeader(header: GroupItem?) { + head = header + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return filter == (other as TriStateSectionItem).filter + } + + override fun hashCode(): Int { + return filter.hashCode() + } +} + +class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable { + + private var head: GroupItem? = null + + override fun getHeader(): GroupItem? = head + + override fun setHeader(header: GroupItem?) { + head = header + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return filter == (other as TextSectionItem).filter + } + + override fun hashCode(): Int { + return filter.hashCode() + } +} + +class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISectionable { + + private var head: GroupItem? = null + + override fun getHeader(): GroupItem? = head + + override fun setHeader(header: GroupItem?) { + head = header + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return filter == (other as CheckboxSectionItem).filter + } + + override fun hashCode(): Int { + return filter.hashCode() + } +} + +class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISectionable { + + private var head: GroupItem? = null + + override fun getHeader(): GroupItem? = head + + override fun setHeader(header: GroupItem?) { + head = header + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return filter == (other as SelectSectionItem).filter + } + + override fun hashCode(): Int { + return filter.hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt index fba525832..bb253d7b4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt @@ -1,54 +1,54 @@ -package eu.kanade.tachiyomi.ui.catalogue.filter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.davidea.flexibleadapter.items.ISectionable -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.util.setVectorCompat - -class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem>() { - - init { - isExpanded = false - } - - override fun getLayoutRes(): Int { - return R.layout.navigation_view_group - } - - override fun getItemViewType(): Int { - return 100 - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - holder.title.text = filter.name - - holder.icon.setVectorCompat(if (isExpanded) - R.drawable.ic_expand_more_white_24dp - else - R.drawable.ic_chevron_right_white_24dp) - - holder.itemView.setOnClickListener(holder) - - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - return filter == (other as SortGroup).filter - } - - override fun hashCode(): Int { - return filter.hashCode() - } - - class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter) -} +package eu.kanade.tachiyomi.ui.catalogue.filter + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.flexibleadapter.items.ISectionable +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.util.setVectorCompat + +class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem>() { + + init { + isExpanded = false + } + + override fun getLayoutRes(): Int { + return R.layout.navigation_view_group + } + + override fun getItemViewType(): Int { + return 100 + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { + return Holder(view, adapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { + holder.title.text = filter.name + + holder.icon.setVectorCompat(if (isExpanded) + R.drawable.ic_expand_more_white_24dp + else + R.drawable.ic_chevron_right_white_24dp) + + holder.itemView.setOnClickListener(holder) + + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return filter == (other as SortGroup).filter + } + + override fun hashCode(): Int { + return filter.hashCode() + } + + class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt index 9b3b71b0a..07ba3b8e2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt @@ -1,28 +1,28 @@ -package eu.kanade.tachiyomi.ui.catalogue.global_search - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.data.database.models.Manga - -/** - * Adapter that holds the manga items from search results. - * - * @param controller instance of [CatalogueSearchController]. - */ -class CatalogueSearchCardAdapter(controller: CatalogueSearchController) : - FlexibleAdapter(null, controller, true) { - - /** - * Listen for browse item clicks. - */ - val mangaClickListener: OnMangaClickListener = controller - - /** - * Listener which should be called when user clicks browse. - * Note: Should only be handled by [CatalogueSearchController] - */ - interface OnMangaClickListener { - fun onMangaClick(manga: Manga) - fun onMangaLongClick(manga: Manga) - } - +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.data.database.models.Manga + +/** + * Adapter that holds the manga items from search results. + * + * @param controller instance of [CatalogueSearchController]. + */ +class CatalogueSearchCardAdapter(controller: CatalogueSearchController) : + FlexibleAdapter(null, controller, true) { + + /** + * Listen for browse item clicks. + */ + val mangaClickListener: OnMangaClickListener = controller + + /** + * Listener which should be called when user clicks browse. + * Note: Should only be handled by [CatalogueSearchController] + */ + interface OnMangaClickListener { + fun onMangaClick(manga: Manga) + fun onMangaLongClick(manga: Manga) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt index cf21e7437..f6ad6f6eb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt @@ -1,52 +1,52 @@ -package eu.kanade.tachiyomi.ui.catalogue.global_search - -import android.view.View -import com.bumptech.glide.load.engine.DiskCacheStrategy -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.widget.StateImageViewTarget -import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.* - -class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter) - : BaseFlexibleViewHolder(view, adapter) { - - init { - // Call onMangaClickListener when item is pressed. - itemView.setOnClickListener { - val item = adapter.getItem(adapterPosition) - if (item != null) { - 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) { - tvTitle.text = manga.title - // Set alpha of thumbnail. - itemImage.alpha = if (manga.favorite) 0.3f else 1.0f - - setImage(manga) - } - - fun setImage(manga: Manga) { - GlideApp.with(itemView.context).clear(itemImage) - if (!manga.thumbnail_url.isNullOrEmpty()) { - GlideApp.with(itemView.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.DATA) - .centerCrop() - .skipMemoryCache(true) - .placeholder(android.R.color.transparent) - .into(StateImageViewTarget(itemImage, progress)) - } - } - +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.view.View +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.widget.StateImageViewTarget +import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.* + +class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter) + : BaseFlexibleViewHolder(view, adapter) { + + init { + // Call onMangaClickListener when item is pressed. + itemView.setOnClickListener { + val item = adapter.getItem(adapterPosition) + if (item != null) { + 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) { + tvTitle.text = manga.title + // Set alpha of thumbnail. + itemImage.alpha = if (manga.favorite) 0.3f else 1.0f + + setImage(manga) + } + + fun setImage(manga: Manga) { + GlideApp.with(itemView.context).clear(itemImage) + if (!manga.thumbnail_url.isNullOrEmpty()) { + GlideApp.with(itemView.context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .centerCrop() + .skipMemoryCache(true) + .placeholder(android.R.color.transparent) + .into(StateImageViewTarget(itemImage, progress)) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt index 44e790fba..532879e40 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt @@ -1,37 +1,37 @@ -package eu.kanade.tachiyomi.ui.catalogue.global_search - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga - -class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem() { - - override fun getLayoutRes(): Int { - return R.layout.catalogue_global_search_controller_card_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): CatalogueSearchCardHolder { - return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: CatalogueSearchCardHolder, - position: Int, payloads: List?) { - holder.bind(manga) - } - - override fun equals(other: Any?): Boolean { - if (other is CatalogueSearchCardItem) { - return manga.id == other.manga.id - } - return false - } - - override fun hashCode(): Int { - return manga.id?.toInt() ?: 0 - } - -} +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga + +class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.catalogue_global_search_controller_card_item + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): CatalogueSearchCardHolder { + return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter>, holder: CatalogueSearchCardHolder, + position: Int, payloads: List?) { + holder.bind(manga) + } + + override fun equals(other: Any?): Boolean { + if (other is CatalogueSearchCardItem) { + return manga.id == other.manga.id + } + return false + } + + override fun hashCode(): Int { + return manga.id?.toInt() ?: 0 + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt index 8cc6e4aa3..87851de69 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt @@ -1,247 +1,247 @@ -package eu.kanade.tachiyomi.ui.download - -import androidx.recyclerview.widget.LinearLayoutManager -import android.view.* -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.download.DownloadService -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import kotlinx.android.synthetic.main.download_controller.* -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import java.util.* -import java.util.concurrent.TimeUnit - -/** - * Controller that shows the currently active downloads. - * Uses R.layout.fragment_download_queue. - */ -class DownloadController : NucleusController() { - - /** - * Adapter containing the active downloads. - */ - private var adapter: DownloadAdapter? = null - - /** - * Map of subscriptions for active downloads. - */ - private val progressSubscriptions by lazy { HashMap() } - - /** - * Whether the download queue is running or not. - */ - private var isRunning: Boolean = false - - init { - setHasOptionsMenu(true) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.download_controller, container, false) - } - - override fun createPresenter(): DownloadPresenter { - return DownloadPresenter() - } - - override fun getTitle(): String? { - return resources?.getString(R.string.label_download_queue) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - // Check if download queue is empty and update information accordingly. - setInformationView() - - // Initialize adapter. - adapter = DownloadAdapter() - recycler.adapter = adapter - - // Set the layout manager for the recycler and fixed size. - recycler.layoutManager = LinearLayoutManager(view.context) - recycler.setHasFixedSize(true) - - // Suscribe to changes - DownloadService.runningRelay - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { onQueueStatusChange(it) } - - presenter.getDownloadStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { onStatusChange(it) } - - presenter.getDownloadProgressObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { onUpdateDownloadedPages(it) } - } - - override fun onDestroyView(view: View) { - for (subscription in progressSubscriptions.values) { - subscription.unsubscribe() - } - progressSubscriptions.clear() - adapter = null - super.onDestroyView(view) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.download_queue, menu) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - // Set start button visibility. - menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty() - - // Set pause button visibility. - menu.findItem(R.id.pause_queue).isVisible = isRunning - - // Set clear button visibility. - menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val context = applicationContext ?: return false - when (item.itemId) { - R.id.start_queue -> DownloadService.start(context) - R.id.pause_queue -> { - DownloadService.stop(context) - presenter.pauseDownloads() - } - R.id.clear_queue -> { - DownloadService.stop(context) - presenter.clearQueue() - } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Called when the status of a download changes. - * - * @param download the download whose status has changed. - */ - private fun onStatusChange(download: Download) { - when (download.status) { - Download.DOWNLOADING -> { - observeProgress(download) - // Initial update of the downloaded pages - onUpdateDownloadedPages(download) - } - Download.DOWNLOADED -> { - unsubscribeProgress(download) - onUpdateProgress(download) - onUpdateDownloadedPages(download) - } - Download.ERROR -> unsubscribeProgress(download) - } - } - - /** - * Observe the progress of a download and notify the view. - * - * @param download the download to observe its progress. - */ - private fun observeProgress(download: Download) { - val subscription = Observable.interval(50, TimeUnit.MILLISECONDS) - // Get the sum of percentages for all the pages. - .flatMap { - Observable.from(download.pages) - .map(Page::progress) - .reduce { x, y -> x + y } - } - // Keep only the latest emission to avoid backpressure. - .onBackpressureLatest() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { progress -> - // Update the view only if the progress has changed. - if (download.totalProgress != progress) { - download.totalProgress = progress - onUpdateProgress(download) - } - } - - // Avoid leaking subscriptions - progressSubscriptions.remove(download)?.unsubscribe() - - progressSubscriptions.put(download, subscription) - } - - /** - * Unsubscribes the given download from the progress subscriptions. - * - * @param download the download to unsubscribe. - */ - private fun unsubscribeProgress(download: Download) { - progressSubscriptions.remove(download)?.unsubscribe() - } - - /** - * Called when the queue's status has changed. Updates the visibility of the buttons. - * - * @param running whether the queue is now running or not. - */ - private fun onQueueStatusChange(running: Boolean) { - isRunning = running - activity?.invalidateOptionsMenu() - - // Check if download queue is empty and update information accordingly. - setInformationView() - } - - /** - * Called from the presenter to assign the downloads for the adapter. - * - * @param downloads the downloads from the queue. - */ - fun onNextDownloads(downloads: List) { - activity?.invalidateOptionsMenu() - setInformationView() - adapter?.setItems(downloads) - } - - /** - * Called when the progress of a download changes. - * - * @param download the download whose progress has changed. - */ - fun onUpdateProgress(download: Download) { - getHolder(download)?.notifyProgress() - } - - /** - * Called when a page of a download is downloaded. - * - * @param download the download whose page has been downloaded. - */ - fun onUpdateDownloadedPages(download: Download) { - getHolder(download)?.notifyDownloadedPages() - } - - /** - * Returns the holder for the given download. - * - * @param download the download to find. - * @return the holder of the download or null if it's not bound. - */ - private fun getHolder(download: Download): DownloadHolder? { - return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder - } - - /** - * Set information view when queue is empty - */ - private fun setInformationView() { - if (presenter.downloadQueue.isEmpty()) { - empty_view?.show(R.drawable.ic_file_download_black_128dp, - R.string.information_no_downloads) - } else { - empty_view?.hide() - } - } - -} +package eu.kanade.tachiyomi.ui.download + +import androidx.recyclerview.widget.LinearLayoutManager +import android.view.* +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import kotlinx.android.synthetic.main.download_controller.* +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import java.util.* +import java.util.concurrent.TimeUnit + +/** + * Controller that shows the currently active downloads. + * Uses R.layout.fragment_download_queue. + */ +class DownloadController : NucleusController() { + + /** + * Adapter containing the active downloads. + */ + private var adapter: DownloadAdapter? = null + + /** + * Map of subscriptions for active downloads. + */ + private val progressSubscriptions by lazy { HashMap() } + + /** + * Whether the download queue is running or not. + */ + private var isRunning: Boolean = false + + init { + setHasOptionsMenu(true) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.download_controller, container, false) + } + + override fun createPresenter(): DownloadPresenter { + return DownloadPresenter() + } + + override fun getTitle(): String? { + return resources?.getString(R.string.label_download_queue) + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + // Check if download queue is empty and update information accordingly. + setInformationView() + + // Initialize adapter. + adapter = DownloadAdapter() + recycler.adapter = adapter + + // Set the layout manager for the recycler and fixed size. + recycler.layoutManager = LinearLayoutManager(view.context) + recycler.setHasFixedSize(true) + + // Suscribe to changes + DownloadService.runningRelay + .observeOn(AndroidSchedulers.mainThread()) + .subscribeUntilDestroy { onQueueStatusChange(it) } + + presenter.getDownloadStatusObservable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeUntilDestroy { onStatusChange(it) } + + presenter.getDownloadProgressObservable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeUntilDestroy { onUpdateDownloadedPages(it) } + } + + override fun onDestroyView(view: View) { + for (subscription in progressSubscriptions.values) { + subscription.unsubscribe() + } + progressSubscriptions.clear() + adapter = null + super.onDestroyView(view) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.download_queue, menu) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + // Set start button visibility. + menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty() + + // Set pause button visibility. + menu.findItem(R.id.pause_queue).isVisible = isRunning + + // Set clear button visibility. + menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val context = applicationContext ?: return false + when (item.itemId) { + R.id.start_queue -> DownloadService.start(context) + R.id.pause_queue -> { + DownloadService.stop(context) + presenter.pauseDownloads() + } + R.id.clear_queue -> { + DownloadService.stop(context) + presenter.clearQueue() + } + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Called when the status of a download changes. + * + * @param download the download whose status has changed. + */ + private fun onStatusChange(download: Download) { + when (download.status) { + Download.DOWNLOADING -> { + observeProgress(download) + // Initial update of the downloaded pages + onUpdateDownloadedPages(download) + } + Download.DOWNLOADED -> { + unsubscribeProgress(download) + onUpdateProgress(download) + onUpdateDownloadedPages(download) + } + Download.ERROR -> unsubscribeProgress(download) + } + } + + /** + * Observe the progress of a download and notify the view. + * + * @param download the download to observe its progress. + */ + private fun observeProgress(download: Download) { + val subscription = Observable.interval(50, TimeUnit.MILLISECONDS) + // Get the sum of percentages for all the pages. + .flatMap { + Observable.from(download.pages) + .map(Page::progress) + .reduce { x, y -> x + y } + } + // Keep only the latest emission to avoid backpressure. + .onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { progress -> + // Update the view only if the progress has changed. + if (download.totalProgress != progress) { + download.totalProgress = progress + onUpdateProgress(download) + } + } + + // Avoid leaking subscriptions + progressSubscriptions.remove(download)?.unsubscribe() + + progressSubscriptions.put(download, subscription) + } + + /** + * Unsubscribes the given download from the progress subscriptions. + * + * @param download the download to unsubscribe. + */ + private fun unsubscribeProgress(download: Download) { + progressSubscriptions.remove(download)?.unsubscribe() + } + + /** + * Called when the queue's status has changed. Updates the visibility of the buttons. + * + * @param running whether the queue is now running or not. + */ + private fun onQueueStatusChange(running: Boolean) { + isRunning = running + activity?.invalidateOptionsMenu() + + // Check if download queue is empty and update information accordingly. + setInformationView() + } + + /** + * Called from the presenter to assign the downloads for the adapter. + * + * @param downloads the downloads from the queue. + */ + fun onNextDownloads(downloads: List) { + activity?.invalidateOptionsMenu() + setInformationView() + adapter?.setItems(downloads) + } + + /** + * Called when the progress of a download changes. + * + * @param download the download whose progress has changed. + */ + fun onUpdateProgress(download: Download) { + getHolder(download)?.notifyProgress() + } + + /** + * Called when a page of a download is downloaded. + * + * @param download the download whose page has been downloaded. + */ + fun onUpdateDownloadedPages(download: Download) { + getHolder(download)?.notifyDownloadedPages() + } + + /** + * Returns the holder for the given download. + * + * @param download the download to find. + * @return the holder of the download or null if it's not bound. + */ + private fun getHolder(download: Download): DownloadHolder? { + return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder + } + + /** + * Set information view when queue is empty + */ + private fun setInformationView() { + if (presenter.downloadQueue.isEmpty()) { + empty_view?.show(R.drawable.ic_file_download_black_128dp, + R.string.information_no_downloads) + } else { + empty_view?.hide() + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt index 08f933c8e..8513ac91e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt @@ -1,48 +1,48 @@ -package eu.kanade.tachiyomi.ui.library - -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.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.controller.DialogController - -class ChangeMangaCategoriesDialog(bundle: Bundle? = null) : - DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener { - - private var mangas = emptyList() - - private var categories = emptyList() - - private var preselected = emptyArray() - - constructor(target: T, mangas: List, categories: List, - preselected: Array) : this() { - - this.mangas = mangas - this.categories = categories - this.preselected = preselected - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialDialog.Builder(activity!!) - .title(R.string.action_move_category) - .items(categories.map { it.name }) - .itemsCallbackMultiChoice(preselected) { dialog, _, _ -> - val newCategories = dialog.selectedIndices?.map { categories[it] }.orEmpty() - (targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories) - true - } - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .build() - } - - interface Listener { - fun updateCategoriesForMangas(mangas: List, categories: List) - } - +package eu.kanade.tachiyomi.ui.library + +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.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class ChangeMangaCategoriesDialog(bundle: Bundle? = null) : + DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener { + + private var mangas = emptyList() + + private var categories = emptyList() + + private var preselected = emptyArray() + + constructor(target: T, mangas: List, categories: List, + preselected: Array) : this() { + + this.mangas = mangas + this.categories = categories + this.preselected = preselected + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .title(R.string.action_move_category) + .items(categories.map { it.name }) + .itemsCallbackMultiChoice(preselected) { dialog, _, _ -> + val newCategories = dialog.selectedIndices?.map { categories[it] }.orEmpty() + (targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories) + true + } + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .build() + } + + interface Listener { + fun updateCategoriesForMangas(mangas: List, categories: List) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt index 1aa376eb8..1cf40828a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt @@ -1,43 +1,43 @@ -package eu.kanade.tachiyomi.ui.library - -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.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.DialogCheckboxView - -class DeleteLibraryMangasDialog(bundle: Bundle? = null) : - DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener { - - private var mangas = emptyList() - - constructor(target: T, mangas: List) : this() { - this.mangas = mangas - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val view = DialogCheckboxView(activity!!).apply { - setDescription(R.string.confirm_delete_manga) - setOptionDescription(R.string.also_delete_chapters) - } - - return MaterialDialog.Builder(activity!!) - .title(R.string.action_remove) - .customView(view, true) - .positiveText(android.R.string.yes) - .negativeText(android.R.string.no) - .onPositive { _, _ -> - val deleteChapters = view.isChecked() - (targetController as? Listener)?.deleteMangasFromLibrary(mangas, deleteChapters) - } - .build() - } - - interface Listener { - fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) - } +package eu.kanade.tachiyomi.ui.library + +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.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.widget.DialogCheckboxView + +class DeleteLibraryMangasDialog(bundle: Bundle? = null) : + DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener { + + private var mangas = emptyList() + + constructor(target: T, mangas: List) : this() { + this.mangas = mangas + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val view = DialogCheckboxView(activity!!).apply { + setDescription(R.string.confirm_delete_manga) + setOptionDescription(R.string.also_delete_chapters) + } + + return MaterialDialog.Builder(activity!!) + .title(R.string.action_remove) + .customView(view, true) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no) + .onPositive { _, _ -> + val deleteChapters = view.isChecked() + (targetController as? Listener)?.deleteMangasFromLibrary(mangas, deleteChapters) + } + .build() + } + + interface Listener { + fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt index 1557a0edd..061ac919a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt @@ -1,103 +1,103 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.View -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.util.inflate -import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter - -/** - * This adapter stores the categories from the library, used with a ViewPager. - * - * @constructor creates an instance of the adapter. - */ -class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() { - - /** - * The categories to bind in the adapter. - */ - var categories: List = emptyList() - // This setter helps to not refresh the adapter if the reference to the list doesn't change. - set(value) { - if (field !== value) { - field = value - notifyDataSetChanged() - } - } - - private var boundViews = arrayListOf() - - /** - * Creates a new view for this adapter. - * - * @return a new view. - */ - override fun createView(container: ViewGroup): View { - val view = container.inflate(R.layout.library_category) as LibraryCategoryView - view.onCreate(controller) - return view - } - - /** - * Binds a view with a position. - * - * @param view the view to bind. - * @param position the position in the adapter. - */ - override fun bindView(view: View, position: Int) { - (view as LibraryCategoryView).onBind(categories[position]) - boundViews.add(view) - } - - /** - * Recycles a view. - * - * @param view the view to recycle. - * @param position the position in the adapter. - */ - override fun recycleView(view: View, position: Int) { - (view as LibraryCategoryView).onRecycle() - boundViews.remove(view) - } - - /** - * Returns the number of categories. - * - * @return the number of categories or 0 if the list is null. - */ - override fun getCount(): Int { - return categories.size - } - - /** - * Returns the title to display for a category. - * - * @param position the position of the element. - * @return the title to display. - */ - override fun getPageTitle(position: Int): CharSequence { - return categories[position].name - } - - /** - * Returns the position of the view. - */ - override fun getItemPosition(obj: Any): Int { - val view = obj as? LibraryCategoryView ?: return POSITION_NONE - val index = categories.indexOfFirst { it.id == view.category.id } - return if (index == -1) POSITION_NONE else index - } - - /** - * Called when the view of this adapter is being destroyed. - */ - fun onDestroy() { - for (view in boundViews) { - if (view is LibraryCategoryView) { - view.unsubscribe() - } - } - } - +package eu.kanade.tachiyomi.ui.library + +import android.view.View +import android.view.ViewGroup +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter + +/** + * This adapter stores the categories from the library, used with a ViewPager. + * + * @constructor creates an instance of the adapter. + */ +class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() { + + /** + * The categories to bind in the adapter. + */ + var categories: List = emptyList() + // This setter helps to not refresh the adapter if the reference to the list doesn't change. + set(value) { + if (field !== value) { + field = value + notifyDataSetChanged() + } + } + + private var boundViews = arrayListOf() + + /** + * Creates a new view for this adapter. + * + * @return a new view. + */ + override fun createView(container: ViewGroup): View { + val view = container.inflate(R.layout.library_category) as LibraryCategoryView + view.onCreate(controller) + return view + } + + /** + * Binds a view with a position. + * + * @param view the view to bind. + * @param position the position in the adapter. + */ + override fun bindView(view: View, position: Int) { + (view as LibraryCategoryView).onBind(categories[position]) + boundViews.add(view) + } + + /** + * Recycles a view. + * + * @param view the view to recycle. + * @param position the position in the adapter. + */ + override fun recycleView(view: View, position: Int) { + (view as LibraryCategoryView).onRecycle() + boundViews.remove(view) + } + + /** + * Returns the number of categories. + * + * @return the number of categories or 0 if the list is null. + */ + override fun getCount(): Int { + return categories.size + } + + /** + * Returns the title to display for a category. + * + * @param position the position of the element. + * @return the title to display. + */ + override fun getPageTitle(position: Int): CharSequence { + return categories[position].name + } + + /** + * Returns the position of the view. + */ + override fun getItemPosition(obj: Any): Int { + val view = obj as? LibraryCategoryView ?: return POSITION_NONE + val index = categories.indexOfFirst { it.id == view.category.id } + return if (index == -1) POSITION_NONE else index + } + + /** + * Called when the view of this adapter is being destroyed. + */ + fun onDestroy() { + for (view in boundViews) { + if (view is LibraryCategoryView) { + view.unsubscribe() + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index f98de90a3..455cd304e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -1,48 +1,48 @@ -package eu.kanade.tachiyomi.ui.library - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.data.database.models.Manga - -/** - * Adapter storing a list of manga in a certain category. - * - * @param view the fragment containing this adapter. - */ -class LibraryCategoryAdapter(view: LibraryCategoryView) : - FlexibleAdapter(null, view, true) { - - /** - * The list of manga in this category. - */ - private var mangas: List = emptyList() - - /** - * Sets a list of manga in the adapter. - * - * @param list the list to set. - */ - fun setItems(list: List) { - // A copy of manga always unfiltered. - mangas = list.toList() - - performFilter() - } - - /** - * Returns the position in the adapter for the given manga. - * - * @param manga the manga to find. - */ - fun indexOf(manga: Manga): Int { - return currentItems.indexOfFirst { it.manga.id == manga.id } - } - - fun performFilter() { - var s = getFilter(String::class.java) - if (s == null) { - s = "" - } - updateDataSet(mangas.filter { it.filter(s) }) - } - -} +package eu.kanade.tachiyomi.ui.library + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.data.database.models.Manga + +/** + * Adapter storing a list of manga in a certain category. + * + * @param view the fragment containing this adapter. + */ +class LibraryCategoryAdapter(view: LibraryCategoryView) : + FlexibleAdapter(null, view, true) { + + /** + * The list of manga in this category. + */ + private var mangas: List = emptyList() + + /** + * Sets a list of manga in the adapter. + * + * @param list the list to set. + */ + fun setItems(list: List) { + // A copy of manga always unfiltered. + mangas = list.toList() + + performFilter() + } + + /** + * Returns the position in the adapter for the given manga. + * + * @param manga the manga to find. + */ + fun indexOf(manga: Manga): Int { + return currentItems.indexOfFirst { it.manga.id == manga.id } + } + + fun performFilter() { + var s = getFilter(String::class.java) + if (s == null) { + s = "" + } + updateDataSet(mangas.filter { it.filter(s) }) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt index 6221ca00f..bf3859dba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt @@ -1,248 +1,248 @@ -package eu.kanade.tachiyomi.ui.library - -import android.content.Context -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import android.util.AttributeSet -import android.view.View -import android.widget.FrameLayout -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.util.inflate -import eu.kanade.tachiyomi.util.plusAssign -import eu.kanade.tachiyomi.util.toast -import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import kotlinx.android.synthetic.main.library_category.view.* -import rx.subscriptions.CompositeSubscription -import uy.kohesive.injekt.injectLazy - -/** - * Fragment containing the library manga for a certain category. - */ -class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - FrameLayout(context, attrs), - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener { - - /** - * Preferences. - */ - private val preferences: PreferencesHelper by injectLazy() - - /** - * The fragment containing this view. - */ - private lateinit var controller: LibraryController - - /** - * Category for this view. - */ - lateinit var category: Category - private set - - /** - * Recycler view of the list of manga. - */ - private lateinit var recycler: RecyclerView - - /** - * Adapter to hold the manga in this category. - */ - private lateinit var adapter: LibraryCategoryAdapter - - /** - * Subscriptions while the view is bound. - */ - private var subscriptions = CompositeSubscription() - - fun onCreate(controller: LibraryController) { - this.controller = controller - - recycler = if (preferences.libraryAsList().getOrDefault()) { - (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply { - layoutManager = LinearLayoutManager(context) - } - } else { - (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply { - spanCount = controller.mangaPerRow - } - } - - adapter = LibraryCategoryAdapter(this) - - recycler.setHasFixedSize(true) - recycler.adapter = adapter - swipe_refresh.addView(recycler) - - recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) { - // Disable swipe refresh when view is not at the top - val firstPos = (recycler.layoutManager as LinearLayoutManager) - .findFirstCompletelyVisibleItemPosition() - swipe_refresh.isEnabled = firstPos <= 0 - } - }) - - // Double the distance required to trigger sync - swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) - swipe_refresh.setOnRefreshListener { - if (!LibraryUpdateService.isRunning(context)) { - LibraryUpdateService.start(context, category) - context.toast(R.string.updating_category) - } - // It can be a very long operation, so we disable swipe refresh and show a toast. - swipe_refresh.isRefreshing = false - } - } - - fun onBind(category: Category) { - this.category = category - - adapter.mode = if (controller.selectedMangas.isNotEmpty()) { - SelectableAdapter.Mode.MULTI - } else { - SelectableAdapter.Mode.SINGLE - } - - subscriptions += controller.searchRelay - .doOnNext { adapter.setFilter(it) } - .skip(1) - .subscribe { adapter.performFilter() } - - subscriptions += controller.libraryMangaRelay - .subscribe { onNextLibraryManga(it) } - - subscriptions += controller.selectionRelay - .subscribe { onSelectionChanged(it) } - } - - fun onRecycle() { - adapter.setItems(emptyList()) - adapter.clearSelection() - unsubscribe() - } - - fun unsubscribe() { - subscriptions.clear() - } - - /** - * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the - * adapter. - * - * @param event the event received. - */ - fun onNextLibraryManga(event: LibraryMangaEvent) { - // Get the manga list for this category. - val mangaForCategory = event.getMangaForCategory(category).orEmpty() - - // Update the category with its manga. - adapter.setItems(mangaForCategory) - - if (adapter.mode == SelectableAdapter.Mode.MULTI) { - controller.selectedMangas.forEach { manga -> - val position = adapter.indexOf(manga) - if (position != -1 && !adapter.isSelected(position)) { - adapter.toggleSelection(position) - (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation() - } - } - } - } - - /** - * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection - * depending on the type of event received. - * - * @param event the selection event received. - */ - private fun onSelectionChanged(event: LibrarySelectionEvent) { - when (event) { - is LibrarySelectionEvent.Selected -> { - if (adapter.mode != SelectableAdapter.Mode.MULTI) { - adapter.mode = SelectableAdapter.Mode.MULTI - } - findAndToggleSelection(event.manga) - } - is LibrarySelectionEvent.Unselected -> { - findAndToggleSelection(event.manga) - if (controller.selectedMangas.isEmpty()) { - adapter.mode = SelectableAdapter.Mode.SINGLE - } - } - is LibrarySelectionEvent.Cleared -> { - adapter.mode = SelectableAdapter.Mode.SINGLE - adapter.clearSelection() - } - } - } - - /** - * Toggles the selection for the given manga and updates the view if needed. - * - * @param manga the manga to toggle. - */ - private fun findAndToggleSelection(manga: Manga) { - val position = adapter.indexOf(manga) - if (position != -1) { - adapter.toggleSelection(position) - (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation() - } - } - - /** - * Called when a manga is clicked. - * - * @param position the position of the element clicked. - * @return true if the item should be selected, false otherwise. - */ - override fun onItemClick(view: View, position: Int): Boolean { - // If the action mode is created and the position is valid, toggle the selection. - val item = adapter.getItem(position) ?: return false - if (adapter.mode == SelectableAdapter.Mode.MULTI) { - toggleSelection(position) - return true - } else { - openManga(item.manga) - return false - } - } - - /** - * Called when a manga is long clicked. - * - * @param position the position of the element clicked. - */ - override fun onItemLongClick(position: Int) { - controller.createActionModeIfNeeded() - toggleSelection(position) - } - - /** - * Opens a manga. - * - * @param manga the manga to open. - */ - private fun openManga(manga: Manga) { - controller.openManga(manga) - } - - /** - * Tells the presenter to toggle the selection for the given position. - * - * @param position the position to toggle. - */ - private fun toggleSelection(position: Int) { - val item = adapter.getItem(position) ?: return - - controller.setSelection(item.manga, !adapter.isSelected(position)) - controller.invalidateActionMode() - } - -} +package eu.kanade.tachiyomi.ui.library + +import android.content.Context +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.util.plusAssign +import eu.kanade.tachiyomi.util.toast +import eu.kanade.tachiyomi.widget.AutofitRecyclerView +import kotlinx.android.synthetic.main.library_category.view.* +import rx.subscriptions.CompositeSubscription +import uy.kohesive.injekt.injectLazy + +/** + * Fragment containing the library manga for a certain category. + */ +class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + FrameLayout(context, attrs), + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener { + + /** + * Preferences. + */ + private val preferences: PreferencesHelper by injectLazy() + + /** + * The fragment containing this view. + */ + private lateinit var controller: LibraryController + + /** + * Category for this view. + */ + lateinit var category: Category + private set + + /** + * Recycler view of the list of manga. + */ + private lateinit var recycler: RecyclerView + + /** + * Adapter to hold the manga in this category. + */ + private lateinit var adapter: LibraryCategoryAdapter + + /** + * Subscriptions while the view is bound. + */ + private var subscriptions = CompositeSubscription() + + fun onCreate(controller: LibraryController) { + this.controller = controller + + recycler = if (preferences.libraryAsList().getOrDefault()) { + (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply { + layoutManager = LinearLayoutManager(context) + } + } else { + (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply { + spanCount = controller.mangaPerRow + } + } + + adapter = LibraryCategoryAdapter(this) + + recycler.setHasFixedSize(true) + recycler.adapter = adapter + swipe_refresh.addView(recycler) + + recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) { + // Disable swipe refresh when view is not at the top + val firstPos = (recycler.layoutManager as LinearLayoutManager) + .findFirstCompletelyVisibleItemPosition() + swipe_refresh.isEnabled = firstPos <= 0 + } + }) + + // Double the distance required to trigger sync + swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) + swipe_refresh.setOnRefreshListener { + if (!LibraryUpdateService.isRunning(context)) { + LibraryUpdateService.start(context, category) + context.toast(R.string.updating_category) + } + // It can be a very long operation, so we disable swipe refresh and show a toast. + swipe_refresh.isRefreshing = false + } + } + + fun onBind(category: Category) { + this.category = category + + adapter.mode = if (controller.selectedMangas.isNotEmpty()) { + SelectableAdapter.Mode.MULTI + } else { + SelectableAdapter.Mode.SINGLE + } + + subscriptions += controller.searchRelay + .doOnNext { adapter.setFilter(it) } + .skip(1) + .subscribe { adapter.performFilter() } + + subscriptions += controller.libraryMangaRelay + .subscribe { onNextLibraryManga(it) } + + subscriptions += controller.selectionRelay + .subscribe { onSelectionChanged(it) } + } + + fun onRecycle() { + adapter.setItems(emptyList()) + adapter.clearSelection() + unsubscribe() + } + + fun unsubscribe() { + subscriptions.clear() + } + + /** + * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the + * adapter. + * + * @param event the event received. + */ + fun onNextLibraryManga(event: LibraryMangaEvent) { + // Get the manga list for this category. + val mangaForCategory = event.getMangaForCategory(category).orEmpty() + + // Update the category with its manga. + adapter.setItems(mangaForCategory) + + if (adapter.mode == SelectableAdapter.Mode.MULTI) { + controller.selectedMangas.forEach { manga -> + val position = adapter.indexOf(manga) + if (position != -1 && !adapter.isSelected(position)) { + adapter.toggleSelection(position) + (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation() + } + } + } + } + + /** + * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection + * depending on the type of event received. + * + * @param event the selection event received. + */ + private fun onSelectionChanged(event: LibrarySelectionEvent) { + when (event) { + is LibrarySelectionEvent.Selected -> { + if (adapter.mode != SelectableAdapter.Mode.MULTI) { + adapter.mode = SelectableAdapter.Mode.MULTI + } + findAndToggleSelection(event.manga) + } + is LibrarySelectionEvent.Unselected -> { + findAndToggleSelection(event.manga) + if (controller.selectedMangas.isEmpty()) { + adapter.mode = SelectableAdapter.Mode.SINGLE + } + } + is LibrarySelectionEvent.Cleared -> { + adapter.mode = SelectableAdapter.Mode.SINGLE + adapter.clearSelection() + } + } + } + + /** + * Toggles the selection for the given manga and updates the view if needed. + * + * @param manga the manga to toggle. + */ + private fun findAndToggleSelection(manga: Manga) { + val position = adapter.indexOf(manga) + if (position != -1) { + adapter.toggleSelection(position) + (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation() + } + } + + /** + * Called when a manga is clicked. + * + * @param position the position of the element clicked. + * @return true if the item should be selected, false otherwise. + */ + override fun onItemClick(view: View, position: Int): Boolean { + // If the action mode is created and the position is valid, toggle the selection. + val item = adapter.getItem(position) ?: return false + if (adapter.mode == SelectableAdapter.Mode.MULTI) { + toggleSelection(position) + return true + } else { + openManga(item.manga) + return false + } + } + + /** + * Called when a manga is long clicked. + * + * @param position the position of the element clicked. + */ + override fun onItemLongClick(position: Int) { + controller.createActionModeIfNeeded() + toggleSelection(position) + } + + /** + * Opens a manga. + * + * @param manga the manga to open. + */ + private fun openManga(manga: Manga) { + controller.openManga(manga) + } + + /** + * Tells the presenter to toggle the selection for the given position. + * + * @param position the position to toggle. + */ + private fun toggleSelection(position: Int) { + val item = adapter.getItem(position) ?: return + + controller.setSelection(item.manga, !adapter.isSelected(position)) + controller.invalidateActionMode() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index a5eb2c310..4ff8beb2b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -1,524 +1,524 @@ -package eu.kanade.tachiyomi.ui.library - -import android.app.Activity -import android.content.Intent -import android.content.res.Configuration -import android.graphics.Color -import android.os.Bundle -import com.google.android.material.tabs.TabLayout -import androidx.core.graphics.drawable.DrawableCompat -import androidx.drawerlayout.widget.DrawerLayout -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.appcompat.widget.SearchView -import android.view.* -import androidx.core.view.GravityCompat -import com.bluelinelabs.conductor.ControllerChangeHandler -import com.bluelinelabs.conductor.ControllerChangeType -import com.f2prateek.rx.preferences.Preference -import com.jakewharton.rxbinding.support.v4.view.pageSelections -import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController -import eu.kanade.tachiyomi.ui.base.controller.TabbedController -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.category.CategoryController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.migration.MigrationController -import eu.kanade.tachiyomi.util.inflate -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.library_controller.* -import kotlinx.android.synthetic.main.main_activity.* -import rx.Subscription -import timber.log.Timber -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.IOException - - -class LibraryController( - bundle: Bundle? = null, - private val preferences: PreferencesHelper = Injekt.get() -) : NucleusController(bundle), - TabbedController, - SecondaryDrawerController, - ActionMode.Callback, - ChangeMangaCategoriesDialog.Listener, - DeleteLibraryMangasDialog.Listener { - - /** - * Position of the active category. - */ - var activeCategory: Int = preferences.lastUsedCategory().getOrDefault() - private set - - /** - * Action mode for selections. - */ - private var actionMode: ActionMode? = null - - /** - * Library search query. - */ - private var query = "" - - /** - * Currently selected mangas. - */ - val selectedMangas = mutableSetOf() - - private var selectedCoverManga: Manga? = null - - /** - * Relay to notify the UI of selection updates. - */ - val selectionRelay: PublishRelay = PublishRelay.create() - - /** - * Relay to notify search query changes. - */ - val searchRelay: BehaviorRelay = BehaviorRelay.create() - - /** - * Relay to notify the library's viewpager for updates. - */ - val libraryMangaRelay: BehaviorRelay = BehaviorRelay.create() - - /** - * Number of manga per row in grid mode. - */ - var mangaPerRow = 0 - private set - - /** - * Adapter of the view pager. - */ - private var adapter: LibraryAdapter? = null - - /** - * Navigation view containing filter/sort/display items. - */ - private var navView: LibraryNavigationView? = null - - /** - * Drawer listener to allow swipe only for closing the drawer. - */ - private var drawerListener: DrawerLayout.DrawerListener? = null - - private var tabsVisibilityRelay: BehaviorRelay = BehaviorRelay.create(false) - - private var tabsVisibilitySubscription: Subscription? = null - - private var searchViewSubscription: Subscription? = null - - init { - setHasOptionsMenu(true) - retainViewMode = RetainViewMode.RETAIN_DETACH - } - - override fun getTitle(): String? { - return resources?.getString(R.string.label_library) - } - - override fun createPresenter(): LibraryPresenter { - return LibraryPresenter() - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.library_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - adapter = LibraryAdapter(this) - library_pager.adapter = adapter - library_pager.pageSelections().skip(1).subscribeUntilDestroy { - preferences.lastUsedCategory().set(it) - activeCategory = it - } - - getColumnsPreferenceForCurrentOrientation().asObservable() - .doOnNext { mangaPerRow = it } - .skip(1) - // Set again the adapter to recalculate the covers height - .subscribeUntilDestroy { reattachAdapter() } - - if (selectedMangas.isNotEmpty()) { - createActionModeIfNeeded() - } - } - - override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeStarted(handler, type) - if (type.isEnter) { - activity?.tabs?.setupWithViewPager(library_pager) - presenter.subscribeLibrary() - } - } - - override fun onDestroyView(view: View) { - adapter?.onDestroy() - adapter = null - actionMode = null - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = null - super.onDestroyView(view) - } - - override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup { - val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView - navView = view - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END) - - navView?.onGroupClicked = { group -> - when (group) { - is LibraryNavigationView.FilterGroup -> onFilterChanged() - is LibraryNavigationView.SortGroup -> onSortChanged() - is LibraryNavigationView.DisplayGroup -> reattachAdapter() - is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged() - } - } - - return view - } - - override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { - navView = null - } - - override fun configureTabs(tabs: TabLayout) { - with(tabs) { - tabGravity = TabLayout.GRAVITY_CENTER - tabMode = TabLayout.MODE_SCROLLABLE - } - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible -> - val tabAnimator = (activity as? MainActivity)?.tabAnimator - if (visible) { - tabAnimator?.expand() - } else { - tabAnimator?.collapse() - } - } - } - - override fun cleanupTabs(tabs: TabLayout) { - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = null - } - - fun onNextLibraryUpdate(categories: List, mangaMap: Map>) { - val view = view ?: return - val adapter = adapter ?: return - - // Show empty view if needed - if (mangaMap.isNotEmpty()) { - empty_view.hide() - } else { - empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library) - } - - // Get the current active category. - val activeCat = if (adapter.categories.isNotEmpty()) - library_pager.currentItem - else - activeCategory - - // Set the categories - adapter.categories = categories - - // Restore active category. - library_pager.setCurrentItem(activeCat, false) - - tabsVisibilityRelay.call(categories.size > 1) - - // Delay the scroll position to allow the view to be properly measured. - view.post { - if (isAttached) { - activity?.tabs?.setScrollPosition(library_pager.currentItem, 0f, true) - } - } - - // Send the manga map to child fragments after the adapter is updated. - libraryMangaRelay.call(LibraryMangaEvent(mangaMap)) - } - - /** - * Returns a preference for the number of manga per row based on the current orientation. - * - * @return the preference. - */ - private fun getColumnsPreferenceForCurrentOrientation(): Preference { - return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) - preferences.portraitColumns() - else - preferences.landscapeColumns() - } - - /** - * Called when a filter is changed. - */ - private fun onFilterChanged() { - presenter.requestFilterUpdate() - activity?.invalidateOptionsMenu() - } - - private fun onDownloadBadgeChanged() { - presenter.requestDownloadBadgesUpdate() - } - - /** - * Called when the sorting mode is changed. - */ - private fun onSortChanged() { - presenter.requestSortUpdate() - } - - /** - * Reattaches the adapter to the view pager to recreate fragments - */ - private fun reattachAdapter() { - val adapter = adapter ?: return - - val position = library_pager.currentItem - - adapter.recycle = false - library_pager.adapter = adapter - library_pager.currentItem = position - adapter.recycle = true - } - - /** - * Creates the action mode if it's not created already. - */ - fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = (activity as AppCompatActivity).startSupportActionMode(this) - } - } - - /** - * Destroys the action mode. - */ - fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.library, menu) - - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - - if (!query.isEmpty()) { - searchItem.expandActionView() - searchView.setQuery(query, true) - searchView.clearFocus() - } - - // Mutate the filter icon because it needs to be tinted and the resource is shared. - menu.findItem(R.id.action_filter).icon.mutate() - - searchViewSubscription?.unsubscribe() - searchViewSubscription = searchView.queryTextChanges() - // Ignore events if this controller isn't at the top - .filter { router.backstack.lastOrNull()?.controller() == this } - .subscribeUntilDestroy { - query = it.toString() - searchRelay.call(query) - } - - searchItem.fixExpand() - } - - override fun onPrepareOptionsMenu(menu: Menu) { - val navView = navView ?: return - - val filterItem = menu.findItem(R.id.action_filter) - - // Tint icon if there's a filter active - val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE - DrawableCompat.setTint(filterItem.icon, filterColor) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_filter -> { - navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) } - } - R.id.action_update_library -> { - activity?.let { LibraryUpdateService.start(it) } - } - R.id.action_edit_categories -> { - router.pushController(CategoryController().withFadeTransaction()) - } - R.id.action_source_migration -> { - router.pushController(MigrationController().withFadeTransaction()) - } - else -> return super.onOptionsItemSelected(item) - } - - return true - } - - /** - * Invalidates the action mode, forcing it to refresh its content. - */ - fun invalidateActionMode() { - actionMode?.invalidate() - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.library_selection, menu) - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = selectedMangas.size - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = resources?.getString(R.string.label_selected, count) - menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1 - } - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_edit_cover -> { - changeSelectedCover() - destroyActionModeIfNeeded() - } - R.id.action_move_to_category -> showChangeMangaCategoriesDialog() - R.id.action_delete -> showDeleteMangaDialog() - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode?) { - // Clear all the manga selections and notify child views. - selectedMangas.clear() - selectionRelay.call(LibrarySelectionEvent.Cleared()) - actionMode = null - } - - fun openManga(manga: Manga) { - // Notify the presenter a manga is being opened. - presenter.onOpenManga() - - router.pushController(MangaController(manga).withFadeTransaction()) - } - - /** - * Sets the selection for a given manga. - * - * @param manga the manga whose selection has changed. - * @param selected whether it's now selected or not. - */ - fun setSelection(manga: Manga, selected: Boolean) { - if (selected) { - if (selectedMangas.add(manga)) { - selectionRelay.call(LibrarySelectionEvent.Selected(manga)) - } - } else { - if (selectedMangas.remove(manga)) { - selectionRelay.call(LibrarySelectionEvent.Unselected(manga)) - } - } - } - - /** - * Move the selected manga to a list of categories. - */ - private fun showChangeMangaCategoriesDialog() { - // Create a copy of selected manga - val mangas = selectedMangas.toList() - - // Hide the default category because it has a different behavior than the ones from db. - val categories = presenter.categories.filter { it.id != 0 } - - // Get indexes of the common categories to preselect. - val commonCategoriesIndexes = presenter.getCommonCategories(mangas) - .map { categories.indexOf(it) } - .toTypedArray() - - ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes) - .showDialog(router) - } - - private fun showDeleteMangaDialog() { - DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router) - } - - override fun updateCategoriesForMangas(mangas: List, categories: List) { - presenter.moveMangasToCategories(categories, mangas) - destroyActionModeIfNeeded() - } - - override fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) { - presenter.removeMangaFromLibrary(mangas, deleteChapters) - destroyActionModeIfNeeded() - } - - /** - * Changes the cover for the selected manga. - */ - private fun changeSelectedCover() { - val manga = selectedMangas.firstOrNull() ?: return - selectedCoverManga = manga - - if (manga.favorite) { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "image/*" - startActivityForResult(Intent.createChooser(intent, - resources?.getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN) - } else { - activity?.toast(R.string.notification_first_add_to_library) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == REQUEST_IMAGE_OPEN) { - if (data == null || resultCode != Activity.RESULT_OK) return - val activity = activity ?: return - val manga = selectedCoverManga ?: return - - try { - // Get the file's input stream from the incoming Intent - activity.contentResolver.openInputStream(data.data).use { - // Update cover to selected file, show error if something went wrong - if (presenter.editCoverWithStream(it, manga)) { - // TODO refresh cover - } else { - activity.toast(R.string.notification_cover_update_failed) - } - } - } catch (error: IOException) { - activity.toast(R.string.notification_cover_update_failed) - Timber.e(error) - } - selectedCoverManga = null - } - } - - private companion object { - /** - * Key to change the cover of a manga in [onActivityResult]. - */ - const val REQUEST_IMAGE_OPEN = 101 - } - -} +package eu.kanade.tachiyomi.ui.library + +import android.app.Activity +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Color +import android.os.Bundle +import com.google.android.material.tabs.TabLayout +import androidx.core.graphics.drawable.DrawableCompat +import androidx.drawerlayout.widget.DrawerLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.appcompat.widget.SearchView +import android.view.* +import androidx.core.view.GravityCompat +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import com.f2prateek.rx.preferences.Preference +import com.jakewharton.rxbinding.support.v4.view.pageSelections +import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController +import eu.kanade.tachiyomi.ui.base.controller.TabbedController +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.category.CategoryController +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.migration.MigrationController +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.library_controller.* +import kotlinx.android.synthetic.main.main_activity.* +import rx.Subscription +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.IOException + + +class LibraryController( + bundle: Bundle? = null, + private val preferences: PreferencesHelper = Injekt.get() +) : NucleusController(bundle), + TabbedController, + SecondaryDrawerController, + ActionMode.Callback, + ChangeMangaCategoriesDialog.Listener, + DeleteLibraryMangasDialog.Listener { + + /** + * Position of the active category. + */ + var activeCategory: Int = preferences.lastUsedCategory().getOrDefault() + private set + + /** + * Action mode for selections. + */ + private var actionMode: ActionMode? = null + + /** + * Library search query. + */ + private var query = "" + + /** + * Currently selected mangas. + */ + val selectedMangas = mutableSetOf() + + private var selectedCoverManga: Manga? = null + + /** + * Relay to notify the UI of selection updates. + */ + val selectionRelay: PublishRelay = PublishRelay.create() + + /** + * Relay to notify search query changes. + */ + val searchRelay: BehaviorRelay = BehaviorRelay.create() + + /** + * Relay to notify the library's viewpager for updates. + */ + val libraryMangaRelay: BehaviorRelay = BehaviorRelay.create() + + /** + * Number of manga per row in grid mode. + */ + var mangaPerRow = 0 + private set + + /** + * Adapter of the view pager. + */ + private var adapter: LibraryAdapter? = null + + /** + * Navigation view containing filter/sort/display items. + */ + private var navView: LibraryNavigationView? = null + + /** + * Drawer listener to allow swipe only for closing the drawer. + */ + private var drawerListener: DrawerLayout.DrawerListener? = null + + private var tabsVisibilityRelay: BehaviorRelay = BehaviorRelay.create(false) + + private var tabsVisibilitySubscription: Subscription? = null + + private var searchViewSubscription: Subscription? = null + + init { + setHasOptionsMenu(true) + retainViewMode = RetainViewMode.RETAIN_DETACH + } + + override fun getTitle(): String? { + return resources?.getString(R.string.label_library) + } + + override fun createPresenter(): LibraryPresenter { + return LibraryPresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.library_controller, container, false) + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + adapter = LibraryAdapter(this) + library_pager.adapter = adapter + library_pager.pageSelections().skip(1).subscribeUntilDestroy { + preferences.lastUsedCategory().set(it) + activeCategory = it + } + + getColumnsPreferenceForCurrentOrientation().asObservable() + .doOnNext { mangaPerRow = it } + .skip(1) + // Set again the adapter to recalculate the covers height + .subscribeUntilDestroy { reattachAdapter() } + + if (selectedMangas.isNotEmpty()) { + createActionModeIfNeeded() + } + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeStarted(handler, type) + if (type.isEnter) { + activity?.tabs?.setupWithViewPager(library_pager) + presenter.subscribeLibrary() + } + } + + override fun onDestroyView(view: View) { + adapter?.onDestroy() + adapter = null + actionMode = null + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = null + super.onDestroyView(view) + } + + override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup { + val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView + navView = view + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END) + + navView?.onGroupClicked = { group -> + when (group) { + is LibraryNavigationView.FilterGroup -> onFilterChanged() + is LibraryNavigationView.SortGroup -> onSortChanged() + is LibraryNavigationView.DisplayGroup -> reattachAdapter() + is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged() + } + } + + return view + } + + override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { + navView = null + } + + override fun configureTabs(tabs: TabLayout) { + with(tabs) { + tabGravity = TabLayout.GRAVITY_CENTER + tabMode = TabLayout.MODE_SCROLLABLE + } + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible -> + val tabAnimator = (activity as? MainActivity)?.tabAnimator + if (visible) { + tabAnimator?.expand() + } else { + tabAnimator?.collapse() + } + } + } + + override fun cleanupTabs(tabs: TabLayout) { + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = null + } + + fun onNextLibraryUpdate(categories: List, mangaMap: Map>) { + val view = view ?: return + val adapter = adapter ?: return + + // Show empty view if needed + if (mangaMap.isNotEmpty()) { + empty_view.hide() + } else { + empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library) + } + + // Get the current active category. + val activeCat = if (adapter.categories.isNotEmpty()) + library_pager.currentItem + else + activeCategory + + // Set the categories + adapter.categories = categories + + // Restore active category. + library_pager.setCurrentItem(activeCat, false) + + tabsVisibilityRelay.call(categories.size > 1) + + // Delay the scroll position to allow the view to be properly measured. + view.post { + if (isAttached) { + activity?.tabs?.setScrollPosition(library_pager.currentItem, 0f, true) + } + } + + // Send the manga map to child fragments after the adapter is updated. + libraryMangaRelay.call(LibraryMangaEvent(mangaMap)) + } + + /** + * Returns a preference for the number of manga per row based on the current orientation. + * + * @return the preference. + */ + private fun getColumnsPreferenceForCurrentOrientation(): Preference { + return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) + preferences.portraitColumns() + else + preferences.landscapeColumns() + } + + /** + * Called when a filter is changed. + */ + private fun onFilterChanged() { + presenter.requestFilterUpdate() + activity?.invalidateOptionsMenu() + } + + private fun onDownloadBadgeChanged() { + presenter.requestDownloadBadgesUpdate() + } + + /** + * Called when the sorting mode is changed. + */ + private fun onSortChanged() { + presenter.requestSortUpdate() + } + + /** + * Reattaches the adapter to the view pager to recreate fragments + */ + private fun reattachAdapter() { + val adapter = adapter ?: return + + val position = library_pager.currentItem + + adapter.recycle = false + library_pager.adapter = adapter + library_pager.currentItem = position + adapter.recycle = true + } + + /** + * Creates the action mode if it's not created already. + */ + fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = (activity as AppCompatActivity).startSupportActionMode(this) + } + } + + /** + * Destroys the action mode. + */ + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.library, menu) + + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + + if (!query.isEmpty()) { + searchItem.expandActionView() + searchView.setQuery(query, true) + searchView.clearFocus() + } + + // Mutate the filter icon because it needs to be tinted and the resource is shared. + menu.findItem(R.id.action_filter).icon.mutate() + + searchViewSubscription?.unsubscribe() + searchViewSubscription = searchView.queryTextChanges() + // Ignore events if this controller isn't at the top + .filter { router.backstack.lastOrNull()?.controller() == this } + .subscribeUntilDestroy { + query = it.toString() + searchRelay.call(query) + } + + searchItem.fixExpand() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + val navView = navView ?: return + + val filterItem = menu.findItem(R.id.action_filter) + + // Tint icon if there's a filter active + val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE + DrawableCompat.setTint(filterItem.icon, filterColor) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_filter -> { + navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) } + } + R.id.action_update_library -> { + activity?.let { LibraryUpdateService.start(it) } + } + R.id.action_edit_categories -> { + router.pushController(CategoryController().withFadeTransaction()) + } + R.id.action_source_migration -> { + router.pushController(MigrationController().withFadeTransaction()) + } + else -> return super.onOptionsItemSelected(item) + } + + return true + } + + /** + * Invalidates the action mode, forcing it to refresh its content. + */ + fun invalidateActionMode() { + actionMode?.invalidate() + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.library_selection, menu) + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = selectedMangas.size + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = resources?.getString(R.string.label_selected, count) + menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1 + } + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_edit_cover -> { + changeSelectedCover() + destroyActionModeIfNeeded() + } + R.id.action_move_to_category -> showChangeMangaCategoriesDialog() + R.id.action_delete -> showDeleteMangaDialog() + else -> return false + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode?) { + // Clear all the manga selections and notify child views. + selectedMangas.clear() + selectionRelay.call(LibrarySelectionEvent.Cleared()) + actionMode = null + } + + fun openManga(manga: Manga) { + // Notify the presenter a manga is being opened. + presenter.onOpenManga() + + router.pushController(MangaController(manga).withFadeTransaction()) + } + + /** + * Sets the selection for a given manga. + * + * @param manga the manga whose selection has changed. + * @param selected whether it's now selected or not. + */ + fun setSelection(manga: Manga, selected: Boolean) { + if (selected) { + if (selectedMangas.add(manga)) { + selectionRelay.call(LibrarySelectionEvent.Selected(manga)) + } + } else { + if (selectedMangas.remove(manga)) { + selectionRelay.call(LibrarySelectionEvent.Unselected(manga)) + } + } + } + + /** + * Move the selected manga to a list of categories. + */ + private fun showChangeMangaCategoriesDialog() { + // Create a copy of selected manga + val mangas = selectedMangas.toList() + + // Hide the default category because it has a different behavior than the ones from db. + val categories = presenter.categories.filter { it.id != 0 } + + // Get indexes of the common categories to preselect. + val commonCategoriesIndexes = presenter.getCommonCategories(mangas) + .map { categories.indexOf(it) } + .toTypedArray() + + ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes) + .showDialog(router) + } + + private fun showDeleteMangaDialog() { + DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router) + } + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + presenter.moveMangasToCategories(categories, mangas) + destroyActionModeIfNeeded() + } + + override fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) { + presenter.removeMangaFromLibrary(mangas, deleteChapters) + destroyActionModeIfNeeded() + } + + /** + * Changes the cover for the selected manga. + */ + private fun changeSelectedCover() { + val manga = selectedMangas.firstOrNull() ?: return + selectedCoverManga = manga + + if (manga.favorite) { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "image/*" + startActivityForResult(Intent.createChooser(intent, + resources?.getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN) + } else { + activity?.toast(R.string.notification_first_add_to_library) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_IMAGE_OPEN) { + if (data == null || resultCode != Activity.RESULT_OK) return + val activity = activity ?: return + val manga = selectedCoverManga ?: return + + try { + // Get the file's input stream from the incoming Intent + activity.contentResolver.openInputStream(data.data).use { + // Update cover to selected file, show error if something went wrong + if (presenter.editCoverWithStream(it, manga)) { + // TODO refresh cover + } else { + activity.toast(R.string.notification_cover_update_failed) + } + } + } catch (error: IOException) { + activity.toast(R.string.notification_cover_update_failed) + Timber.e(error) + } + selectedCoverManga = null + } + } + + private companion object { + /** + * Key to change the cover of a manga in [onActivityResult]. + */ + const val REQUEST_IMAGE_OPEN = 101 + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt index 2bc68cf3d..0584f8c7a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt @@ -1,57 +1,57 @@ -package eu.kanade.tachiyomi.ui.library - -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.source.LocalSource -import kotlinx.android.synthetic.main.catalogue_grid_item.* - -/** - * Class used to hold the displayed data of a manga in the library, like the cover or the title. - * All the elements from the layout file "item_catalogue_grid" are available in this class. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param listener a listener to react to single tap and long tap events. - * @constructor creates a new library holder. - */ -class LibraryGridHolder( - private val view: View, - private val adapter: FlexibleAdapter<*> - -) : LibraryHolder(view, adapter) { - - /** - * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param item the manga item to bind. - */ - override fun onSetValues(item: LibraryItem) { - // Update the title of the manga. - title.text = item.manga.title - - // Update the unread count and its visibility. - with(unread_text) { - visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE - text = item.manga.unread.toString() - } - // Update the download count and its visibility. - with(download_text) { - visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE - text = item.downloadCount.toString() - } - //set local visibility if its local manga - local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE - - // Update the cover. - GlideApp.with(view.context).clear(thumbnail) - GlideApp.with(view.context) - .load(item.manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(thumbnail) - } - -} +package eu.kanade.tachiyomi.ui.library + +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.source.LocalSource +import kotlinx.android.synthetic.main.catalogue_grid_item.* + +/** + * Class used to hold the displayed data of a manga in the library, like the cover or the title. + * All the elements from the layout file "item_catalogue_grid" are available in this class. + * + * @param view the inflated view for this holder. + * @param adapter the adapter handling this holder. + * @param listener a listener to react to single tap and long tap events. + * @constructor creates a new library holder. + */ +class LibraryGridHolder( + private val view: View, + private val adapter: FlexibleAdapter<*> + +) : LibraryHolder(view, adapter) { + + /** + * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this + * holder with the given manga. + * + * @param item the manga item to bind. + */ + override fun onSetValues(item: LibraryItem) { + // Update the title of the manga. + title.text = item.manga.title + + // Update the unread count and its visibility. + with(unread_text) { + visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE + text = item.manga.unread.toString() + } + // Update the download count and its visibility. + with(download_text) { + visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE + text = item.downloadCount.toString() + } + //set local visibility if its local manga + local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE + + // Update the cover. + GlideApp.with(view.context).clear(thumbnail) + GlideApp.with(view.context) + .load(item.manga) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .into(thumbnail) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt index 41d7f9879..4136ce312 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -1,27 +1,27 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.View -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder - -/** - * Generic class used to hold the displayed data of a manga in the library. - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param listener a listener to react to the single tap and long tap events. - */ - -abstract class LibraryHolder( - view: View, - adapter: FlexibleAdapter<*> -) : BaseFlexibleViewHolder(view, adapter) { - - /** - * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param item the manga item to bind. - */ - abstract fun onSetValues(item: LibraryItem) - -} +package eu.kanade.tachiyomi.ui.library + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder + +/** + * Generic class used to hold the displayed data of a manga in the library. + * @param view the inflated view for this holder. + * @param adapter the adapter handling this holder. + * @param listener a listener to react to the single tap and long tap events. + */ + +abstract class LibraryHolder( + view: View, + adapter: FlexibleAdapter<*> +) : BaseFlexibleViewHolder(view, adapter) { + + /** + * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this + * holder with the given manga. + * + * @param item the manga item to bind. + */ + abstract fun onSetValues(item: LibraryItem) + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index 0191e2b1b..e12a1b5c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -1,75 +1,75 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.Gravity -import android.view.View -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.widget.FrameLayout -import androidx.recyclerview.widget.RecyclerView -import com.f2prateek.rx.preferences.Preference -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFilterable -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.LibraryManga -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import kotlinx.android.synthetic.main.catalogue_grid_item.view.* - -class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference) : - AbstractFlexibleItem(), IFilterable { - - var downloadCount = -1 - - override fun getLayoutRes(): Int { - return if (libraryAsList.getOrDefault()) - R.layout.catalogue_list_item - else - R.layout.catalogue_grid_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): LibraryHolder { - val parent = adapter.recyclerView - return if (parent is AutofitRecyclerView) { - view.apply { - val coverHeight = parent.itemWidth / 3 * 4 - card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) - gradient.layoutParams = FrameLayout.LayoutParams( - MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM) - } - LibraryGridHolder(view, adapter) - } else { - LibraryListHolder(view, adapter) - } - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, - holder: LibraryHolder, - position: Int, - payloads: List?) { - - holder.onSetValues(this) - } - - /** - * Filters a manga depending on a query. - * - * @param constraint the query to apply. - * @return true if the manga should be included, false otherwise. - */ - override fun filter(constraint: String): Boolean { - return manga.title.contains(constraint, true) || - (manga.author?.contains(constraint, true) ?: false) - } - - override fun equals(other: Any?): Boolean { - if (other is LibraryItem) { - return manga.id == other.manga.id - } - return false - } - - override fun hashCode(): Int { - return manga.id!!.hashCode() - } -} +package eu.kanade.tachiyomi.ui.library + +import android.view.Gravity +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import androidx.recyclerview.widget.RecyclerView +import com.f2prateek.rx.preferences.Preference +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.widget.AutofitRecyclerView +import kotlinx.android.synthetic.main.catalogue_grid_item.view.* + +class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference) : + AbstractFlexibleItem(), IFilterable { + + var downloadCount = -1 + + override fun getLayoutRes(): Int { + return if (libraryAsList.getOrDefault()) + R.layout.catalogue_list_item + else + R.layout.catalogue_grid_item + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): LibraryHolder { + val parent = adapter.recyclerView + return if (parent is AutofitRecyclerView) { + view.apply { + val coverHeight = parent.itemWidth / 3 * 4 + card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) + gradient.layoutParams = FrameLayout.LayoutParams( + MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM) + } + LibraryGridHolder(view, adapter) + } else { + LibraryListHolder(view, adapter) + } + } + + override fun bindViewHolder(adapter: FlexibleAdapter>, + holder: LibraryHolder, + position: Int, + payloads: List?) { + + holder.onSetValues(this) + } + + /** + * Filters a manga depending on a query. + * + * @param constraint the query to apply. + * @return true if the manga should be included, false otherwise. + */ + override fun filter(constraint: String): Boolean { + return manga.title.contains(constraint, true) || + (manga.author?.contains(constraint, true) ?: false) + } + + override fun equals(other: Any?): Boolean { + if (other is LibraryItem) { + return manga.id == other.manga.id + } + return false + } + + override fun hashCode(): Int { + return manga.id!!.hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt index 83cc69e25..53b448dd7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt @@ -1,65 +1,65 @@ -package eu.kanade.tachiyomi.ui.library - -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.source.LocalSource -import kotlinx.android.synthetic.main.catalogue_list_item.* - -/** - * Class used to hold the displayed data of a manga in the library, like the cover or the title. - * All the elements from the layout file "item_library_list" are available in this class. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param listener a listener to react to single tap and long tap events. - * @constructor creates a new library holder. - */ - -class LibraryListHolder( - private val view: View, - private val adapter: FlexibleAdapter<*> -) : LibraryHolder(view, adapter) { - - /** - * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param item the manga item to bind. - */ - override fun onSetValues(item: LibraryItem) { - // Update the title of the manga. - title.text = item.manga.title - - // Update the unread count and its visibility. - with(unread_text) { - visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE - text = item.manga.unread.toString() - } - // Update the download count and its visibility. - with(download_text) { - visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE - text = "${item.downloadCount}" - } - //show local text badge if local manga - local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE - - // 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) - } - -} +package eu.kanade.tachiyomi.ui.library + +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.source.LocalSource +import kotlinx.android.synthetic.main.catalogue_list_item.* + +/** + * Class used to hold the displayed data of a manga in the library, like the cover or the title. + * All the elements from the layout file "item_library_list" are available in this class. + * + * @param view the inflated view for this holder. + * @param adapter the adapter handling this holder. + * @param listener a listener to react to single tap and long tap events. + * @constructor creates a new library holder. + */ + +class LibraryListHolder( + private val view: View, + private val adapter: FlexibleAdapter<*> +) : LibraryHolder(view, adapter) { + + /** + * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this + * holder with the given manga. + * + * @param item the manga item to bind. + */ + override fun onSetValues(item: LibraryItem) { + // Update the title of the manga. + title.text = item.manga.title + + // Update the unread count and its visibility. + with(unread_text) { + visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE + text = item.manga.unread.toString() + } + // Update the download count and its visibility. + with(download_text) { + visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE + text = "${item.downloadCount}" + } + //show local text badge if local manga + local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE + + // 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) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt index aa9f0b666..2b8d57f8a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt @@ -1,217 +1,217 @@ -package eu.kanade.tachiyomi.ui.library - -import android.content.Context -import android.util.AttributeSet -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.widget.ExtendedNavigationView -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE -import uy.kohesive.injekt.injectLazy - -/** - * The navigation view shown in a drawer with the different options to show the library. - */ -class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) - : ExtendedNavigationView(context, attrs) { - - /** - * Preferences helper. - */ - private val preferences: PreferencesHelper by injectLazy() - - /** - * List of groups shown in the view. - */ - private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup(), BadgeGroup()) - - /** - * Adapter instance. - */ - private val adapter = Adapter(groups.map { it.createItems() }.flatten()) - - /** - * Click listener to notify the parent fragment when an item from a group is clicked. - */ - var onGroupClicked: (Group) -> Unit = {} - - init { - recycler.adapter = adapter - addView(recycler) - - groups.forEach { it.initModels() } - } - - /** - * Returns true if there's at least one filter from [FilterGroup] active. - */ - fun hasActiveFilters(): Boolean { - return (groups[0] as FilterGroup).items.any { it.checked } - } - - /** - * Adapter of the recycler view. - */ - inner class Adapter(items: List) : ExtendedNavigationView.Adapter(items) { - - override fun onItemClicked(item: Item) { - if (item is GroupedItem) { - item.group.onItemClicked(item) - onGroupClicked(item.group) - } - } - } - - /** - * Filters group (unread, downloaded, ...). - */ - inner class FilterGroup : Group { - - private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this) - - private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this) - - private val completed = Item.CheckboxGroup(R.string.completed, this) - - override val items = listOf(downloaded, unread, completed) - - override val header = Item.Header(R.string.action_filter) - - override val footer = Item.Separator() - - override fun initModels() { - downloaded.checked = preferences.filterDownloaded().getOrDefault() - unread.checked = preferences.filterUnread().getOrDefault() - completed.checked = preferences.filterCompleted().getOrDefault() - } - - override fun onItemClicked(item: Item) { - item as Item.CheckboxGroup - item.checked = !item.checked - when (item) { - downloaded -> preferences.filterDownloaded().set(item.checked) - unread -> preferences.filterUnread().set(item.checked) - completed -> preferences.filterCompleted().set(item.checked) - } - - adapter.notifyItemChanged(item) - } - } - - /** - * Sorting group (alphabetically, by last read, ...) and ascending or descending. - */ - inner class SortGroup : Group { - - private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this) - - private val total = Item.MultiSort(R.string.action_sort_total, this) - - private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this) - - private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this) - - private val unread = Item.MultiSort(R.string.action_filter_unread, this) - - private val source = Item.MultiSort(R.string.manga_info_source_label, this) - - override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source) - - override val header = Item.Header(R.string.action_sort) - - override val footer = Item.Separator() - - override fun initModels() { - val sorting = preferences.librarySortingMode().getOrDefault() - val order = if (preferences.librarySortingAscending().getOrDefault()) - SORT_ASC else SORT_DESC - - alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE - lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE - lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE - unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE - total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE - source.state = if (sorting == LibrarySort.SOURCE) order else SORT_NONE - } - - override fun onItemClicked(item: Item) { - item as Item.MultiStateGroup - val prevState = item.state - - item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE } - item.state = when (prevState) { - SORT_NONE -> SORT_ASC - SORT_ASC -> SORT_DESC - SORT_DESC -> SORT_ASC - else -> throw Exception("Unknown state") - } - - preferences.librarySortingMode().set(when (item) { - alphabetically -> LibrarySort.ALPHA - lastRead -> LibrarySort.LAST_READ - lastUpdated -> LibrarySort.LAST_UPDATED - unread -> LibrarySort.UNREAD - total -> LibrarySort.TOTAL - source -> LibrarySort.SOURCE - else -> throw Exception("Unknown sorting") - }) - preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false) - - item.group.items.forEach { adapter.notifyItemChanged(it) } - } - - } - - inner class BadgeGroup : Group { - private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this) - override val header = null - override val footer = null - override val items = listOf(downloadBadge) - override fun initModels() { - downloadBadge.checked = preferences.downloadBadge().getOrDefault() - } - - override fun onItemClicked(item: Item) { - item as Item.CheckboxGroup - item.checked = !item.checked - preferences.downloadBadge().set((item.checked)) - adapter.notifyItemChanged(item) - } - } - - /** - * Display group, to show the library as a list or a grid. - */ - inner class DisplayGroup : Group { - - private val grid = Item.Radio(R.string.action_display_grid, this) - - private val list = Item.Radio(R.string.action_display_list, this) - - override val items = listOf(grid, list) - - override val header = Item.Header(R.string.action_display) - - override val footer = null - - override fun initModels() { - val asList = preferences.libraryAsList().getOrDefault() - grid.checked = !asList - list.checked = asList - } - - override fun onItemClicked(item: Item) { - item as Item.Radio - if (item.checked) return - - item.group.items.forEach { (it as Item.Radio).checked = false } - item.checked = true - - preferences.libraryAsList().set(if (item == list) true else false) - - item.group.items.forEach { adapter.notifyItemChanged(it) } - } - } +package eu.kanade.tachiyomi.ui.library + +import android.content.Context +import android.util.AttributeSet +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.widget.ExtendedNavigationView +import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC +import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC +import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE +import uy.kohesive.injekt.injectLazy + +/** + * The navigation view shown in a drawer with the different options to show the library. + */ +class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) + : ExtendedNavigationView(context, attrs) { + + /** + * Preferences helper. + */ + private val preferences: PreferencesHelper by injectLazy() + + /** + * List of groups shown in the view. + */ + private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup(), BadgeGroup()) + + /** + * Adapter instance. + */ + private val adapter = Adapter(groups.map { it.createItems() }.flatten()) + + /** + * Click listener to notify the parent fragment when an item from a group is clicked. + */ + var onGroupClicked: (Group) -> Unit = {} + + init { + recycler.adapter = adapter + addView(recycler) + + groups.forEach { it.initModels() } + } + + /** + * Returns true if there's at least one filter from [FilterGroup] active. + */ + fun hasActiveFilters(): Boolean { + return (groups[0] as FilterGroup).items.any { it.checked } + } + + /** + * Adapter of the recycler view. + */ + inner class Adapter(items: List) : ExtendedNavigationView.Adapter(items) { + + override fun onItemClicked(item: Item) { + if (item is GroupedItem) { + item.group.onItemClicked(item) + onGroupClicked(item.group) + } + } + } + + /** + * Filters group (unread, downloaded, ...). + */ + inner class FilterGroup : Group { + + private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this) + + private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this) + + private val completed = Item.CheckboxGroup(R.string.completed, this) + + override val items = listOf(downloaded, unread, completed) + + override val header = Item.Header(R.string.action_filter) + + override val footer = Item.Separator() + + override fun initModels() { + downloaded.checked = preferences.filterDownloaded().getOrDefault() + unread.checked = preferences.filterUnread().getOrDefault() + completed.checked = preferences.filterCompleted().getOrDefault() + } + + override fun onItemClicked(item: Item) { + item as Item.CheckboxGroup + item.checked = !item.checked + when (item) { + downloaded -> preferences.filterDownloaded().set(item.checked) + unread -> preferences.filterUnread().set(item.checked) + completed -> preferences.filterCompleted().set(item.checked) + } + + adapter.notifyItemChanged(item) + } + } + + /** + * Sorting group (alphabetically, by last read, ...) and ascending or descending. + */ + inner class SortGroup : Group { + + private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this) + + private val total = Item.MultiSort(R.string.action_sort_total, this) + + private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this) + + private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this) + + private val unread = Item.MultiSort(R.string.action_filter_unread, this) + + private val source = Item.MultiSort(R.string.manga_info_source_label, this) + + override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source) + + override val header = Item.Header(R.string.action_sort) + + override val footer = Item.Separator() + + override fun initModels() { + val sorting = preferences.librarySortingMode().getOrDefault() + val order = if (preferences.librarySortingAscending().getOrDefault()) + SORT_ASC else SORT_DESC + + alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE + lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE + lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE + unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE + total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE + source.state = if (sorting == LibrarySort.SOURCE) order else SORT_NONE + } + + override fun onItemClicked(item: Item) { + item as Item.MultiStateGroup + val prevState = item.state + + item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE } + item.state = when (prevState) { + SORT_NONE -> SORT_ASC + SORT_ASC -> SORT_DESC + SORT_DESC -> SORT_ASC + else -> throw Exception("Unknown state") + } + + preferences.librarySortingMode().set(when (item) { + alphabetically -> LibrarySort.ALPHA + lastRead -> LibrarySort.LAST_READ + lastUpdated -> LibrarySort.LAST_UPDATED + unread -> LibrarySort.UNREAD + total -> LibrarySort.TOTAL + source -> LibrarySort.SOURCE + else -> throw Exception("Unknown sorting") + }) + preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false) + + item.group.items.forEach { adapter.notifyItemChanged(it) } + } + + } + + inner class BadgeGroup : Group { + private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this) + override val header = null + override val footer = null + override val items = listOf(downloadBadge) + override fun initModels() { + downloadBadge.checked = preferences.downloadBadge().getOrDefault() + } + + override fun onItemClicked(item: Item) { + item as Item.CheckboxGroup + item.checked = !item.checked + preferences.downloadBadge().set((item.checked)) + adapter.notifyItemChanged(item) + } + } + + /** + * Display group, to show the library as a list or a grid. + */ + inner class DisplayGroup : Group { + + private val grid = Item.Radio(R.string.action_display_grid, this) + + private val list = Item.Radio(R.string.action_display_list, this) + + override val items = listOf(grid, list) + + override val header = Item.Header(R.string.action_display) + + override val footer = null + + override fun initModels() { + val asList = preferences.libraryAsList().getOrDefault() + grid.checked = !asList + list.checked = asList + } + + override fun onItemClicked(item: Item) { + item as Item.Radio + if (item.checked) return + + item.group.items.forEach { (it as Item.Radio).checked = false } + item.checked = true + + preferences.libraryAsList().set(if (item == list) true else false) + + item.group.items.forEach { adapter.notifyItemChanged(it) } + } + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 17ac0cba5..4e8bf01a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -1,371 +1,371 @@ -package eu.kanade.tachiyomi.ui.library - -import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory -import eu.kanade.tachiyomi.data.download.DownloadManager -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.SourceManager -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.combineLatest -import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.IOException -import java.io.InputStream -import java.util.ArrayList -import java.util.Collections -import java.util.Comparator - -/** - * Class containing library information. - */ -private data class Library(val categories: List, val mangaMap: LibraryMap) - -/** - * Typealias for the library manga, using the category as keys, and list of manga as values. - */ -private typealias LibraryMap = Map> - -/** - * Presenter of [LibraryController]. - */ -class LibraryPresenter( - private val db: DatabaseHelper = Injekt.get(), - private val preferences: PreferencesHelper = Injekt.get(), - private val coverCache: CoverCache = Injekt.get(), - private val sourceManager: SourceManager = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get() -) : BasePresenter() { - - private val context = preferences.context - - /** - * Categories of the library. - */ - var categories: List = emptyList() - private set - - /** - * Relay used to apply the UI filters to the last emission of the library. - */ - private val filterTriggerRelay = BehaviorRelay.create(Unit) - - /** - * Relay used to apply the UI update to the last emission of the library. - */ - private val downloadTriggerRelay = BehaviorRelay.create(Unit) - - /** - * Relay used to apply the selected sorting method to the last emission of the library. - */ - private val sortTriggerRelay = BehaviorRelay.create(Unit) - - /** - * Library subscription. - */ - private var librarySubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - subscribeLibrary() - } - - /** - * Subscribes to library if needed. - */ - fun subscribeLibrary() { - if (librarySubscription.isNullOrUnsubscribed()) { - librarySubscription = getLibraryObservable() - .combineLatest(downloadTriggerRelay.observeOn(Schedulers.io()), - { lib, _ -> lib.apply { setDownloadCount(mangaMap) } }) - .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()), - { lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) }) - .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()), - { lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) }) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache({ view, (categories, mangaMap) -> - view.onNextLibraryUpdate(categories, mangaMap) - }) - } - } - - /** - * Applies library filters to the given map of manga. - * - * @param map the map to filter. - */ - private fun applyFilters(map: LibraryMap): LibraryMap { - val filterDownloaded = preferences.filterDownloaded().getOrDefault() - - val filterUnread = preferences.filterUnread().getOrDefault() - - val filterCompleted = preferences.filterCompleted().getOrDefault() - - val filterFn: (LibraryItem) -> Boolean = f@ { item -> - // Filter when there isn't unread chapters. - if (filterUnread && item.manga.unread == 0) { - return@f false - } - - if (filterCompleted && item.manga.status != SManga.COMPLETED) { - return@f false - } - - // Filter when there are no downloads. - if (filterDownloaded) { - // Local manga are always downloaded - if (item.manga.source == LocalSource.ID) { - return@f true - } - // Don't bother with directory checking if download count has been set. - if (item.downloadCount != -1) { - return@f item.downloadCount > 0 - } - - return@f downloadManager.getDownloadCount(item.manga) > 0 - } - true - } - - return map.mapValues { entry -> entry.value.filter(filterFn) } - } - - /** - * Sets downloaded chapter count to each manga. - * - * @param map the map of manga. - */ - private fun setDownloadCount(map: LibraryMap) { - if (!preferences.downloadBadge().getOrDefault()) { - // Unset download count if the preference is not enabled. - for ((_, itemList) in map) { - for (item in itemList) { - item.downloadCount = -1 - } - } - return - } - - for ((_, itemList) in map) { - for (item in itemList) { - item.downloadCount = downloadManager.getDownloadCount(item.manga) - } - } - } - - /** - * Applies library sorting to the given map of manga. - * - * @param map the map to sort. - */ - private fun applySort(map: LibraryMap): LibraryMap { - val sortingMode = preferences.librarySortingMode().getOrDefault() - - val lastReadManga by lazy { - var counter = 0 - db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ } - } - val totalChapterManga by lazy { - var counter = 0 - db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ } - } - - val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> - when (sortingMode) { - LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true) - LibrarySort.LAST_READ -> { - // Get index of manga, set equal to list if size unknown. - val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size - val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size - manga1LastRead.compareTo(manga2LastRead) - } - LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update) - LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread) - LibrarySort.TOTAL -> { - val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0 - val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0 - manga1TotalChapter.compareTo(mange2TotalChapter) - } - LibrarySort.SOURCE -> { - val source1Name = sourceManager.getOrStub(i1.manga.source).name - val source2Name = sourceManager.getOrStub(i2.manga.source).name - source1Name.compareTo(source2Name) - } - else -> throw Exception("Unknown sorting mode") - } - } - - val comparator = if (preferences.librarySortingAscending().getOrDefault()) - Comparator(sortFn) - else - Collections.reverseOrder(sortFn) - - return map.mapValues { entry -> entry.value.sortedWith(comparator) } - } - - /** - * Get the categories and all its manga from the database. - * - * @return an observable of the categories and its manga. - */ - private fun getLibraryObservable(): Observable { - return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), - { dbCategories, libraryManga -> - val categories = if (libraryManga.containsKey(0)) - arrayListOf(Category.createDefault()) + dbCategories - else - dbCategories - - this.categories = categories - Library(categories, libraryManga) - }) - } - - /** - * Get the categories from the database. - * - * @return an observable of the categories. - */ - private fun getCategoriesObservable(): Observable> { - return db.getCategories().asRxObservable() - } - - /** - * Get the manga grouped by categories. - * - * @return an observable containing a map with the category id as key and a list of manga as the - * value. - */ - private fun getLibraryMangasObservable(): Observable { - val libraryAsList = preferences.libraryAsList() - return db.getLibraryMangas().asRxObservable() - .map { list -> - list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category } - } - } - - /** - * Requests the library to be filtered. - */ - fun requestFilterUpdate() { - filterTriggerRelay.call(Unit) - } - - /** - * Requests the library to have download badges added. - */ - fun requestDownloadBadgesUpdate() { - downloadTriggerRelay.call(Unit) - } - - /** - * Requests the library to be sorted. - */ - fun requestSortUpdate() { - sortTriggerRelay.call(Unit) - } - - /** - * Called when a manga is opened. - */ - fun onOpenManga() { - // Avoid further db updates for the library when it's not needed - librarySubscription?.let { remove(it) } - } - - /** - * Returns the common categories for the given list of manga. - * - * @param mangas the list of manga. - */ - fun getCommonCategories(mangas: List): Collection { - if (mangas.isEmpty()) return emptyList() - return mangas.toSet() - .map { db.getCategoriesForManga(it).executeAsBlocking() } - .reduce { set1: Iterable, set2 -> set1.intersect(set2) } - } - - /** - * Remove the selected manga from the library. - * - * @param mangas the list of manga to delete. - * @param deleteChapters whether to also delete downloaded chapters. - */ - fun removeMangaFromLibrary(mangas: List, deleteChapters: Boolean) { - // Create a set of the list - val mangaToDelete = mangas.distinctBy { it.id } - mangaToDelete.forEach { it.favorite = false } - - Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() } - .onErrorResumeNext { Observable.empty() } - .subscribeOn(Schedulers.io()) - .subscribe() - - Observable.fromCallable { - mangaToDelete.forEach { manga -> - coverCache.deleteFromCache(manga.thumbnail_url) - if (deleteChapters) { - val source = sourceManager.get(manga.source) as? HttpSource - if (source != null) { - downloadManager.deleteManga(manga, source) - } - } - } - } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Move the given list of manga to categories. - * - * @param categories the selected categories. - * @param mangas the list of manga to move. - */ - fun moveMangasToCategories(categories: List, mangas: List) { - val mc = ArrayList() - - for (manga in mangas) { - for (cat in categories) { - mc.add(MangaCategory.create(manga, cat)) - } - } - - db.setMangaCategories(mc, mangas) - } - - /** - * Update cover with local file. - * - * @param inputStream the new cover. - * @param manga the manga edited. - * @return true if the cover is updated, false otherwise - */ - @Throws(IOException::class) - fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean { - if (manga.source == LocalSource.ID) { - LocalSource.updateCover(context, manga, inputStream) - return true - } - - if (manga.thumbnail_url != null && manga.favorite) { - coverCache.copyToCache(manga.thumbnail_url!!, inputStream) - return true - } - return false - } - -} +package eu.kanade.tachiyomi.ui.library + +import android.os.Bundle +import com.jakewharton.rxrelay.BehaviorRelay +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.download.DownloadManager +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.SourceManager +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.combineLatest +import eu.kanade.tachiyomi.util.isNullOrUnsubscribed +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.IOException +import java.io.InputStream +import java.util.ArrayList +import java.util.Collections +import java.util.Comparator + +/** + * Class containing library information. + */ +private data class Library(val categories: List, val mangaMap: LibraryMap) + +/** + * Typealias for the library manga, using the category as keys, and list of manga as values. + */ +private typealias LibraryMap = Map> + +/** + * Presenter of [LibraryController]. + */ +class LibraryPresenter( + private val db: DatabaseHelper = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get() +) : BasePresenter() { + + private val context = preferences.context + + /** + * Categories of the library. + */ + var categories: List = emptyList() + private set + + /** + * Relay used to apply the UI filters to the last emission of the library. + */ + private val filterTriggerRelay = BehaviorRelay.create(Unit) + + /** + * Relay used to apply the UI update to the last emission of the library. + */ + private val downloadTriggerRelay = BehaviorRelay.create(Unit) + + /** + * Relay used to apply the selected sorting method to the last emission of the library. + */ + private val sortTriggerRelay = BehaviorRelay.create(Unit) + + /** + * Library subscription. + */ + private var librarySubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + subscribeLibrary() + } + + /** + * Subscribes to library if needed. + */ + fun subscribeLibrary() { + if (librarySubscription.isNullOrUnsubscribed()) { + librarySubscription = getLibraryObservable() + .combineLatest(downloadTriggerRelay.observeOn(Schedulers.io()), + { lib, _ -> lib.apply { setDownloadCount(mangaMap) } }) + .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()), + { lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) }) + .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()), + { lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) }) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache({ view, (categories, mangaMap) -> + view.onNextLibraryUpdate(categories, mangaMap) + }) + } + } + + /** + * Applies library filters to the given map of manga. + * + * @param map the map to filter. + */ + private fun applyFilters(map: LibraryMap): LibraryMap { + val filterDownloaded = preferences.filterDownloaded().getOrDefault() + + val filterUnread = preferences.filterUnread().getOrDefault() + + val filterCompleted = preferences.filterCompleted().getOrDefault() + + val filterFn: (LibraryItem) -> Boolean = f@ { item -> + // Filter when there isn't unread chapters. + if (filterUnread && item.manga.unread == 0) { + return@f false + } + + if (filterCompleted && item.manga.status != SManga.COMPLETED) { + return@f false + } + + // Filter when there are no downloads. + if (filterDownloaded) { + // Local manga are always downloaded + if (item.manga.source == LocalSource.ID) { + return@f true + } + // Don't bother with directory checking if download count has been set. + if (item.downloadCount != -1) { + return@f item.downloadCount > 0 + } + + return@f downloadManager.getDownloadCount(item.manga) > 0 + } + true + } + + return map.mapValues { entry -> entry.value.filter(filterFn) } + } + + /** + * Sets downloaded chapter count to each manga. + * + * @param map the map of manga. + */ + private fun setDownloadCount(map: LibraryMap) { + if (!preferences.downloadBadge().getOrDefault()) { + // Unset download count if the preference is not enabled. + for ((_, itemList) in map) { + for (item in itemList) { + item.downloadCount = -1 + } + } + return + } + + for ((_, itemList) in map) { + for (item in itemList) { + item.downloadCount = downloadManager.getDownloadCount(item.manga) + } + } + } + + /** + * Applies library sorting to the given map of manga. + * + * @param map the map to sort. + */ + private fun applySort(map: LibraryMap): LibraryMap { + val sortingMode = preferences.librarySortingMode().getOrDefault() + + val lastReadManga by lazy { + var counter = 0 + db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ } + } + val totalChapterManga by lazy { + var counter = 0 + db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ } + } + + val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> + when (sortingMode) { + LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true) + LibrarySort.LAST_READ -> { + // Get index of manga, set equal to list if size unknown. + val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size + val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size + manga1LastRead.compareTo(manga2LastRead) + } + LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update) + LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread) + LibrarySort.TOTAL -> { + val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0 + val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0 + manga1TotalChapter.compareTo(mange2TotalChapter) + } + LibrarySort.SOURCE -> { + val source1Name = sourceManager.getOrStub(i1.manga.source).name + val source2Name = sourceManager.getOrStub(i2.manga.source).name + source1Name.compareTo(source2Name) + } + else -> throw Exception("Unknown sorting mode") + } + } + + val comparator = if (preferences.librarySortingAscending().getOrDefault()) + Comparator(sortFn) + else + Collections.reverseOrder(sortFn) + + return map.mapValues { entry -> entry.value.sortedWith(comparator) } + } + + /** + * Get the categories and all its manga from the database. + * + * @return an observable of the categories and its manga. + */ + private fun getLibraryObservable(): Observable { + return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), + { dbCategories, libraryManga -> + val categories = if (libraryManga.containsKey(0)) + arrayListOf(Category.createDefault()) + dbCategories + else + dbCategories + + this.categories = categories + Library(categories, libraryManga) + }) + } + + /** + * Get the categories from the database. + * + * @return an observable of the categories. + */ + private fun getCategoriesObservable(): Observable> { + return db.getCategories().asRxObservable() + } + + /** + * Get the manga grouped by categories. + * + * @return an observable containing a map with the category id as key and a list of manga as the + * value. + */ + private fun getLibraryMangasObservable(): Observable { + val libraryAsList = preferences.libraryAsList() + return db.getLibraryMangas().asRxObservable() + .map { list -> + list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category } + } + } + + /** + * Requests the library to be filtered. + */ + fun requestFilterUpdate() { + filterTriggerRelay.call(Unit) + } + + /** + * Requests the library to have download badges added. + */ + fun requestDownloadBadgesUpdate() { + downloadTriggerRelay.call(Unit) + } + + /** + * Requests the library to be sorted. + */ + fun requestSortUpdate() { + sortTriggerRelay.call(Unit) + } + + /** + * Called when a manga is opened. + */ + fun onOpenManga() { + // Avoid further db updates for the library when it's not needed + librarySubscription?.let { remove(it) } + } + + /** + * Returns the common categories for the given list of manga. + * + * @param mangas the list of manga. + */ + fun getCommonCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + return mangas.toSet() + .map { db.getCategoriesForManga(it).executeAsBlocking() } + .reduce { set1: Iterable, set2 -> set1.intersect(set2) } + } + + /** + * Remove the selected manga from the library. + * + * @param mangas the list of manga to delete. + * @param deleteChapters whether to also delete downloaded chapters. + */ + fun removeMangaFromLibrary(mangas: List, deleteChapters: Boolean) { + // Create a set of the list + val mangaToDelete = mangas.distinctBy { it.id } + mangaToDelete.forEach { it.favorite = false } + + Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() } + .onErrorResumeNext { Observable.empty() } + .subscribeOn(Schedulers.io()) + .subscribe() + + Observable.fromCallable { + mangaToDelete.forEach { manga -> + coverCache.deleteFromCache(manga.thumbnail_url) + if (deleteChapters) { + val source = sourceManager.get(manga.source) as? HttpSource + if (source != null) { + downloadManager.deleteManga(manga, source) + } + } + } + } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Move the given list of manga to categories. + * + * @param categories the selected categories. + * @param mangas the list of manga to move. + */ + fun moveMangasToCategories(categories: List, mangas: List) { + val mc = ArrayList() + + for (manga in mangas) { + for (cat in categories) { + mc.add(MangaCategory.create(manga, cat)) + } + } + + db.setMangaCategories(mc, mangas) + } + + /** + * Update cover with local file. + * + * @param inputStream the new cover. + * @param manga the manga edited. + * @return true if the cover is updated, false otherwise + */ + @Throws(IOException::class) + fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean { + if (manga.source == LocalSource.ID) { + LocalSource.updateCover(context, manga, inputStream) + return true + } + + if (manga.thumbnail_url != null && manga.favorite) { + coverCache.copyToCache(manga.thumbnail_url!!, inputStream) + return true + } + return false + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt index 57c9f28b1..a67b793fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt @@ -1,11 +1,11 @@ -package eu.kanade.tachiyomi.ui.library - -object LibrarySort { - - const val ALPHA = 0 - const val LAST_READ = 1 - const val LAST_UPDATED = 2 - const val UNREAD = 3 - const val TOTAL = 4 - const val SOURCE = 5 +package eu.kanade.tachiyomi.ui.library + +object LibrarySort { + + const val ALPHA = 0 + const val LAST_READ = 1 + const val LAST_UPDATED = 2 + const val UNREAD = 3 + const val TOTAL = 4 + const val SOURCE = 5 } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogController.kt index 4d24b7e20..60627bee5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogController.kt @@ -1,32 +1,32 @@ -package eu.kanade.tachiyomi.ui.main - -import android.app.Dialog -import android.content.Context -import android.os.Bundle -import android.util.AttributeSet -import com.afollestad.materialdialogs.MaterialDialog -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView - -class ChangelogDialogController : DialogController() { - - override fun onCreateDialog(savedState: Bundle?): Dialog { - val activity = activity!! - val view = WhatsNewRecyclerView(activity) - return MaterialDialog.Builder(activity) - .title(if (BuildConfig.DEBUG) "Notices" else "Changelog") - .customView(view, false) - .positiveText(android.R.string.yes) - .build() - } - - class WhatsNewRecyclerView(context: Context) : ChangeLogRecyclerView(context) { - override fun initAttrs(attrs: AttributeSet?, defStyle: Int) { - mRowLayoutId = R.layout.changelog_row_layout - mRowHeaderLayoutId = R.layout.changelog_header_layout - mChangeLogFileResourceId = if (BuildConfig.DEBUG) R.raw.changelog_debug else R.raw.changelog_release - } - } +package eu.kanade.tachiyomi.ui.main + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView + +class ChangelogDialogController : DialogController() { + + override fun onCreateDialog(savedState: Bundle?): Dialog { + val activity = activity!! + val view = WhatsNewRecyclerView(activity) + return MaterialDialog.Builder(activity) + .title(if (BuildConfig.DEBUG) "Notices" else "Changelog") + .customView(view, false) + .positiveText(android.R.string.yes) + .build() + } + + class WhatsNewRecyclerView(context: Context) : ChangeLogRecyclerView(context) { + override fun initAttrs(attrs: AttributeSet?, defStyle: Int) { + mRowLayoutId = R.layout.changelog_row_layout + mRowHeaderLayoutId = R.layout.changelog_header_layout + mChangeLogFileResourceId = if (BuildConfig.DEBUG) R.raw.changelog_debug else R.raw.changelog_release + } + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index c8cfd2676..397d2cb03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -1,282 +1,282 @@ -package eu.kanade.tachiyomi.ui.main - -import android.animation.ObjectAnimator -import android.app.SearchManager -import android.content.Intent -import android.graphics.Color -import android.os.Bundle -import androidx.core.view.GravityCompat -import androidx.drawerlayout.widget.DrawerLayout -import androidx.appcompat.graphics.drawable.DrawerArrowDrawable -import android.view.ViewGroup -import com.bluelinelabs.conductor.* -import eu.kanade.tachiyomi.Migrations -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.ui.base.activity.BaseActivity -import eu.kanade.tachiyomi.ui.base.controller.* -import eu.kanade.tachiyomi.ui.catalogue.CatalogueController -import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController -import eu.kanade.tachiyomi.ui.download.DownloadController -import eu.kanade.tachiyomi.ui.extension.ExtensionController -import eu.kanade.tachiyomi.ui.library.LibraryController -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController -import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController -import eu.kanade.tachiyomi.ui.setting.SettingsMainController -import eu.kanade.tachiyomi.util.openInBrowser -import kotlinx.android.synthetic.main.main_activity.* -import uy.kohesive.injekt.injectLazy - - -class MainActivity : BaseActivity() { - - private lateinit var router: Router - - val preferences: PreferencesHelper by injectLazy() - - private var drawerArrow: DrawerArrowDrawable? = null - - private var secondaryDrawer: ViewGroup? = null - - private val startScreenId by lazy { - when (preferences.startScreen()) { - 2 -> R.id.nav_drawer_recently_read - 3 -> R.id.nav_drawer_recent_updates - else -> R.id.nav_drawer_library - } - } - - lateinit var tabAnimator: TabsAnimator - - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(when (preferences.theme()) { - 2 -> R.style.Theme_Tachiyomi_Dark - 3 -> R.style.Theme_Tachiyomi_Amoled - 4 -> R.style.Theme_Tachiyomi_DarkBlue - else -> R.style.Theme_Tachiyomi - }) - super.onCreate(savedInstanceState) - - // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 - if (!isTaskRoot) { - finish() - return - } - - setContentView(R.layout.main_activity) - - setSupportActionBar(toolbar) - - drawerArrow = DrawerArrowDrawable(this) - drawerArrow?.color = Color.WHITE - toolbar.navigationIcon = drawerArrow - - tabAnimator = TabsAnimator(tabs) - - // Set behavior of Navigation drawer - nav_view.setNavigationItemSelectedListener { item -> - val id = item.itemId - - val currentRoot = router.backstack.firstOrNull() - if (currentRoot?.tag()?.toIntOrNull() != id) { - when (id) { - R.id.nav_drawer_library -> setRoot(LibraryController(), id) - R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) - R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id) - R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id) - R.id.nav_drawer_extensions -> setRoot(ExtensionController(), id) - R.id.nav_drawer_downloads -> { - router.pushController(DownloadController().withFadeTransaction()) - } - R.id.nav_drawer_settings -> { - router.pushController(SettingsMainController().withFadeTransaction()) - } - R.id.nav_drawer_help -> { - openInBrowser(URL_HELP) - } - } - } - drawer.closeDrawer(GravityCompat.START) - true - } - - val container: ViewGroup = findViewById(R.id.controller_container) - - router = Conductor.attachRouter(this, container, savedInstanceState) - if (!router.hasRootController()) { - // Set start screen - if (!handleIntentAction(intent)) { - setSelectedDrawerItem(startScreenId) - } - } - - toolbar.setNavigationOnClickListener { - if (router.backstackSize == 1) { - drawer.openDrawer(GravityCompat.START) - } else { - onBackPressed() - } - } - - router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener { - override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean, - container: ViewGroup, handler: ControllerChangeHandler) { - - syncActivityViewWithController(to, from) - } - - override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean, - container: ViewGroup, handler: ControllerChangeHandler) { - - } - - }) - - syncActivityViewWithController(router.backstack.lastOrNull()?.controller()) - - if (savedInstanceState == null) { - // Show changelog if needed - if (Migrations.upgrade(preferences)) { - ChangelogDialogController().showDialog(router) - } - } - } - - override fun onNewIntent(intent: Intent) { - if (!handleIntentAction(intent)) { - super.onNewIntent(intent) - } - } - - private fun handleIntentAction(intent: Intent): Boolean { - when (intent.action) { - SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library) - SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates) - SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read) - SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues) - SHORTCUT_MANGA -> { - val extras = intent.extras ?: return false - router.setRoot(RouterTransaction.with(MangaController(extras))) - } - SHORTCUT_DOWNLOADS -> { - if (router.backstack.none { it.controller() is DownloadController }) { - setSelectedDrawerItem(R.id.nav_drawer_downloads) - } - } - Intent.ACTION_SEARCH, "com.google.android.gms.actions.SEARCH_ACTION" -> { - //If the intent match the "standard" Android search intent - // or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant) - - //Get the search query provided in extras, and if not null, perform a global search with it. - val query = intent.getStringExtra(SearchManager.QUERY) - if (query != null && !query.isEmpty()) { - if (router.backstackSize > 1) { - router.popToRoot() - } - router.pushController(CatalogueSearchController(query).withFadeTransaction()) - } - } - INTENT_SEARCH -> { - val query = intent.getStringExtra(INTENT_SEARCH_QUERY) - val filter = intent.getStringExtra(INTENT_SEARCH_FILTER) - if (query != null && !query.isEmpty()) { - if (router.backstackSize > 1) { - router.popToRoot() - } - router.pushController(CatalogueSearchController(query, filter).withFadeTransaction()) - } - } - else -> return false - } - return true - } - - override fun onDestroy() { - super.onDestroy() - nav_view?.setNavigationItemSelectedListener(null) - toolbar?.setNavigationOnClickListener(null) - } - - override fun onBackPressed() { - val backstackSize = router.backstackSize - if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) { - drawer.closeDrawers() - } else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) { - setSelectedDrawerItem(startScreenId) - } else if (backstackSize == 1 || !router.handleBack()) { - super.onBackPressed() - } - } - - private fun setSelectedDrawerItem(itemId: Int) { - if (!isFinishing) { - nav_view.setCheckedItem(itemId) - nav_view.menu.performIdentifierAction(itemId, 0) - } - } - - private fun setRoot(controller: Controller, id: Int) { - router.setRoot(controller.withFadeTransaction().tag(id.toString())) - } - - private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) { - if (from is DialogController || to is DialogController) { - return - } - - val showHamburger = router.backstackSize == 1 - if (showHamburger) { - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) - } else { - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - } - - ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start() - - if (from is TabbedController) { - from.cleanupTabs(tabs) - } - if (to is TabbedController) { - tabAnimator.expand() - to.configureTabs(tabs) - } else { - tabAnimator.collapse() - tabs.setupWithViewPager(null) - } - - if (from is SecondaryDrawerController) { - if (secondaryDrawer != null) { - from.cleanupSecondaryDrawer(drawer) - drawer.removeView(secondaryDrawer) - secondaryDrawer = null - } - } - if (to is SecondaryDrawerController) { - secondaryDrawer = to.createSecondaryDrawer(drawer)?.also { drawer.addView(it) } - } - - if (to is NoToolbarElevationController) { - appbar.disableElevation() - } else { - appbar.enableElevation() - } - } - - companion object { - // Shortcut actions - const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" - const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" - const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ" - const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES" - const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS" - const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA" - - const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH" - const val INTENT_SEARCH_QUERY = "query" - const val INTENT_SEARCH_FILTER = "filter" - - private const val URL_HELP = "https://tachiyomi.org/help/" - } - -} +package eu.kanade.tachiyomi.ui.main + +import android.animation.ObjectAnimator +import android.app.SearchManager +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout +import androidx.appcompat.graphics.drawable.DrawerArrowDrawable +import android.view.ViewGroup +import com.bluelinelabs.conductor.* +import eu.kanade.tachiyomi.Migrations +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.base.activity.BaseActivity +import eu.kanade.tachiyomi.ui.base.controller.* +import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController +import eu.kanade.tachiyomi.ui.download.DownloadController +import eu.kanade.tachiyomi.ui.extension.ExtensionController +import eu.kanade.tachiyomi.ui.library.LibraryController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController +import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController +import eu.kanade.tachiyomi.ui.setting.SettingsMainController +import eu.kanade.tachiyomi.util.openInBrowser +import kotlinx.android.synthetic.main.main_activity.* +import uy.kohesive.injekt.injectLazy + + +class MainActivity : BaseActivity() { + + private lateinit var router: Router + + val preferences: PreferencesHelper by injectLazy() + + private var drawerArrow: DrawerArrowDrawable? = null + + private var secondaryDrawer: ViewGroup? = null + + private val startScreenId by lazy { + when (preferences.startScreen()) { + 2 -> R.id.nav_drawer_recently_read + 3 -> R.id.nav_drawer_recent_updates + else -> R.id.nav_drawer_library + } + } + + lateinit var tabAnimator: TabsAnimator + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(when (preferences.theme()) { + 2 -> R.style.Theme_Tachiyomi_Dark + 3 -> R.style.Theme_Tachiyomi_Amoled + 4 -> R.style.Theme_Tachiyomi_DarkBlue + else -> R.style.Theme_Tachiyomi + }) + super.onCreate(savedInstanceState) + + // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 + if (!isTaskRoot) { + finish() + return + } + + setContentView(R.layout.main_activity) + + setSupportActionBar(toolbar) + + drawerArrow = DrawerArrowDrawable(this) + drawerArrow?.color = Color.WHITE + toolbar.navigationIcon = drawerArrow + + tabAnimator = TabsAnimator(tabs) + + // Set behavior of Navigation drawer + nav_view.setNavigationItemSelectedListener { item -> + val id = item.itemId + + val currentRoot = router.backstack.firstOrNull() + if (currentRoot?.tag()?.toIntOrNull() != id) { + when (id) { + R.id.nav_drawer_library -> setRoot(LibraryController(), id) + R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) + R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id) + R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id) + R.id.nav_drawer_extensions -> setRoot(ExtensionController(), id) + R.id.nav_drawer_downloads -> { + router.pushController(DownloadController().withFadeTransaction()) + } + R.id.nav_drawer_settings -> { + router.pushController(SettingsMainController().withFadeTransaction()) + } + R.id.nav_drawer_help -> { + openInBrowser(URL_HELP) + } + } + } + drawer.closeDrawer(GravityCompat.START) + true + } + + val container: ViewGroup = findViewById(R.id.controller_container) + + router = Conductor.attachRouter(this, container, savedInstanceState) + if (!router.hasRootController()) { + // Set start screen + if (!handleIntentAction(intent)) { + setSelectedDrawerItem(startScreenId) + } + } + + toolbar.setNavigationOnClickListener { + if (router.backstackSize == 1) { + drawer.openDrawer(GravityCompat.START) + } else { + onBackPressed() + } + } + + router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener { + override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean, + container: ViewGroup, handler: ControllerChangeHandler) { + + syncActivityViewWithController(to, from) + } + + override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean, + container: ViewGroup, handler: ControllerChangeHandler) { + + } + + }) + + syncActivityViewWithController(router.backstack.lastOrNull()?.controller()) + + if (savedInstanceState == null) { + // Show changelog if needed + if (Migrations.upgrade(preferences)) { + ChangelogDialogController().showDialog(router) + } + } + } + + override fun onNewIntent(intent: Intent) { + if (!handleIntentAction(intent)) { + super.onNewIntent(intent) + } + } + + private fun handleIntentAction(intent: Intent): Boolean { + when (intent.action) { + SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library) + SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates) + SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read) + SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues) + SHORTCUT_MANGA -> { + val extras = intent.extras ?: return false + router.setRoot(RouterTransaction.with(MangaController(extras))) + } + SHORTCUT_DOWNLOADS -> { + if (router.backstack.none { it.controller() is DownloadController }) { + setSelectedDrawerItem(R.id.nav_drawer_downloads) + } + } + Intent.ACTION_SEARCH, "com.google.android.gms.actions.SEARCH_ACTION" -> { + //If the intent match the "standard" Android search intent + // or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant) + + //Get the search query provided in extras, and if not null, perform a global search with it. + val query = intent.getStringExtra(SearchManager.QUERY) + if (query != null && !query.isEmpty()) { + if (router.backstackSize > 1) { + router.popToRoot() + } + router.pushController(CatalogueSearchController(query).withFadeTransaction()) + } + } + INTENT_SEARCH -> { + val query = intent.getStringExtra(INTENT_SEARCH_QUERY) + val filter = intent.getStringExtra(INTENT_SEARCH_FILTER) + if (query != null && !query.isEmpty()) { + if (router.backstackSize > 1) { + router.popToRoot() + } + router.pushController(CatalogueSearchController(query, filter).withFadeTransaction()) + } + } + else -> return false + } + return true + } + + override fun onDestroy() { + super.onDestroy() + nav_view?.setNavigationItemSelectedListener(null) + toolbar?.setNavigationOnClickListener(null) + } + + override fun onBackPressed() { + val backstackSize = router.backstackSize + if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) { + drawer.closeDrawers() + } else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) { + setSelectedDrawerItem(startScreenId) + } else if (backstackSize == 1 || !router.handleBack()) { + super.onBackPressed() + } + } + + private fun setSelectedDrawerItem(itemId: Int) { + if (!isFinishing) { + nav_view.setCheckedItem(itemId) + nav_view.menu.performIdentifierAction(itemId, 0) + } + } + + private fun setRoot(controller: Controller, id: Int) { + router.setRoot(controller.withFadeTransaction().tag(id.toString())) + } + + private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) { + if (from is DialogController || to is DialogController) { + return + } + + val showHamburger = router.backstackSize == 1 + if (showHamburger) { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + } else { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + } + + ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start() + + if (from is TabbedController) { + from.cleanupTabs(tabs) + } + if (to is TabbedController) { + tabAnimator.expand() + to.configureTabs(tabs) + } else { + tabAnimator.collapse() + tabs.setupWithViewPager(null) + } + + if (from is SecondaryDrawerController) { + if (secondaryDrawer != null) { + from.cleanupSecondaryDrawer(drawer) + drawer.removeView(secondaryDrawer) + secondaryDrawer = null + } + } + if (to is SecondaryDrawerController) { + secondaryDrawer = to.createSecondaryDrawer(drawer)?.also { drawer.addView(it) } + } + + if (to is NoToolbarElevationController) { + appbar.disableElevation() + } else { + appbar.enableElevation() + } + } + + companion object { + // Shortcut actions + const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" + const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" + const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ" + const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES" + const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS" + const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA" + + const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH" + const val INTENT_SEARCH_QUERY = "query" + const val INTENT_SEARCH_FILTER = "filter" + + private const val URL_HELP = "https://tachiyomi.org/help/" + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 2990d80ac..69ab7003e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -1,193 +1,193 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.os.Bundle -import com.google.android.material.tabs.TabLayout -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView -import com.bluelinelabs.conductor.ControllerChangeHandler -import com.bluelinelabs.conductor.ControllerChangeType -import com.bluelinelabs.conductor.Router -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.support.RouterPagerAdapter -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.controller.RxController -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.info.MangaInfoController -import eu.kanade.tachiyomi.ui.manga.track.TrackController -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.main_activity.* -import kotlinx.android.synthetic.main.manga_controller.* -import rx.Subscription -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.Date - -class MangaController : RxController, TabbedController { - - constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply { - putLong(MANGA_EXTRA, manga?.id ?: 0) - putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) - }) { - this.manga = manga - if (manga != null) { - source = Injekt.get().getOrStub(manga.source) - } - } - - constructor(mangaId: Long) : this( - Injekt.get().getManga(mangaId).executeAsBlocking()) - - @Suppress("unused") - constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) - - var manga: Manga? = null - private set - - var source: Source? = null - private set - - private var adapter: MangaDetailAdapter? = null - - val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) - - val lastUpdateRelay: BehaviorRelay = BehaviorRelay.create() - - val chapterCountRelay: BehaviorRelay = BehaviorRelay.create() - - val mangaFavoriteRelay: PublishRelay = PublishRelay.create() - - private val trackingIconRelay: BehaviorRelay = BehaviorRelay.create() - - private var trackingIconSubscription: Subscription? = null - - override fun getTitle(): String? { - return manga?.title - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.manga_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - if (manga == null || source == null) return - - requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) - - adapter = MangaDetailAdapter() - manga_pager.offscreenPageLimit = 3 - manga_pager.adapter = adapter - - if (!fromCatalogue) - manga_pager.currentItem = CHAPTERS_CONTROLLER - } - - override fun onDestroyView(view: View) { - super.onDestroyView(view) - adapter = null - } - - override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeStarted(handler, type) - if (type.isEnter) { - activity?.tabs?.setupWithViewPager(manga_pager) - trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } - } - } - - override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeEnded(handler, type) - if (manga == null || source == null) { - activity?.toast(R.string.manga_not_in_db) - router.popController(this) - } - } - - override fun configureTabs(tabs: TabLayout) { - with(tabs) { - tabGravity = TabLayout.GRAVITY_FILL - tabMode = TabLayout.MODE_FIXED - } - } - - override fun cleanupTabs(tabs: TabLayout) { - trackingIconSubscription?.unsubscribe() - setTrackingIconInternal(false) - } - - fun setTrackingIcon(visible: Boolean) { - trackingIconRelay.call(visible) - } - - private fun setTrackingIconInternal(visible: Boolean) { - val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return - val drawable = if (visible) - VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null) - else null - - val view = tabField.get(tab) as LinearLayout - val textView = view.getChildAt(1) as TextView - textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) - textView.compoundDrawablePadding = if (visible) 4 else 0 - } - - private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { - - private val tabCount = if (Injekt.get().hasLoggedServices()) 3 else 2 - - private val tabTitles = listOf( - R.string.manga_detail_tab, - R.string.manga_chapters_tab, - R.string.manga_tracking_tab) - .map { resources!!.getString(it) } - - override fun getCount(): Int { - return tabCount - } - - override fun configureRouter(router: Router, position: Int) { - if (!router.hasRootController()) { - val controller = when (position) { - INFO_CONTROLLER -> MangaInfoController() - CHAPTERS_CONTROLLER -> ChaptersController() - TRACK_CONTROLLER -> TrackController() - else -> error("Wrong position $position") - } - router.setRoot(RouterTransaction.with(controller)) - } - } - - override fun getPageTitle(position: Int): CharSequence { - return tabTitles[position] - } - - } - - companion object { - const val FROM_CATALOGUE_EXTRA = "from_catalogue" - const val MANGA_EXTRA = "manga" - - const val INFO_CONTROLLER = 0 - const val CHAPTERS_CONTROLLER = 1 - const val TRACK_CONTROLLER = 2 - - private val tabField = TabLayout.Tab::class.java.getDeclaredField("view") - .apply { isAccessible = true } - } - -} +package eu.kanade.tachiyomi.ui.manga + +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.os.Bundle +import com.google.android.material.tabs.TabLayout +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import com.bluelinelabs.conductor.Router +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.support.RouterPagerAdapter +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.base.controller.RxController +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.info.MangaInfoController +import eu.kanade.tachiyomi.ui.manga.track.TrackController +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.main_activity.* +import kotlinx.android.synthetic.main.manga_controller.* +import rx.Subscription +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.Date + +class MangaController : RxController, TabbedController { + + constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply { + putLong(MANGA_EXTRA, manga?.id ?: 0) + putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) + }) { + this.manga = manga + if (manga != null) { + source = Injekt.get().getOrStub(manga.source) + } + } + + constructor(mangaId: Long) : this( + Injekt.get().getManga(mangaId).executeAsBlocking()) + + @Suppress("unused") + constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) + + var manga: Manga? = null + private set + + var source: Source? = null + private set + + private var adapter: MangaDetailAdapter? = null + + val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) + + val lastUpdateRelay: BehaviorRelay = BehaviorRelay.create() + + val chapterCountRelay: BehaviorRelay = BehaviorRelay.create() + + val mangaFavoriteRelay: PublishRelay = PublishRelay.create() + + private val trackingIconRelay: BehaviorRelay = BehaviorRelay.create() + + private var trackingIconSubscription: Subscription? = null + + override fun getTitle(): String? { + return manga?.title + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.manga_controller, container, false) + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + if (manga == null || source == null) return + + requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) + + adapter = MangaDetailAdapter() + manga_pager.offscreenPageLimit = 3 + manga_pager.adapter = adapter + + if (!fromCatalogue) + manga_pager.currentItem = CHAPTERS_CONTROLLER + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeStarted(handler, type) + if (type.isEnter) { + activity?.tabs?.setupWithViewPager(manga_pager) + trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } + } + } + + override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeEnded(handler, type) + if (manga == null || source == null) { + activity?.toast(R.string.manga_not_in_db) + router.popController(this) + } + } + + override fun configureTabs(tabs: TabLayout) { + with(tabs) { + tabGravity = TabLayout.GRAVITY_FILL + tabMode = TabLayout.MODE_FIXED + } + } + + override fun cleanupTabs(tabs: TabLayout) { + trackingIconSubscription?.unsubscribe() + setTrackingIconInternal(false) + } + + fun setTrackingIcon(visible: Boolean) { + trackingIconRelay.call(visible) + } + + private fun setTrackingIconInternal(visible: Boolean) { + val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return + val drawable = if (visible) + VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null) + else null + + val view = tabField.get(tab) as LinearLayout + val textView = view.getChildAt(1) as TextView + textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) + textView.compoundDrawablePadding = if (visible) 4 else 0 + } + + private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { + + private val tabCount = if (Injekt.get().hasLoggedServices()) 3 else 2 + + private val tabTitles = listOf( + R.string.manga_detail_tab, + R.string.manga_chapters_tab, + R.string.manga_tracking_tab) + .map { resources!!.getString(it) } + + override fun getCount(): Int { + return tabCount + } + + override fun configureRouter(router: Router, position: Int) { + if (!router.hasRootController()) { + val controller = when (position) { + INFO_CONTROLLER -> MangaInfoController() + CHAPTERS_CONTROLLER -> ChaptersController() + TRACK_CONTROLLER -> TrackController() + else -> error("Wrong position $position") + } + router.setRoot(RouterTransaction.with(controller)) + } + } + + override fun getPageTitle(position: Int): CharSequence { + return tabTitles[position] + } + + } + + companion object { + const val FROM_CATALOGUE_EXTRA = "from_catalogue" + const val MANGA_EXTRA = "manga" + + const val INFO_CONTROLLER = 0 + const val CHAPTERS_CONTROLLER = 1 + const val TRACK_CONTROLLER = 2 + + private val tabField = TabLayout.Tab::class.java.getDeclaredField("view") + .apply { isAccessible = true } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt index d02d95102..0a232a4af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt @@ -1,122 +1,122 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.view.View -import android.widget.PopupMenu -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.util.getResourceColor -import eu.kanade.tachiyomi.util.gone -import eu.kanade.tachiyomi.util.setVectorCompat -import kotlinx.android.synthetic.main.chapters_item.* -import java.util.* - -class ChapterHolder( - private val view: View, - private val adapter: ChaptersAdapter -) : BaseFlexibleViewHolder(view, adapter) { - - init { - // We need to post a Runnable to show the popup to make sure that the PopupMenu is - // correctly positioned. The reason being that the view may change position before the - // PopupMenu is shown. - chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } - } - - fun bind(item: ChapterItem, manga: Manga) { - val chapter = item.chapter - - chapter_title.text = when (manga.displayMode) { - Manga.DISPLAY_NUMBER -> { - val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) - itemView.context.getString(R.string.display_mode_chapter, number) - } - else -> chapter.name - } - - // Set the correct drawable for dropdown and update the tint to match theme. - chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color)) - - // Set correct text color - chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) - if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor) - - if (chapter.date_upload > 0) { - chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload)) - chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) - } else { - chapter_date.text = "" - } - - //add scanlator if exists - chapter_scanlator.text = chapter.scanlator - //allow longer titles if there is no scanlator (most sources) - if (chapter_scanlator.text.isNullOrBlank()) { - chapter_title.maxLines = 2 - chapter_scanlator.gone() - } else { - chapter_title.maxLines = 1 - } - - chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) { - itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1) - } else { - "" - } - - notifyStatus(item.status) - } - - fun notifyStatus(status: Int) = with(download_text) { - when (status) { - Download.QUEUE -> setText(R.string.chapter_queued) - Download.DOWNLOADING -> setText(R.string.chapter_downloading) - Download.DOWNLOADED -> setText(R.string.chapter_downloaded) - Download.ERROR -> setText(R.string.chapter_error) - else -> text = "" - } - } - - private fun showPopupMenu(view: View) { - val item = adapter.getItem(adapterPosition) ?: return - - // Create a PopupMenu, giving it the clicked view for an anchor - val popup = PopupMenu(view.context, view) - - // Inflate our menu resource into the PopupMenu's Menu - popup.menuInflater.inflate(R.menu.chapter_single, popup.menu) - - val chapter = item.chapter - - // Hide download and show delete if the chapter is downloaded - if (item.isDownloaded) { - popup.menu.findItem(R.id.action_download).isVisible = false - popup.menu.findItem(R.id.action_delete).isVisible = true - } - - // Hide bookmark if bookmark - popup.menu.findItem(R.id.action_bookmark).isVisible = !chapter.bookmark - popup.menu.findItem(R.id.action_remove_bookmark).isVisible = chapter.bookmark - - // Hide mark as unread when the chapter is unread - if (!chapter.read && chapter.last_page_read == 0) { - popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false - } - - // Hide mark as read when the chapter is read - if (chapter.read) { - popup.menu.findItem(R.id.action_mark_as_read).isVisible = false - } - - // Set a listener so we are notified if a menu item is clicked - popup.setOnMenuItemClickListener { menuItem -> - adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem) - true - } - - // Finally show the PopupMenu - popup.show() - } - -} +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.view.View +import android.widget.PopupMenu +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.util.getResourceColor +import eu.kanade.tachiyomi.util.gone +import eu.kanade.tachiyomi.util.setVectorCompat +import kotlinx.android.synthetic.main.chapters_item.* +import java.util.* + +class ChapterHolder( + private val view: View, + private val adapter: ChaptersAdapter +) : BaseFlexibleViewHolder(view, adapter) { + + init { + // We need to post a Runnable to show the popup to make sure that the PopupMenu is + // correctly positioned. The reason being that the view may change position before the + // PopupMenu is shown. + chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } + } + + fun bind(item: ChapterItem, manga: Manga) { + val chapter = item.chapter + + chapter_title.text = when (manga.displayMode) { + Manga.DISPLAY_NUMBER -> { + val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) + itemView.context.getString(R.string.display_mode_chapter, number) + } + else -> chapter.name + } + + // Set the correct drawable for dropdown and update the tint to match theme. + chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color)) + + // Set correct text color + chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) + if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor) + + if (chapter.date_upload > 0) { + chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload)) + chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) + } else { + chapter_date.text = "" + } + + //add scanlator if exists + chapter_scanlator.text = chapter.scanlator + //allow longer titles if there is no scanlator (most sources) + if (chapter_scanlator.text.isNullOrBlank()) { + chapter_title.maxLines = 2 + chapter_scanlator.gone() + } else { + chapter_title.maxLines = 1 + } + + chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) { + itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1) + } else { + "" + } + + notifyStatus(item.status) + } + + fun notifyStatus(status: Int) = with(download_text) { + when (status) { + Download.QUEUE -> setText(R.string.chapter_queued) + Download.DOWNLOADING -> setText(R.string.chapter_downloading) + Download.DOWNLOADED -> setText(R.string.chapter_downloaded) + Download.ERROR -> setText(R.string.chapter_error) + else -> text = "" + } + } + + private fun showPopupMenu(view: View) { + val item = adapter.getItem(adapterPosition) ?: return + + // Create a PopupMenu, giving it the clicked view for an anchor + val popup = PopupMenu(view.context, view) + + // Inflate our menu resource into the PopupMenu's Menu + popup.menuInflater.inflate(R.menu.chapter_single, popup.menu) + + val chapter = item.chapter + + // Hide download and show delete if the chapter is downloaded + if (item.isDownloaded) { + popup.menu.findItem(R.id.action_download).isVisible = false + popup.menu.findItem(R.id.action_delete).isVisible = true + } + + // Hide bookmark if bookmark + popup.menu.findItem(R.id.action_bookmark).isVisible = !chapter.bookmark + popup.menu.findItem(R.id.action_remove_bookmark).isVisible = chapter.bookmark + + // Hide mark as unread when the chapter is unread + if (!chapter.read && chapter.last_page_read == 0) { + popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false + } + + // Hide mark as read when the chapter is read + if (chapter.read) { + popup.menu.findItem(R.id.action_mark_as_read).isVisible = false + } + + // Set a listener so we are notified if a menu item is clicked + popup.setOnMenuItemClickListener { menuItem -> + adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem) + true + } + + // Finally show the PopupMenu + popup.show() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt index 34fb89ff2..eae788c0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt @@ -1,55 +1,55 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download - -class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem(), - Chapter by chapter { - - private var _status: Int = 0 - - var status: Int - get() = download?.status ?: _status - set(value) { _status = value } - - @Transient var download: Download? = null - - val isDownloaded: Boolean - get() = status == Download.DOWNLOADED - - override fun getLayoutRes(): Int { - return R.layout.chapters_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ChapterHolder { - return ChapterHolder(view, adapter as ChaptersAdapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, - holder: ChapterHolder, - position: Int, - payloads: List?) { - - holder.bind(this, manga) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other is ChapterItem) { - return chapter.id!! == other.chapter.id!! - } - return false - } - - override fun hashCode(): Int { - return chapter.id!!.hashCode() - } - -} +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.model.Download + +class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem(), + Chapter by chapter { + + private var _status: Int = 0 + + var status: Int + get() = download?.status ?: _status + set(value) { _status = value } + + @Transient var download: Download? = null + + val isDownloaded: Boolean + get() = status == Download.DOWNLOADED + + override fun getLayoutRes(): Int { + return R.layout.chapters_item + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ChapterHolder { + return ChapterHolder(view, adapter as ChaptersAdapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter>, + holder: ChapterHolder, + position: Int, + payloads: List?) { + + holder.bind(this, manga) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is ChapterItem) { + return chapter.id!! == other.chapter.id!! + } + return false + } + + override fun hashCode(): Int { + return chapter.id!!.hashCode() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt index f22b57613..6bb9226d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt @@ -1,45 +1,45 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.content.Context -import android.view.MenuItem -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.getResourceColor -import java.text.DateFormat -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols - -class ChaptersAdapter( - controller: ChaptersController, - context: Context -) : FlexibleAdapter(null, controller, true) { - - var items: List = emptyList() - - val menuItemListener: OnMenuItemClickListener = controller - - val readColor = context.getResourceColor(android.R.attr.textColorHint) - - val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary) - - val bookmarkedColor = context.getResourceColor(R.attr.colorAccent) - - val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols() - .apply { decimalSeparator = '.' }) - - val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT) - - override fun updateDataSet(items: List?) { - this.items = items ?: emptyList() - super.updateDataSet(items) - } - - fun indexOf(item: ChapterItem): Int { - return items.indexOf(item) - } - - interface OnMenuItemClickListener { - fun onMenuItemClick(position: Int, item: MenuItem) - } - -} +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.content.Context +import android.view.MenuItem +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceColor +import java.text.DateFormat +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols + +class ChaptersAdapter( + controller: ChaptersController, + context: Context +) : FlexibleAdapter(null, controller, true) { + + var items: List = emptyList() + + val menuItemListener: OnMenuItemClickListener = controller + + val readColor = context.getResourceColor(android.R.attr.textColorHint) + + val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary) + + val bookmarkedColor = context.getResourceColor(R.attr.colorAccent) + + val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols() + .apply { decimalSeparator = '.' }) + + val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT) + + override fun updateDataSet(items: List?) { + this.items = items ?: emptyList() + super.updateDataSet(items) + } + + fun indexOf(item: ChapterItem): Int { + return items.indexOf(item) + } + + interface OnMenuItemClickListener { + fun onMenuItemClick(position: Int, item: MenuItem) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt index 7c5c07a21..315985106 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt @@ -1,486 +1,486 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import com.google.android.material.snackbar.Snackbar -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import android.view.* -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import com.jakewharton.rxbinding.view.clicks -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.getCoordinates -import eu.kanade.tachiyomi.util.snack -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.chapters_controller.* -import timber.log.Timber - -class ChaptersController : NucleusController(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - ChaptersAdapter.OnMenuItemClickListener, - SetDisplayModeDialog.Listener, - SetSortingDialog.Listener, - DownloadChaptersDialog.Listener, - DownloadCustomChaptersDialog.Listener, - DeleteChaptersDialog.Listener { - - /** - * Adapter containing a list of chapters. - */ - private var adapter: ChaptersAdapter? = null - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - /** - * Selected items. Used to restore selections after a rotation. - */ - private val selectedItems = mutableSetOf() - - init { - setHasOptionsMenu(true) - setOptionsMenuHidden(true) - } - - override fun createPresenter(): ChaptersPresenter { - val ctrl = parentController as MangaController - return ChaptersPresenter(ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.chapters_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - // Init RecyclerView and adapter - adapter = ChaptersAdapter(this, view.context) - - recycler.adapter = adapter - recycler.layoutManager = LinearLayoutManager(view.context) - recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - recycler.setHasFixedSize(true) - adapter?.fastScroller = fast_scroller - - swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() } - - fab.clicks().subscribeUntilDestroy { - val item = presenter.getNextUnreadChapter() - if (item != null) { - // Create animation listener - val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?) { - openChapter(item.chapter, true) - } - } - - // Get coordinates and start animation - val coordinates = fab.getCoordinates() - if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { - openChapter(item.chapter) - } - } else { - view.context.toast(R.string.no_next_chapter) - } - } - } - - override fun onDestroyView(view: View) { - adapter = null - actionMode = null - super.onDestroyView(view) - } - - override fun onActivityResumed(activity: Activity) { - if (view == null) return - - // Check if animation view is visible - if (reveal_view.visibility == View.VISIBLE) { - // Show the unReveal effect - val coordinates = fab.getCoordinates() - reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) - } - super.onActivityResumed(activity) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.chapters, menu) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - // Initialize menu items. - val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return - val menuFilterUnread = menu.findItem(R.id.action_filter_unread) - val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) - val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) - - // Set correct checkbox values. - menuFilterRead.isChecked = presenter.onlyRead() - menuFilterUnread.isChecked = presenter.onlyUnread() - menuFilterDownloaded.isChecked = presenter.onlyDownloaded() - menuFilterBookmarked.isChecked = presenter.onlyBookmarked() - - if (presenter.onlyRead()) - //Disable unread filter option if read filter is enabled. - menuFilterUnread.isEnabled = false - if (presenter.onlyUnread()) - //Disable read filter option if unread filter is enabled. - menuFilterRead.isEnabled = false - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_display_mode -> showDisplayModeDialog() - R.id.manga_download -> showDownloadDialog() - R.id.action_sorting_mode -> showSortingDialog() - R.id.action_filter_unread -> { - item.isChecked = !item.isChecked - presenter.setUnreadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_read -> { - item.isChecked = !item.isChecked - presenter.setReadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_downloaded -> { - item.isChecked = !item.isChecked - presenter.setDownloadedFilter(item.isChecked) - } - R.id.action_filter_bookmarked -> { - item.isChecked = !item.isChecked - presenter.setBookmarkedFilter(item.isChecked) - } - R.id.action_filter_empty -> { - presenter.removeFilters() - activity?.invalidateOptionsMenu() - } - R.id.action_sort -> presenter.revertSortOrder() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - fun onNextChapters(chapters: List) { - // If the list is empty, fetch chapters from source if the conditions are met - // We use presenter chapters instead because they are always unfiltered - if (presenter.chapters.isEmpty()) - initialFetchChapters() - - val adapter = adapter ?: return - adapter.updateDataSet(chapters) - - if (selectedItems.isNotEmpty()) { - adapter.clearSelection() // we need to start from a clean state, index may have changed - createActionModeIfNeeded() - selectedItems.forEach { item -> - val position = adapter.indexOf(item) - if (position != -1 && !adapter.isSelected(position)) { - adapter.toggleSelection(position) - } - } - actionMode?.invalidate() - } - - } - - private fun initialFetchChapters() { - // Only fetch if this view is from the catalog and it hasn't requested previously - if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) { - fetchChaptersFromSource() - } - } - - private fun fetchChaptersFromSource() { - swipe_refresh?.isRefreshing = true - presenter.fetchChaptersFromSource() - } - - fun onFetchChaptersDone() { - swipe_refresh?.isRefreshing = false - } - - fun onFetchChaptersError(error: Throwable) { - swipe_refresh?.isRefreshing = false - activity?.toast(error.message) - } - - fun onChapterStatusChange(download: Download) { - getHolder(download.chapter)?.notifyStatus(download.status) - } - - private fun getHolder(chapter: Chapter): ChapterHolder? { - return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder - } - - fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { - val activity = activity ?: return - val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) - if (hasAnimation) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) - } - startActivity(intent) - } - - override fun onItemClick(view: View, position: Int): Boolean { - val adapter = adapter ?: return false - val item = adapter.getItem(position) ?: return false - if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { - toggleSelection(position) - return true - } else { - openChapter(item.chapter) - return false - } - } - - override fun onItemLongClick(position: Int) { - createActionModeIfNeeded() - toggleSelection(position) - } - - // SELECTIONS & ACTION MODE - - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - val item = adapter.getItem(position) ?: return - adapter.toggleSelection(position) - if (adapter.isSelected(position)) { - selectedItems.add(item) - } else { - selectedItems.remove(item) - } - actionMode?.invalidate() - } - - private fun getSelectedChapters(): List { - val adapter = adapter ?: return emptyList() - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } - } - - private fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) - } - } - - private fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.chapter_selection, menu) - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - @SuppressLint("StringFormatInvalid") - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = adapter?.selectedItemCount ?: 0 - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = resources?.getString(R.string.label_selected, count) - } - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_select_all -> selectAll() - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> showDeleteChaptersConfirmationDialog() - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode) { - adapter?.mode = SelectableAdapter.Mode.SINGLE - adapter?.clearSelection() - selectedItems.clear() - actionMode = null - } - - override fun onMenuItemClick(position: Int, item: MenuItem) { - val chapter = adapter?.getItem(position) ?: return - val chapters = listOf(chapter) - - when (item.itemId) { - R.id.action_download -> downloadChapters(chapters) - R.id.action_bookmark -> bookmarkChapters(chapters, true) - R.id.action_remove_bookmark -> bookmarkChapters(chapters, false) - R.id.action_delete -> deleteChapters(chapters) - R.id.action_mark_as_read -> markAsRead(chapters) - R.id.action_mark_as_unread -> markAsUnread(chapters) - R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter) - } - } - - // SELECTION MODE ACTIONS - - private fun selectAll() { - val adapter = adapter ?: return - adapter.selectAll() - selectedItems.addAll(adapter.items) - actionMode?.invalidate() - } - - private fun markAsRead(chapters: List) { - presenter.markChaptersRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) - } - } - - private fun markAsUnread(chapters: List) { - presenter.markChaptersRead(chapters, false) - } - - private fun downloadChapters(chapters: List) { - val view = view - destroyActionModeIfNeeded() - presenter.downloadChapters(chapters) - if (view != null && !presenter.manga.favorite) { - recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_add) { - presenter.addToLibrary() - } - } - } - } - - - private fun showDeleteChaptersConfirmationDialog() { - DeleteChaptersDialog(this).showDialog(router) - } - - override fun deleteChapters() { - deleteChapters(getSelectedChapters()) - } - - private fun markPreviousAsRead(chapter: ChapterItem) { - val adapter = adapter ?: return - val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items - val chapterPos = chapters.indexOf(chapter) - if (chapterPos != -1) { - markAsRead(chapters.take(chapterPos)) - } - } - - private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { - destroyActionModeIfNeeded() - presenter.bookmarkChapters(chapters, bookmarked) - } - - fun deleteChapters(chapters: List) { - destroyActionModeIfNeeded() - if (chapters.isEmpty()) return - - DeletingChaptersDialog().showDialog(router) - presenter.deleteChapters(chapters) - } - - fun onChaptersDeleted() { - dismissDeletingDialog() - adapter?.notifyDataSetChanged() - } - - fun onChaptersDeletedError(error: Throwable) { - dismissDeletingDialog() - Timber.e(error) - } - - private fun dismissDeletingDialog() { - router.popControllerWithTag(DeletingChaptersDialog.TAG) - } - - // OVERFLOW MENU DIALOGS - - private fun showDisplayModeDialog() { - val preselected = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1 - SetDisplayModeDialog(this, preselected).showDialog(router) - } - - override fun setDisplayMode(id: Int) { - presenter.setDisplayMode(id) - adapter?.notifyDataSetChanged() - } - - private fun showSortingDialog() { - val preselected = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1 - SetSortingDialog(this, preselected).showDialog(router) - } - - override fun setSorting(id: Int) { - presenter.setSorting(id) - } - - private fun showDownloadDialog() { - DownloadChaptersDialog(this).showDialog(router) - } - - private fun getUnreadChaptersSorted() = presenter.chapters - .filter { !it.read && it.status == Download.NOT_DOWNLOADED } - .distinctBy { it.name } - .sortedByDescending { it.source_order } - - override fun downloadCustomChapters(amount: Int) { - val chaptersToDownload = getUnreadChaptersSorted().take(amount) - if (chaptersToDownload.isNotEmpty()) { - 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) - } - } -} +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import com.google.android.material.snackbar.Snackbar +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import android.view.* +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import com.jakewharton.rxbinding.view.clicks +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.getCoordinates +import eu.kanade.tachiyomi.util.snack +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.chapters_controller.* +import timber.log.Timber + +class ChaptersController : NucleusController(), + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + ChaptersAdapter.OnMenuItemClickListener, + SetDisplayModeDialog.Listener, + SetSortingDialog.Listener, + DownloadChaptersDialog.Listener, + DownloadCustomChaptersDialog.Listener, + DeleteChaptersDialog.Listener { + + /** + * Adapter containing a list of chapters. + */ + private var adapter: ChaptersAdapter? = null + + /** + * Action mode for multiple selection. + */ + private var actionMode: ActionMode? = null + + /** + * Selected items. Used to restore selections after a rotation. + */ + private val selectedItems = mutableSetOf() + + init { + setHasOptionsMenu(true) + setOptionsMenuHidden(true) + } + + override fun createPresenter(): ChaptersPresenter { + val ctrl = parentController as MangaController + return ChaptersPresenter(ctrl.manga!!, ctrl.source!!, + ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.chapters_controller, container, false) + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + // Init RecyclerView and adapter + adapter = ChaptersAdapter(this, view.context) + + recycler.adapter = adapter + recycler.layoutManager = LinearLayoutManager(view.context) + recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + recycler.setHasFixedSize(true) + adapter?.fastScroller = fast_scroller + + swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() } + + fab.clicks().subscribeUntilDestroy { + val item = presenter.getNextUnreadChapter() + if (item != null) { + // Create animation listener + val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator?) { + openChapter(item.chapter, true) + } + } + + // Get coordinates and start animation + val coordinates = fab.getCoordinates() + if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { + openChapter(item.chapter) + } + } else { + view.context.toast(R.string.no_next_chapter) + } + } + } + + override fun onDestroyView(view: View) { + adapter = null + actionMode = null + super.onDestroyView(view) + } + + override fun onActivityResumed(activity: Activity) { + if (view == null) return + + // Check if animation view is visible + if (reveal_view.visibility == View.VISIBLE) { + // Show the unReveal effect + val coordinates = fab.getCoordinates() + reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) + } + super.onActivityResumed(activity) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.chapters, menu) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + // Initialize menu items. + val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return + val menuFilterUnread = menu.findItem(R.id.action_filter_unread) + val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) + val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) + + // Set correct checkbox values. + menuFilterRead.isChecked = presenter.onlyRead() + menuFilterUnread.isChecked = presenter.onlyUnread() + menuFilterDownloaded.isChecked = presenter.onlyDownloaded() + menuFilterBookmarked.isChecked = presenter.onlyBookmarked() + + if (presenter.onlyRead()) + //Disable unread filter option if read filter is enabled. + menuFilterUnread.isEnabled = false + if (presenter.onlyUnread()) + //Disable read filter option if unread filter is enabled. + menuFilterRead.isEnabled = false + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_display_mode -> showDisplayModeDialog() + R.id.manga_download -> showDownloadDialog() + R.id.action_sorting_mode -> showSortingDialog() + R.id.action_filter_unread -> { + item.isChecked = !item.isChecked + presenter.setUnreadFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_read -> { + item.isChecked = !item.isChecked + presenter.setReadFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_downloaded -> { + item.isChecked = !item.isChecked + presenter.setDownloadedFilter(item.isChecked) + } + R.id.action_filter_bookmarked -> { + item.isChecked = !item.isChecked + presenter.setBookmarkedFilter(item.isChecked) + } + R.id.action_filter_empty -> { + presenter.removeFilters() + activity?.invalidateOptionsMenu() + } + R.id.action_sort -> presenter.revertSortOrder() + else -> return super.onOptionsItemSelected(item) + } + return true + } + + fun onNextChapters(chapters: List) { + // If the list is empty, fetch chapters from source if the conditions are met + // We use presenter chapters instead because they are always unfiltered + if (presenter.chapters.isEmpty()) + initialFetchChapters() + + val adapter = adapter ?: return + adapter.updateDataSet(chapters) + + if (selectedItems.isNotEmpty()) { + adapter.clearSelection() // we need to start from a clean state, index may have changed + createActionModeIfNeeded() + selectedItems.forEach { item -> + val position = adapter.indexOf(item) + if (position != -1 && !adapter.isSelected(position)) { + adapter.toggleSelection(position) + } + } + actionMode?.invalidate() + } + + } + + private fun initialFetchChapters() { + // Only fetch if this view is from the catalog and it hasn't requested previously + if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) { + fetchChaptersFromSource() + } + } + + private fun fetchChaptersFromSource() { + swipe_refresh?.isRefreshing = true + presenter.fetchChaptersFromSource() + } + + fun onFetchChaptersDone() { + swipe_refresh?.isRefreshing = false + } + + fun onFetchChaptersError(error: Throwable) { + swipe_refresh?.isRefreshing = false + activity?.toast(error.message) + } + + fun onChapterStatusChange(download: Download) { + getHolder(download.chapter)?.notifyStatus(download.status) + } + + private fun getHolder(chapter: Chapter): ChapterHolder? { + return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder + } + + fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { + val activity = activity ?: return + val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) + if (hasAnimation) { + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + } + startActivity(intent) + } + + override fun onItemClick(view: View, position: Int): Boolean { + val adapter = adapter ?: return false + val item = adapter.getItem(position) ?: return false + if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { + toggleSelection(position) + return true + } else { + openChapter(item.chapter) + return false + } + } + + override fun onItemLongClick(position: Int) { + createActionModeIfNeeded() + toggleSelection(position) + } + + // SELECTIONS & ACTION MODE + + private fun toggleSelection(position: Int) { + val adapter = adapter ?: return + val item = adapter.getItem(position) ?: return + adapter.toggleSelection(position) + if (adapter.isSelected(position)) { + selectedItems.add(item) + } else { + selectedItems.remove(item) + } + actionMode?.invalidate() + } + + private fun getSelectedChapters(): List { + val adapter = adapter ?: return emptyList() + return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } + } + + private fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) + } + } + + private fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.chapter_selection, menu) + adapter?.mode = SelectableAdapter.Mode.MULTI + return true + } + + @SuppressLint("StringFormatInvalid") + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = adapter?.selectedItemCount ?: 0 + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = resources?.getString(R.string.label_selected, count) + } + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_select_all -> selectAll() + R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) + R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) + R.id.action_download -> downloadChapters(getSelectedChapters()) + R.id.action_delete -> showDeleteChaptersConfirmationDialog() + else -> return false + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + adapter?.mode = SelectableAdapter.Mode.SINGLE + adapter?.clearSelection() + selectedItems.clear() + actionMode = null + } + + override fun onMenuItemClick(position: Int, item: MenuItem) { + val chapter = adapter?.getItem(position) ?: return + val chapters = listOf(chapter) + + when (item.itemId) { + R.id.action_download -> downloadChapters(chapters) + R.id.action_bookmark -> bookmarkChapters(chapters, true) + R.id.action_remove_bookmark -> bookmarkChapters(chapters, false) + R.id.action_delete -> deleteChapters(chapters) + R.id.action_mark_as_read -> markAsRead(chapters) + R.id.action_mark_as_unread -> markAsUnread(chapters) + R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter) + } + } + + // SELECTION MODE ACTIONS + + private fun selectAll() { + val adapter = adapter ?: return + adapter.selectAll() + selectedItems.addAll(adapter.items) + actionMode?.invalidate() + } + + private fun markAsRead(chapters: List) { + presenter.markChaptersRead(chapters, true) + if (presenter.preferences.removeAfterMarkedAsRead()) { + deleteChapters(chapters) + } + } + + private fun markAsUnread(chapters: List) { + presenter.markChaptersRead(chapters, false) + } + + private fun downloadChapters(chapters: List) { + val view = view + destroyActionModeIfNeeded() + presenter.downloadChapters(chapters) + if (view != null && !presenter.manga.favorite) { + recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { + setAction(R.string.action_add) { + presenter.addToLibrary() + } + } + } + } + + + private fun showDeleteChaptersConfirmationDialog() { + DeleteChaptersDialog(this).showDialog(router) + } + + override fun deleteChapters() { + deleteChapters(getSelectedChapters()) + } + + private fun markPreviousAsRead(chapter: ChapterItem) { + val adapter = adapter ?: return + val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items + val chapterPos = chapters.indexOf(chapter) + if (chapterPos != -1) { + markAsRead(chapters.take(chapterPos)) + } + } + + private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + destroyActionModeIfNeeded() + presenter.bookmarkChapters(chapters, bookmarked) + } + + fun deleteChapters(chapters: List) { + destroyActionModeIfNeeded() + if (chapters.isEmpty()) return + + DeletingChaptersDialog().showDialog(router) + presenter.deleteChapters(chapters) + } + + fun onChaptersDeleted() { + dismissDeletingDialog() + adapter?.notifyDataSetChanged() + } + + fun onChaptersDeletedError(error: Throwable) { + dismissDeletingDialog() + Timber.e(error) + } + + private fun dismissDeletingDialog() { + router.popControllerWithTag(DeletingChaptersDialog.TAG) + } + + // OVERFLOW MENU DIALOGS + + private fun showDisplayModeDialog() { + val preselected = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1 + SetDisplayModeDialog(this, preselected).showDialog(router) + } + + override fun setDisplayMode(id: Int) { + presenter.setDisplayMode(id) + adapter?.notifyDataSetChanged() + } + + private fun showSortingDialog() { + val preselected = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1 + SetSortingDialog(this, preselected).showDialog(router) + } + + override fun setSorting(id: Int) { + presenter.setSorting(id) + } + + private fun showDownloadDialog() { + DownloadChaptersDialog(this).showDialog(router) + } + + private fun getUnreadChaptersSorted() = presenter.chapters + .filter { !it.read && it.status == Download.NOT_DOWNLOADED } + .distinctBy { it.name } + .sortedByDescending { it.source_order } + + override fun downloadCustomChapters(amount: Int) { + val chaptersToDownload = getUnreadChaptersSorted().take(amount) + if (chaptersToDownload.isNotEmpty()) { + 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) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index ff000061e..b04369c34 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -1,418 +1,418 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import eu.kanade.tachiyomi.util.syncChaptersWithSource -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import timber.log.Timber -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.Date - -/** - * Presenter of [ChaptersController]. - */ -class ChaptersPresenter( - val manga: Manga, - val source: Source, - private val chapterCountRelay: BehaviorRelay, - private val lastUpdateRelay: BehaviorRelay, - private val mangaFavoriteRelay: PublishRelay, - val preferences: PreferencesHelper = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get() -) : BasePresenter() { - - /** - * List of chapters of the manga. It's always unfiltered and unsorted. - */ - var chapters: List = emptyList() - private set - - /** - * Subject of list of chapters to allow updating the view without going to DB. - */ - val chaptersRelay: PublishRelay> - by lazy { PublishRelay.create>() } - - /** - * Whether the chapter list has been requested to the source. - */ - var hasRequested = false - private set - - /** - * Subscription to retrieve the new list of chapters from the source. - */ - private var fetchChaptersSubscription: Subscription? = null - - /** - * Subscription to observe download status changes. - */ - private var observeDownloadsSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - // Prepare the relay. - chaptersRelay.flatMap { applyChapterFilters(it) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(ChaptersController::onNextChapters, - { _, error -> Timber.e(error) }) - - // Add the subscription that retrieves the chapters from the database, keeps subscribed to - // changes, and sends the list of chapters to the relay. - add(db.getChapters(manga).asRxObservable() - .map { chapters -> - // Convert every chapter to a model. - chapters.map { it.toModel() } - } - .doOnNext { chapters -> - // Find downloaded chapters - setDownloadedChapters(chapters) - - // Store the last emission - this.chapters = chapters - - // Listen for download status changes - observeDownloads() - - // Emit the number of chapters to the info tab. - chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number - ?: 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) }) - } - - private fun observeDownloads() { - observeDownloadsSubscription?.let { remove(it) } - observeDownloadsSubscription = downloadManager.queue.getStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .filter { download -> download.manga.id == manga.id } - .doOnNext { onDownloadStatusChange(it) } - .subscribeLatestCache(ChaptersController::onChapterStatusChange, - { _, error -> Timber.e(error) }) - } - - /** - * Converts a chapter from the database to an extended model, allowing to store new fields. - */ - private fun Chapter.toModel(): ChapterItem { - // Create the model object. - val model = ChapterItem(this, manga) - - // Find an active download for this chapter. - val download = downloadManager.queue.find { it.chapter.id == id } - - if (download != null) { - // If there's an active download, assign it. - model.download = download - } - return model - } - - /** - * Finds and assigns the list of downloaded chapters. - * - * @param chapters the list of chapter from the database. - */ - private fun setDownloadedChapters(chapters: List) { - for (chapter in chapters) { - if (downloadManager.isChapterDownloaded(chapter, manga)) { - chapter.status = Download.DOWNLOADED - } - } - } - - /** - * Requests an updated list of chapters from the source. - */ - fun fetchChaptersFromSource() { - hasRequested = true - - if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return - fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } - .subscribeOn(Schedulers.io()) - .map { syncChaptersWithSource(db, it, manga, source) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onFetchChaptersDone() - }, ChaptersController::onFetchChaptersError) - } - - /** - * Updates the UI after applying the filters. - */ - private fun refreshChapters() { - chaptersRelay.call(chapters) - } - - /** - * Applies the view filters to the list of chapters obtained from the database. - * @param chapters the list of chapters from the database - * @return an observable of the list of chapters filtered and sorted. - */ - private fun applyChapterFilters(chapters: List): Observable> { - var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) - if (onlyUnread()) { - observable = observable.filter { !it.read } - } - else if (onlyRead()) { - observable = observable.filter { it.read } - } - if (onlyDownloaded()) { - observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID } - } - if (onlyBookmarked()) { - observable = observable.filter { it.bookmark } - } - val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { - Manga.SORTING_SOURCE -> when (sortDescending()) { - true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } - false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } - } - Manga.SORTING_NUMBER -> when (sortDescending()) { - true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } - false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } - } - else -> throw NotImplementedError("Unimplemented sorting method") - } - return observable.toSortedList(sortFunction) - } - - /** - * Called when a download for the active manga changes status. - * @param download the download whose status changed. - */ - fun onDownloadStatusChange(download: Download) { - // Assign the download to the model object. - if (download.status == Download.QUEUE) { - chapters.find { it.id == download.chapter.id }?.let { - if (it.download == null) { - it.download = download - } - } - } - - // Force UI update if downloaded filter active and download finished. - if (onlyDownloaded() && download.status == Download.DOWNLOADED) - refreshChapters() - } - - /** - * Returns the next unread chapter or null if everything is read. - */ - fun getNextUnreadChapter(): ChapterItem? { - return chapters.sortedByDescending { it.source_order }.find { !it.read } - } - - /** - * Mark the selected chapter list as read/unread. - * @param selectedChapters the list of selected chapters. - * @param read whether to mark chapters as read or unread. - */ - fun markChaptersRead(selectedChapters: List, read: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.read = read - if (!read) { - chapter.last_page_read = 0 - } - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Downloads the given list of chapters with the manager. - * @param chapters the list of chapters to download. - */ - fun downloadChapters(chapters: List) { - downloadManager.downloadChapters(manga, chapters) - } - - /** - * Bookmarks the given list of chapters. - * @param selectedChapters the list of chapters to bookmark. - */ - fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.bookmark = bookmarked - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Deletes the given list of chapter. - * @param chapters the list of chapters to delete. - */ - fun deleteChapters(chapters: List) { - Observable.just(chapters) - .doOnNext { deleteChaptersInternal(chapters) } - .doOnNext { if (onlyDownloaded()) refreshChapters() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onChaptersDeleted() - }, ChaptersController::onChaptersDeletedError) - } - - /** - * Deletes a list of chapters from disk. This method is called in a background thread. - * @param chapters the chapters to delete. - */ - private fun deleteChaptersInternal(chapters: List) { - downloadManager.deleteChapters(chapters, manga, source) - chapters.forEach { - it.status = Download.NOT_DOWNLOADED - it.download = null - } - } - - /** - * Reverses the sorting and requests an UI update. - */ - fun revertSortOrder() { - manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyUnread whether to display only unread chapters or all chapters. - */ - fun setUnreadFilter(onlyUnread: Boolean) { - manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyRead whether to display only read chapters or all chapters. - */ - fun setReadFilter(onlyRead: Boolean) { - manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the download filter and requests an UI update. - * @param onlyDownloaded whether to display only downloaded chapters or all chapters. - */ - fun setDownloadedFilter(onlyDownloaded: Boolean) { - manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the bookmark filter and requests an UI update. - * @param onlyBookmarked whether to display only bookmarked chapters or all chapters. - */ - fun setBookmarkedFilter(onlyBookmarked: Boolean) { - manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Removes all filters and requests an UI update. - */ - fun removeFilters() { - manga.readFilter = Manga.SHOW_ALL - manga.downloadedFilter = Manga.SHOW_ALL - manga.bookmarkedFilter = Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Adds manga to library - */ - fun addToLibrary() { - mangaFavoriteRelay.call(true) - } - - /** - * Sets the active display mode. - * @param mode the mode to set. - */ - fun setDisplayMode(mode: Int) { - manga.displayMode = mode - db.updateFlags(manga).executeAsBlocking() - } - - /** - * Sets the sorting method and requests an UI update. - * @param sort the sorting mode. - */ - fun setSorting(sort: Int) { - manga.sorting = sort - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyDownloaded(): Boolean { - return manga.downloadedFilter == Manga.SHOW_DOWNLOADED - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyBookmarked(): Boolean { - return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED - } - - /** - * Whether the display only unread filter is enabled. - */ - fun onlyUnread(): Boolean { - return manga.readFilter == Manga.SHOW_UNREAD - } - - /** - * Whether the display only read filter is enabled. - */ - fun onlyRead(): Boolean { - return manga.readFilter == Manga.SHOW_READ - } - - /** - * Whether the sorting method is descending or ascending. - */ - fun sortDescending(): Boolean { - return manga.sortDescending() - } - -} +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.os.Bundle +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.isNullOrUnsubscribed +import eu.kanade.tachiyomi.util.syncChaptersWithSource +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.Date + +/** + * Presenter of [ChaptersController]. + */ +class ChaptersPresenter( + val manga: Manga, + val source: Source, + private val chapterCountRelay: BehaviorRelay, + private val lastUpdateRelay: BehaviorRelay, + private val mangaFavoriteRelay: PublishRelay, + val preferences: PreferencesHelper = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get() +) : BasePresenter() { + + /** + * List of chapters of the manga. It's always unfiltered and unsorted. + */ + var chapters: List = emptyList() + private set + + /** + * Subject of list of chapters to allow updating the view without going to DB. + */ + val chaptersRelay: PublishRelay> + by lazy { PublishRelay.create>() } + + /** + * Whether the chapter list has been requested to the source. + */ + var hasRequested = false + private set + + /** + * Subscription to retrieve the new list of chapters from the source. + */ + private var fetchChaptersSubscription: Subscription? = null + + /** + * Subscription to observe download status changes. + */ + private var observeDownloadsSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + // Prepare the relay. + chaptersRelay.flatMap { applyChapterFilters(it) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(ChaptersController::onNextChapters, + { _, error -> Timber.e(error) }) + + // Add the subscription that retrieves the chapters from the database, keeps subscribed to + // changes, and sends the list of chapters to the relay. + add(db.getChapters(manga).asRxObservable() + .map { chapters -> + // Convert every chapter to a model. + chapters.map { it.toModel() } + } + .doOnNext { chapters -> + // Find downloaded chapters + setDownloadedChapters(chapters) + + // Store the last emission + this.chapters = chapters + + // Listen for download status changes + observeDownloads() + + // Emit the number of chapters to the info tab. + chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number + ?: 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) }) + } + + private fun observeDownloads() { + observeDownloadsSubscription?.let { remove(it) } + observeDownloadsSubscription = downloadManager.queue.getStatusObservable() + .observeOn(AndroidSchedulers.mainThread()) + .filter { download -> download.manga.id == manga.id } + .doOnNext { onDownloadStatusChange(it) } + .subscribeLatestCache(ChaptersController::onChapterStatusChange, + { _, error -> Timber.e(error) }) + } + + /** + * Converts a chapter from the database to an extended model, allowing to store new fields. + */ + private fun Chapter.toModel(): ChapterItem { + // Create the model object. + val model = ChapterItem(this, manga) + + // Find an active download for this chapter. + val download = downloadManager.queue.find { it.chapter.id == id } + + if (download != null) { + // If there's an active download, assign it. + model.download = download + } + return model + } + + /** + * Finds and assigns the list of downloaded chapters. + * + * @param chapters the list of chapter from the database. + */ + private fun setDownloadedChapters(chapters: List) { + for (chapter in chapters) { + if (downloadManager.isChapterDownloaded(chapter, manga)) { + chapter.status = Download.DOWNLOADED + } + } + } + + /** + * Requests an updated list of chapters from the source. + */ + fun fetchChaptersFromSource() { + hasRequested = true + + if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return + fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } + .subscribeOn(Schedulers.io()) + .map { syncChaptersWithSource(db, it, manga, source) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, _ -> + view.onFetchChaptersDone() + }, ChaptersController::onFetchChaptersError) + } + + /** + * Updates the UI after applying the filters. + */ + private fun refreshChapters() { + chaptersRelay.call(chapters) + } + + /** + * Applies the view filters to the list of chapters obtained from the database. + * @param chapters the list of chapters from the database + * @return an observable of the list of chapters filtered and sorted. + */ + private fun applyChapterFilters(chapters: List): Observable> { + var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) + if (onlyUnread()) { + observable = observable.filter { !it.read } + } + else if (onlyRead()) { + observable = observable.filter { it.read } + } + if (onlyDownloaded()) { + observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID } + } + if (onlyBookmarked()) { + observable = observable.filter { it.bookmark } + } + val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { + Manga.SORTING_SOURCE -> when (sortDescending()) { + true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } + false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } + } + Manga.SORTING_NUMBER -> when (sortDescending()) { + true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } + false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } + } + else -> throw NotImplementedError("Unimplemented sorting method") + } + return observable.toSortedList(sortFunction) + } + + /** + * Called when a download for the active manga changes status. + * @param download the download whose status changed. + */ + fun onDownloadStatusChange(download: Download) { + // Assign the download to the model object. + if (download.status == Download.QUEUE) { + chapters.find { it.id == download.chapter.id }?.let { + if (it.download == null) { + it.download = download + } + } + } + + // Force UI update if downloaded filter active and download finished. + if (onlyDownloaded() && download.status == Download.DOWNLOADED) + refreshChapters() + } + + /** + * Returns the next unread chapter or null if everything is read. + */ + fun getNextUnreadChapter(): ChapterItem? { + return chapters.sortedByDescending { it.source_order }.find { !it.read } + } + + /** + * Mark the selected chapter list as read/unread. + * @param selectedChapters the list of selected chapters. + * @param read whether to mark chapters as read or unread. + */ + fun markChaptersRead(selectedChapters: List, read: Boolean) { + Observable.from(selectedChapters) + .doOnNext { chapter -> + chapter.read = read + if (!read) { + chapter.last_page_read = 0 + } + } + .toList() + .flatMap { db.updateChaptersProgress(it).asRxObservable() } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Downloads the given list of chapters with the manager. + * @param chapters the list of chapters to download. + */ + fun downloadChapters(chapters: List) { + downloadManager.downloadChapters(manga, chapters) + } + + /** + * Bookmarks the given list of chapters. + * @param selectedChapters the list of chapters to bookmark. + */ + fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { + Observable.from(selectedChapters) + .doOnNext { chapter -> + chapter.bookmark = bookmarked + } + .toList() + .flatMap { db.updateChaptersProgress(it).asRxObservable() } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Deletes the given list of chapter. + * @param chapters the list of chapters to delete. + */ + fun deleteChapters(chapters: List) { + Observable.just(chapters) + .doOnNext { deleteChaptersInternal(chapters) } + .doOnNext { if (onlyDownloaded()) refreshChapters() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, _ -> + view.onChaptersDeleted() + }, ChaptersController::onChaptersDeletedError) + } + + /** + * Deletes a list of chapters from disk. This method is called in a background thread. + * @param chapters the chapters to delete. + */ + private fun deleteChaptersInternal(chapters: List) { + downloadManager.deleteChapters(chapters, manga, source) + chapters.forEach { + it.status = Download.NOT_DOWNLOADED + it.download = null + } + } + + /** + * Reverses the sorting and requests an UI update. + */ + fun revertSortOrder() { + manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the read filter and requests an UI update. + * @param onlyUnread whether to display only unread chapters or all chapters. + */ + fun setUnreadFilter(onlyUnread: Boolean) { + manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the read filter and requests an UI update. + * @param onlyRead whether to display only read chapters or all chapters. + */ + fun setReadFilter(onlyRead: Boolean) { + manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the download filter and requests an UI update. + * @param onlyDownloaded whether to display only downloaded chapters or all chapters. + */ + fun setDownloadedFilter(onlyDownloaded: Boolean) { + manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the bookmark filter and requests an UI update. + * @param onlyBookmarked whether to display only bookmarked chapters or all chapters. + */ + fun setBookmarkedFilter(onlyBookmarked: Boolean) { + manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Removes all filters and requests an UI update. + */ + fun removeFilters() { + manga.readFilter = Manga.SHOW_ALL + manga.downloadedFilter = Manga.SHOW_ALL + manga.bookmarkedFilter = Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Adds manga to library + */ + fun addToLibrary() { + mangaFavoriteRelay.call(true) + } + + /** + * Sets the active display mode. + * @param mode the mode to set. + */ + fun setDisplayMode(mode: Int) { + manga.displayMode = mode + db.updateFlags(manga).executeAsBlocking() + } + + /** + * Sets the sorting method and requests an UI update. + * @param sort the sorting mode. + */ + fun setSorting(sort: Int) { + manga.sorting = sort + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Whether the display only downloaded filter is enabled. + */ + fun onlyDownloaded(): Boolean { + return manga.downloadedFilter == Manga.SHOW_DOWNLOADED + } + + /** + * Whether the display only downloaded filter is enabled. + */ + fun onlyBookmarked(): Boolean { + return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED + } + + /** + * Whether the display only unread filter is enabled. + */ + fun onlyUnread(): Boolean { + return manga.readFilter == Manga.SHOW_UNREAD + } + + /** + * Whether the display only read filter is enabled. + */ + fun onlyRead(): Boolean { + return manga.readFilter == Manga.SHOW_READ + } + + /** + * Whether the sorting method is descending or ascending. + */ + fun sortDescending(): Boolean { + return manga.sortDescending() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt index a269fe085..1ac72e731 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt @@ -1,32 +1,32 @@ -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 - -class DeleteChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : DeleteChaptersDialog.Listener { - - constructor(target: T) : this() { - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialDialog.Builder(activity!!) - .content(R.string.confirm_delete_chapters) - .positiveText(android.R.string.yes) - .negativeText(android.R.string.no) - .onPositive { _, _ -> - (targetController as? Listener)?.deleteChapters() - } - .show() - } - - interface Listener { - fun deleteChapters() - } - +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 + +class DeleteChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : DeleteChaptersDialog.Listener { + + constructor(target: T) : this() { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .content(R.string.confirm_delete_chapters) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no) + .onPositive { _, _ -> + (targetController as? Listener)?.deleteChapters() + } + .show() + } + + interface Listener { + fun deleteChapters() + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt index fcfd6b9ad..8fa6df586 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt @@ -1,27 +1,27 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.app.Dialog -import android.os.Bundle -import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.Router -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController - -class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) { - - companion object { - const val TAG = "deleting_dialog" - } - - override fun onCreateDialog(savedState: Bundle?): Dialog { - return MaterialDialog.Builder(activity!!) - .progress(true, 0) - .content(R.string.deleting) - .build() - } - - override fun showDialog(router: Router) { - showDialog(router, TAG) - } - +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Router +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) { + + companion object { + const val TAG = "deleting_dialog" + } + + override fun onCreateDialog(savedState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .progress(true, 0) + .content(R.string.deleting) + .build() + } + + override fun showDialog(router: Router) { + showDialog(router, TAG) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt index c3016841c..b00356a47 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt @@ -1,42 +1,42 @@ -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 - -class DownloadChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : DownloadChaptersDialog.Listener { - - constructor(target: T) : this() { - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val activity = activity!! - - val choices = intArrayOf( - R.string.download_1, - R.string.download_5, - R.string.download_10, - R.string.download_custom, - R.string.download_unread, - R.string.download_all - ).map { activity.getString(it) } - - return MaterialDialog.Builder(activity) - .negativeText(android.R.string.cancel) - .items(choices) - .itemsCallback { _, _, position, _ -> - (targetController as? Listener)?.downloadChapters(position) - } - .build() - } - - interface Listener { - fun downloadChapters(choice: Int) - } - +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 + +class DownloadChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : DownloadChaptersDialog.Listener { + + constructor(target: T) : this() { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + + val choices = intArrayOf( + R.string.download_1, + R.string.download_5, + R.string.download_10, + R.string.download_custom, + R.string.download_unread, + R.string.download_all + ).map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .negativeText(android.R.string.cancel) + .items(choices) + .itemsCallback { _, _, position, _ -> + (targetController as? Listener)?.downloadChapters(position) + } + .build() + } + + interface Listener { + fun downloadChapters(choice: Int) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt index 608742b74..56ce4affe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt @@ -1,43 +1,43 @@ -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.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.controller.DialogController - -class SetDisplayModeDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : SetDisplayModeDialog.Listener { - - private val selectedIndex = args.getInt("selected", -1) - - constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply { - putInt("selected", selectedIndex) - }) { - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val activity = activity!! - val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER) - val choices = intArrayOf(R.string.show_title, R.string.show_chapter_number) - .map { activity.getString(it) } - - return MaterialDialog.Builder(activity) - .title(R.string.action_display_mode) - .items(choices) - .itemsIds(ids) - .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> - (targetController as? Listener)?.setDisplayMode(itemView.id) - true - } - .build() - } - - interface Listener { - fun setDisplayMode(id: Int) - } - +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.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class SetDisplayModeDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : SetDisplayModeDialog.Listener { + + private val selectedIndex = args.getInt("selected", -1) + + constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply { + putInt("selected", selectedIndex) + }) { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER) + val choices = intArrayOf(R.string.show_title, R.string.show_chapter_number) + .map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .title(R.string.action_display_mode) + .items(choices) + .itemsIds(ids) + .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> + (targetController as? Listener)?.setDisplayMode(itemView.id) + true + } + .build() + } + + interface Listener { + fun setDisplayMode(id: Int) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt index c6baca5b9..861afaf1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt @@ -1,43 +1,43 @@ -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.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.controller.DialogController - -class SetSortingDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : SetSortingDialog.Listener { - - private val selectedIndex = args.getInt("selected", -1) - - constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply { - putInt("selected", selectedIndex) - }) { - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val activity = activity!! - val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER) - val choices = intArrayOf(R.string.sort_by_source, R.string.sort_by_number) - .map { activity.getString(it) } - - return MaterialDialog.Builder(activity) - .title(R.string.sorting_mode) - .items(choices) - .itemsIds(ids) - .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> - (targetController as? Listener)?.setSorting(itemView.id) - true - } - .build() - } - - interface Listener { - fun setSorting(id: Int) - } - +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.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class SetSortingDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : SetSortingDialog.Listener { + + private val selectedIndex = args.getInt("selected", -1) + + constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply { + putInt("selected", selectedIndex) + }) { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER) + val choices = intArrayOf(R.string.sort_by_source, R.string.sort_by_number) + .map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .title(R.string.sorting_mode) + .items(choices) + .itemsIds(ids) + .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> + (targetController as? Listener)?.setSorting(itemView.id) + true + } + .build() + } + + interface Listener { + fun setSorting(id: Int) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt index 8f12ed164..7e60dc30d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt @@ -1,577 +1,577 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.app.Dialog -import android.app.PendingIntent -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.net.Uri -import android.os.Build -import android.os.Bundle -import androidx.browser.customtabs.CustomTabsIntent -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import android.view.* -import android.widget.Toast -import com.afollestad.materialdialogs.MaterialDialog -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.bumptech.glide.request.target.SimpleTarget -import com.bumptech.glide.request.transition.Transition -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import com.jakewharton.rxbinding.view.clicks -import com.jakewharton.rxbinding.view.longClicks -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.data.notification.NotificationReceiver -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.controller.DialogController -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.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.util.getResourceColor -import eu.kanade.tachiyomi.util.openInBrowser -import eu.kanade.tachiyomi.util.snack -import eu.kanade.tachiyomi.util.toast -import eu.kanade.tachiyomi.util.truncateCenter -import jp.wasabeef.glide.transformations.CropSquareTransformation -import jp.wasabeef.glide.transformations.MaskTransformation -import kotlinx.android.synthetic.main.manga_info_controller.* -import uy.kohesive.injekt.injectLazy -import java.text.DateFormat -import java.text.DecimalFormat -import java.util.Date - -/** - * Fragment that shows manga information. - * Uses R.layout.manga_info_controller. - * UI related actions should be called from here. - */ -class MangaInfoController : NucleusController(), - ChangeMangaCategoriesDialog.Listener { - - /** - * Preferences helper. - */ - private val preferences: PreferencesHelper by injectLazy() - - init { - setHasOptionsMenu(true) - setOptionsMenuHidden(true) - } - - override fun createPresenter(): MangaInfoPresenter { - val ctrl = parentController as MangaController - return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.manga_info_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - // Set onclickListener to toggle favorite when FAB clicked. - fab_favorite.clicks().subscribeUntilDestroy { onFabClick() } - - // Set onLongClickListener to manage categories when FAB is clicked. - fab_favorite.longClicks().subscribeUntilDestroy{ onFabLongClick() } - - // Set SwipeRefresh to refresh manga data. - 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) { - inflater.inflate(R.menu.manga_info, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_open_in_browser -> openInBrowser() - R.id.action_open_in_web_view -> openInWebView() - R.id.action_share -> shareManga() - R.id.action_add_to_home_screen -> addToHomeScreen() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Check if manga is initialized. - * If true update view with manga information, - * if false fetch manga information - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - fun onNextManga(manga: Manga, source: Source) { - if (manga.initialized) { - // Update view. - setMangaInfo(manga, source) - - } else { - // Initialize manga. - fetchMangaFromSource() - } - } - - /** - * Update the view with manga information. - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - private fun setMangaInfo(manga: Manga, source: Source?) { - val view = view ?: return - - //update full title TextView. - manga_full_title.text = if (manga.title.isBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.title - } - - // Update artist TextView. - 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. - manga_status.setText(when (manga.status) { - SManga.ONGOING -> R.string.ongoing - SManga.COMPLETED -> R.string.completed - SManga.LICENSED -> R.string.licensed - else -> R.string.unknown - }) - - // Set the favorite drawable to the correct one. - setFavoriteDrawable(manga.favorite) - - // Set cover if it wasn't already. - if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { - GlideApp.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(manga_cover) - - if (backdrop != null) { - GlideApp.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(backdrop) - } - } - } - - override fun onDestroyView(view: View) { - manga_genres_tags.setOnTagClickListener(null) - super.onDestroyView(view) - } - - /** - * Update chapter count TextView. - * - * @param count number of chapters. - */ - fun setChapterCount(count: Float) { - if (count > 0f) { - manga_chapters?.text = DecimalFormat("#.#").format(count) - } else { - manga_chapters?.text = resources?.getString(R.string.unknown) - } - } - - fun setLastUpdateDate(date: Date) { - if (date.time != 0L) { - manga_last_update?.text = DateFormat.getDateInstance(DateFormat.SHORT).format(date) - } else { - manga_last_update?.text = resources?.getString(R.string.unknown) - } - } - - /** - * Toggles the favorite status and asks for confirmation to delete downloaded chapters. - */ - private fun toggleFavorite() { - val view = view - - val isNowFavorite = presenter.toggleFavorite() - if (view != null && !isNowFavorite && presenter.hasDownloads()) { - view.snack(view.context.getString(R.string.delete_downloads_for_manga)) { - setAction(R.string.action_delete) { - presenter.deleteDownloads() - } - } - } - } - - /** - * Open the manga in browser. - */ - private fun openInBrowser() { - val context = view?.context ?: return - val source = presenter.source as? HttpSource ?: return - - context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url.toString()) - } - - private fun openInWebView() { - val source = presenter.source as? HttpSource ?: return - - val url = try { - source.mangaDetailsRequest(presenter.manga).url.toString() - } catch (e: Exception) { - return - } - - parentController?.router?.pushController(MangaWebViewController(source.id, url) - .withFadeTransaction()) - } - - /** - * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. - */ - private fun shareManga() { - val context = view?.context ?: return - - val source = presenter.source as? HttpSource ?: return - try { - val url = source.mangaDetailsRequest(presenter.manga).url.toString() - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, url) - } - startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) - } catch (e: Exception) { - context.toast(e.message) - } - } - - /** - * Update FAB with correct drawable. - * - * @param isFavorite determines if manga is favorite or not. - */ - private fun setFavoriteDrawable(isFavorite: Boolean) { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - fab_favorite?.setImageResource(if (isFavorite) - R.drawable.ic_bookmark_white_24dp - else - R.drawable.ic_add_to_library_24dp) - } - - /** - * Start fetching manga information from source. - */ - private fun fetchMangaFromSource() { - setRefreshing(true) - // Call presenter and start fetching manga information - presenter.fetchMangaFromSource() - } - - - /** - * Update swipe refresh to stop showing refresh in progress spinner. - */ - fun onFetchMangaDone() { - setRefreshing(false) - } - - /** - * Update swipe refresh to start showing refresh in progress spinner. - */ - fun onFetchMangaError(error: Throwable) { - setRefreshing(false) - activity?.toast(error.message) - } - - /** - * Set swipe refresh status. - * - * @param value whether it should be refreshing or not. - */ - private fun setRefreshing(value: Boolean) { - swipe_refresh?.isRefreshing = value - } - - /** - * Called when the fab is clicked. - */ - private fun onFabClick() { - val manga = presenter.manga - toggleFavorite() - if (manga.favorite) { - val categories = presenter.getCategories() - val defaultCategoryId = preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId } - when { - defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory) - defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category - presenter.moveMangaToCategory(manga, null) - else -> { - val ids = presenter.getMangaCategoryIds(manga) - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } - } - activity?.toast(activity?.getString(R.string.manga_added_library)) - } else { - activity?.toast(activity?.getString(R.string.manga_removed_library)) - } - } - - /** - * Called when the fab is long clicked. - */ - private fun onFabLongClick() { - val manga = presenter.manga - if (!manga.favorite) { - toggleFavorite() - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - val categories = presenter.getCategories() - if (categories.isEmpty()) { - // no categories exist, display a message about adding categories - activity?.toast(activity?.getString(R.string.action_add_category)) - } else { - val ids = presenter.getMangaCategoryIds(manga) - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } - } - - override fun updateCategoriesForMangas(mangas: List, categories: List) { - val manga = mangas.firstOrNull() ?: return - presenter.moveMangaToCategories(manga, categories) - } - - /** - * Add a shortcut of the manga to the home screen - */ - private fun addToHomeScreen() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // TODO are transformations really unsupported or is it just the Pixel Launcher? - createShortcutForShape() - } else { - ChooseShapeDialog(this).showDialog(router) - } - } - - /** - * Dialog to choose a shape for the icon. - */ - private class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) { - - constructor(target: MangaInfoController) : this() { - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val modes = intArrayOf(R.string.circular_icon, - R.string.rounded_icon, - R.string.square_icon, - R.string.star_icon) - - return MaterialDialog.Builder(activity!!) - .title(R.string.icon_shape) - .negativeText(android.R.string.cancel) - .items(modes.map { activity?.getString(it) }) - .itemsCallback { _, _, i, _ -> - (targetController as? MangaInfoController)?.createShortcutForShape(i) - } - .build() - } - } - - /** - * Retrieves the bitmap of the shortcut with the requested shape and calls [createShortcut] when - * the resource is available. - * - * @param i The shape index to apply. Defaults to circle crop transformation. - */ - private fun createShortcutForShape(i: Int = 0) { - if (activity == null) return - GlideApp.with(activity!!) - .asBitmap() - .load(presenter.manga) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .apply { - when (i) { - 0 -> circleCrop() - 1 -> transform(RoundedCorners(5)) - 2 -> transform(CropSquareTransformation()) - 3 -> centerCrop().transform(MaskTransformation(R.drawable.mask_star)) - } - } - .into(object : SimpleTarget(96, 96) { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - createShortcut(resource) - } - - override fun onLoadFailed(errorDrawable: Drawable?) { - activity?.toast(R.string.icon_creation_fail) - } - }) - } - - /** - * 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. - * - * @param icon The image of the shortcut. - */ - private fun createShortcut(icon: Bitmap) { - val activity = activity ?: return - val mangaControllerArgs = parentController?.args ?: return - - // Create the shortcut intent. - val shortcutIntent = activity.intent - .setAction(MainActivity.SHORTCUT_MANGA) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(MangaController.MANGA_EXTRA, - mangaControllerArgs.getLong(MangaController.MANGA_EXTRA)) - - // Check if shortcut placement is supported - if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) { - val shortcutId = "manga-shortcut-${presenter.manga.title}-${presenter.source.name}" - - // Create shortcut info - val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId) - .setShortLabel(presenter.manga.title) - .setIcon(IconCompat.createWithBitmap(icon)) - .setIntent(shortcutIntent) - .build() - - val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Create the CallbackIntent. - val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo) - - // Configure the intent so that the broadcast receiver gets the callback successfully. - PendingIntent.getBroadcast(activity, 0, intent, 0) - } else { - NotificationReceiver.shortcutCreatedBroadcast(activity) - } - - // Request shortcut. - ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo, - successCallback.intentSender) - } - } - -} +package eu.kanade.tachiyomi.ui.manga.info + +import android.app.Dialog +import android.app.PendingIntent +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import android.view.* +import android.widget.Toast +import com.afollestad.materialdialogs.MaterialDialog +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.target.SimpleTarget +import com.bumptech.glide.request.transition.Transition +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import com.jakewharton.rxbinding.view.clicks +import com.jakewharton.rxbinding.view.longClicks +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.base.controller.DialogController +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.main.MainActivity +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.getResourceColor +import eu.kanade.tachiyomi.util.openInBrowser +import eu.kanade.tachiyomi.util.snack +import eu.kanade.tachiyomi.util.toast +import eu.kanade.tachiyomi.util.truncateCenter +import jp.wasabeef.glide.transformations.CropSquareTransformation +import jp.wasabeef.glide.transformations.MaskTransformation +import kotlinx.android.synthetic.main.manga_info_controller.* +import uy.kohesive.injekt.injectLazy +import java.text.DateFormat +import java.text.DecimalFormat +import java.util.Date + +/** + * Fragment that shows manga information. + * Uses R.layout.manga_info_controller. + * UI related actions should be called from here. + */ +class MangaInfoController : NucleusController(), + ChangeMangaCategoriesDialog.Listener { + + /** + * Preferences helper. + */ + private val preferences: PreferencesHelper by injectLazy() + + init { + setHasOptionsMenu(true) + setOptionsMenuHidden(true) + } + + override fun createPresenter(): MangaInfoPresenter { + val ctrl = parentController as MangaController + return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!, + ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.manga_info_controller, container, false) + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + // Set onclickListener to toggle favorite when FAB clicked. + fab_favorite.clicks().subscribeUntilDestroy { onFabClick() } + + // Set onLongClickListener to manage categories when FAB is clicked. + fab_favorite.longClicks().subscribeUntilDestroy{ onFabLongClick() } + + // Set SwipeRefresh to refresh manga data. + 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) { + inflater.inflate(R.menu.manga_info, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_open_in_browser -> openInBrowser() + R.id.action_open_in_web_view -> openInWebView() + R.id.action_share -> shareManga() + R.id.action_add_to_home_screen -> addToHomeScreen() + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Check if manga is initialized. + * If true update view with manga information, + * if false fetch manga information + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + fun onNextManga(manga: Manga, source: Source) { + if (manga.initialized) { + // Update view. + setMangaInfo(manga, source) + + } else { + // Initialize manga. + fetchMangaFromSource() + } + } + + /** + * Update the view with manga information. + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + private fun setMangaInfo(manga: Manga, source: Source?) { + val view = view ?: return + + //update full title TextView. + manga_full_title.text = if (manga.title.isBlank()) { + view.context.getString(R.string.unknown) + } else { + manga.title + } + + // Update artist TextView. + 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. + manga_status.setText(when (manga.status) { + SManga.ONGOING -> R.string.ongoing + SManga.COMPLETED -> R.string.completed + SManga.LICENSED -> R.string.licensed + else -> R.string.unknown + }) + + // Set the favorite drawable to the correct one. + setFavoriteDrawable(manga.favorite) + + // Set cover if it wasn't already. + if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { + GlideApp.with(view.context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .into(manga_cover) + + if (backdrop != null) { + GlideApp.with(view.context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .into(backdrop) + } + } + } + + override fun onDestroyView(view: View) { + manga_genres_tags.setOnTagClickListener(null) + super.onDestroyView(view) + } + + /** + * Update chapter count TextView. + * + * @param count number of chapters. + */ + fun setChapterCount(count: Float) { + if (count > 0f) { + manga_chapters?.text = DecimalFormat("#.#").format(count) + } else { + manga_chapters?.text = resources?.getString(R.string.unknown) + } + } + + fun setLastUpdateDate(date: Date) { + if (date.time != 0L) { + manga_last_update?.text = DateFormat.getDateInstance(DateFormat.SHORT).format(date) + } else { + manga_last_update?.text = resources?.getString(R.string.unknown) + } + } + + /** + * Toggles the favorite status and asks for confirmation to delete downloaded chapters. + */ + private fun toggleFavorite() { + val view = view + + val isNowFavorite = presenter.toggleFavorite() + if (view != null && !isNowFavorite && presenter.hasDownloads()) { + view.snack(view.context.getString(R.string.delete_downloads_for_manga)) { + setAction(R.string.action_delete) { + presenter.deleteDownloads() + } + } + } + } + + /** + * Open the manga in browser. + */ + private fun openInBrowser() { + val context = view?.context ?: return + val source = presenter.source as? HttpSource ?: return + + context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url.toString()) + } + + private fun openInWebView() { + val source = presenter.source as? HttpSource ?: return + + val url = try { + source.mangaDetailsRequest(presenter.manga).url.toString() + } catch (e: Exception) { + return + } + + parentController?.router?.pushController(MangaWebViewController(source.id, url) + .withFadeTransaction()) + } + + /** + * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. + */ + private fun shareManga() { + val context = view?.context ?: return + + val source = presenter.source as? HttpSource ?: return + try { + val url = source.mangaDetailsRequest(presenter.manga).url.toString() + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, url) + } + startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) + } catch (e: Exception) { + context.toast(e.message) + } + } + + /** + * Update FAB with correct drawable. + * + * @param isFavorite determines if manga is favorite or not. + */ + private fun setFavoriteDrawable(isFavorite: Boolean) { + // Set the Favorite drawable to the correct one. + // Border drawable if false, filled drawable if true. + fab_favorite?.setImageResource(if (isFavorite) + R.drawable.ic_bookmark_white_24dp + else + R.drawable.ic_add_to_library_24dp) + } + + /** + * Start fetching manga information from source. + */ + private fun fetchMangaFromSource() { + setRefreshing(true) + // Call presenter and start fetching manga information + presenter.fetchMangaFromSource() + } + + + /** + * Update swipe refresh to stop showing refresh in progress spinner. + */ + fun onFetchMangaDone() { + setRefreshing(false) + } + + /** + * Update swipe refresh to start showing refresh in progress spinner. + */ + fun onFetchMangaError(error: Throwable) { + setRefreshing(false) + activity?.toast(error.message) + } + + /** + * Set swipe refresh status. + * + * @param value whether it should be refreshing or not. + */ + private fun setRefreshing(value: Boolean) { + swipe_refresh?.isRefreshing = value + } + + /** + * Called when the fab is clicked. + */ + private fun onFabClick() { + val manga = presenter.manga + toggleFavorite() + if (manga.favorite) { + val categories = presenter.getCategories() + val defaultCategoryId = preferences.defaultCategory() + val defaultCategory = categories.find { it.id == defaultCategoryId } + when { + defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory) + defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category + presenter.moveMangaToCategory(manga, null) + else -> { + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } + } + activity?.toast(activity?.getString(R.string.manga_added_library)) + } else { + activity?.toast(activity?.getString(R.string.manga_removed_library)) + } + } + + /** + * Called when the fab is long clicked. + */ + private fun onFabLongClick() { + val manga = presenter.manga + if (!manga.favorite) { + toggleFavorite() + activity?.toast(activity?.getString(R.string.manga_added_library)) + } + val categories = presenter.getCategories() + if (categories.isEmpty()) { + // no categories exist, display a message about adding categories + activity?.toast(activity?.getString(R.string.action_add_category)) + } else { + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } + } + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + val manga = mangas.firstOrNull() ?: return + presenter.moveMangaToCategories(manga, categories) + } + + /** + * Add a shortcut of the manga to the home screen + */ + private fun addToHomeScreen() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // TODO are transformations really unsupported or is it just the Pixel Launcher? + createShortcutForShape() + } else { + ChooseShapeDialog(this).showDialog(router) + } + } + + /** + * Dialog to choose a shape for the icon. + */ + private class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) { + + constructor(target: MangaInfoController) : this() { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val modes = intArrayOf(R.string.circular_icon, + R.string.rounded_icon, + R.string.square_icon, + R.string.star_icon) + + return MaterialDialog.Builder(activity!!) + .title(R.string.icon_shape) + .negativeText(android.R.string.cancel) + .items(modes.map { activity?.getString(it) }) + .itemsCallback { _, _, i, _ -> + (targetController as? MangaInfoController)?.createShortcutForShape(i) + } + .build() + } + } + + /** + * Retrieves the bitmap of the shortcut with the requested shape and calls [createShortcut] when + * the resource is available. + * + * @param i The shape index to apply. Defaults to circle crop transformation. + */ + private fun createShortcutForShape(i: Int = 0) { + if (activity == null) return + GlideApp.with(activity!!) + .asBitmap() + .load(presenter.manga) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .apply { + when (i) { + 0 -> circleCrop() + 1 -> transform(RoundedCorners(5)) + 2 -> transform(CropSquareTransformation()) + 3 -> centerCrop().transform(MaskTransformation(R.drawable.mask_star)) + } + } + .into(object : SimpleTarget(96, 96) { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + createShortcut(resource) + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + activity?.toast(R.string.icon_creation_fail) + } + }) + } + + /** + * 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. + * + * @param icon The image of the shortcut. + */ + private fun createShortcut(icon: Bitmap) { + val activity = activity ?: return + val mangaControllerArgs = parentController?.args ?: return + + // Create the shortcut intent. + val shortcutIntent = activity.intent + .setAction(MainActivity.SHORTCUT_MANGA) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(MangaController.MANGA_EXTRA, + mangaControllerArgs.getLong(MangaController.MANGA_EXTRA)) + + // Check if shortcut placement is supported + if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) { + val shortcutId = "manga-shortcut-${presenter.manga.title}-${presenter.source.name}" + + // Create shortcut info + val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId) + .setShortLabel(presenter.manga.title) + .setIcon(IconCompat.createWithBitmap(icon)) + .setIntent(shortcutIntent) + .build() + + val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create the CallbackIntent. + val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo) + + // Configure the intent so that the broadcast receiver gets the callback successfully. + PendingIntent.getBroadcast(activity, 0, intent, 0) + } else { + NotificationReceiver.shortcutCreatedBroadcast(activity) + } + + // Request shortcut. + ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo, + successCallback.intentSender) + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt index 8b1b3731d..6bcad22e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -1,173 +1,173 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.* - -/** - * Presenter of MangaInfoFragment. - * Contains information and data for fragment. - * Observable updates should be called from here. - */ -class MangaInfoPresenter( - val manga: Manga, - val source: Source, - private val chapterCountRelay: BehaviorRelay, - private val lastUpdateRelay: BehaviorRelay, - private val mangaFavoriteRelay: PublishRelay, - private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get(), - private val coverCache: CoverCache = Injekt.get() -) : BasePresenter() { - - /** - * Subscription to send the manga to the view. - */ - private var viewMangaSubscription: Subscription? = null - - /** - * Subscription to update the manga from the source. - */ - private var fetchMangaSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - sendMangaToView() - - // Update chapter count - chapterCountRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(MangaInfoController::setChapterCount) - - // Update favorite status - mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribe { setFavorite(it) } - .apply { add(this) } - - //update last update date - lastUpdateRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(MangaInfoController::setLastUpdateDate) - } - - /** - * Sends the active manga to the view. - */ - fun sendMangaToView() { - viewMangaSubscription?.let { remove(it) } - viewMangaSubscription = Observable.just(manga) - .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) - } - - /** - * Fetch manga information from source. - */ - fun fetchMangaFromSource() { - if (!fetchMangaSubscription.isNullOrUnsubscribed()) return - fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) } - .map { networkManga -> - manga.copyFrom(networkManga) - manga.initialized = true - db.insertManga(manga).executeAsBlocking() - manga - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { sendMangaToView() } - .subscribeFirst({ view, _ -> - view.onFetchMangaDone() - }, MangaInfoController::onFetchMangaError) - } - - /** - * Update favorite status of manga, (removes / adds) manga (to / from) library. - * - * @return the new status of the manga. - */ - fun toggleFavorite(): Boolean { - manga.favorite = !manga.favorite - if (!manga.favorite) { - coverCache.deleteFromCache(manga.thumbnail_url) - } - db.insertManga(manga).executeAsBlocking() - sendMangaToView() - return manga.favorite - } - - private fun setFavorite(favorite: Boolean) { - if (manga.favorite == favorite) { - return - } - toggleFavorite() - } - - /** - * Returns true if the manga has any downloads. - */ - fun hasDownloads(): Boolean { - return downloadManager.getDownloadCount(manga) > 0 - } - - /** - * Deletes all the downloads for the manga. - */ - fun deleteDownloads() { - downloadManager.deleteManga(manga, source) - } - - /** - * Get user categories. - * - * @return List of categories, not including the default category - */ - fun getCategories(): List { - return db.getCategories().executeAsBlocking() - } - - /** - * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. - * - * @param manga the manga to get categories from. - * @return Array of category ids the manga is in, if none returns default id - */ - fun getMangaCategoryIds(manga: Manga): Array { - val categories = db.getCategoriesForManga(manga).executeAsBlocking() - return categories.mapNotNull { it.id }.toTypedArray() - } - - /** - * Move the given manga to categories. - * - * @param manga the manga to move. - * @param categories the selected categories. - */ - fun moveMangaToCategories(manga: Manga, categories: List) { - val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } - db.setMangaCategories(mc, listOf(manga)) - } - - /** - * Move the given manga to the category. - * - * @param manga the manga to move. - * @param category the selected category, or null for default category. - */ - fun moveMangaToCategory(manga: Manga, category: Category?) { - moveMangaToCategories(manga, listOfNotNull(category)) - } - -} +package eu.kanade.tachiyomi.ui.manga.info + +import android.os.Bundle +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.isNullOrUnsubscribed +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.* + +/** + * Presenter of MangaInfoFragment. + * Contains information and data for fragment. + * Observable updates should be called from here. + */ +class MangaInfoPresenter( + val manga: Manga, + val source: Source, + private val chapterCountRelay: BehaviorRelay, + private val lastUpdateRelay: BehaviorRelay, + private val mangaFavoriteRelay: PublishRelay, + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get(), + private val coverCache: CoverCache = Injekt.get() +) : BasePresenter() { + + /** + * Subscription to send the manga to the view. + */ + private var viewMangaSubscription: Subscription? = null + + /** + * Subscription to update the manga from the source. + */ + private var fetchMangaSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + sendMangaToView() + + // Update chapter count + chapterCountRelay.observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(MangaInfoController::setChapterCount) + + // Update favorite status + mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread()) + .subscribe { setFavorite(it) } + .apply { add(this) } + + //update last update date + lastUpdateRelay.observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(MangaInfoController::setLastUpdateDate) + } + + /** + * Sends the active manga to the view. + */ + fun sendMangaToView() { + viewMangaSubscription?.let { remove(it) } + viewMangaSubscription = Observable.just(manga) + .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) + } + + /** + * Fetch manga information from source. + */ + fun fetchMangaFromSource() { + if (!fetchMangaSubscription.isNullOrUnsubscribed()) return + fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) } + .map { networkManga -> + manga.copyFrom(networkManga) + manga.initialized = true + db.insertManga(manga).executeAsBlocking() + manga + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { sendMangaToView() } + .subscribeFirst({ view, _ -> + view.onFetchMangaDone() + }, MangaInfoController::onFetchMangaError) + } + + /** + * Update favorite status of manga, (removes / adds) manga (to / from) library. + * + * @return the new status of the manga. + */ + fun toggleFavorite(): Boolean { + manga.favorite = !manga.favorite + if (!manga.favorite) { + coverCache.deleteFromCache(manga.thumbnail_url) + } + db.insertManga(manga).executeAsBlocking() + sendMangaToView() + return manga.favorite + } + + private fun setFavorite(favorite: Boolean) { + if (manga.favorite == favorite) { + return + } + toggleFavorite() + } + + /** + * Returns true if the manga has any downloads. + */ + fun hasDownloads(): Boolean { + return downloadManager.getDownloadCount(manga) > 0 + } + + /** + * Deletes all the downloads for the manga. + */ + fun deleteDownloads() { + downloadManager.deleteManga(manga, source) + } + + /** + * Get user categories. + * + * @return List of categories, not including the default category + */ + fun getCategories(): List { + return db.getCategories().executeAsBlocking() + } + + /** + * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. + * + * @param manga the manga to get categories from. + * @return Array of category ids the manga is in, if none returns default id + */ + fun getMangaCategoryIds(manga: Manga): Array { + val categories = db.getCategoriesForManga(manga).executeAsBlocking() + return categories.mapNotNull { it.id }.toTypedArray() + } + + /** + * Move the given manga to categories. + * + * @param manga the manga to move. + * @param categories the selected categories. + */ + fun moveMangaToCategories(manga: Manga, categories: List) { + val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mc, listOf(manga)) + } + + /** + * Move the given manga to the category. + * + * @param manga the manga to move. + * @param category the selected category, or null for default category. + */ + fun moveMangaToCategory(manga: Manga, category: Category?) { + moveMangaToCategories(manga, listOfNotNull(category)) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt index 249d96562..59279a2ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt @@ -1,74 +1,74 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.app.Dialog -import android.os.Bundle -import android.widget.NumberPicker -import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.Controller -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class SetTrackChaptersDialog : DialogController - where T : Controller, T : SetTrackChaptersDialog.Listener { - - private val item: TrackItem - - constructor(target: T, item: TrackItem) : super(Bundle().apply { - putSerializable(KEY_ITEM_TRACK, item.track) - }) { - targetController = target - this.item = item - } - - @Suppress("unused") - constructor(bundle: Bundle) : super(bundle) { - val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track - val service = Injekt.get().getService(track.sync_id)!! - item = TrackItem(track, service) - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val item = item - - val dialog = MaterialDialog.Builder(activity!!) - .title(R.string.chapters) - .customView(R.layout.track_chapters_dialog, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { dialog, _ -> - val view = dialog.customView - if (view != null) { - // Remove focus to update selected number - val np: NumberPicker = view.findViewById(R.id.chapters_picker) - np.clearFocus() - - (targetController as? Listener)?.setChaptersRead(item, np.value) - } - } - .build() - - val view = dialog.customView - if (view != null) { - val np: NumberPicker = view.findViewById(R.id.chapters_picker) - // Set initial value - np.value = item.track?.last_chapter_read ?: 0 - // Don't allow to go from 0 to 9999 - np.wrapSelectorWheel = false - } - - return dialog - } - - interface Listener { - fun setChaptersRead(item: TrackItem, chaptersRead: Int) - } - - private companion object { - const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track" - } - +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import android.widget.NumberPicker +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SetTrackChaptersDialog : DialogController + where T : Controller, T : SetTrackChaptersDialog.Listener { + + private val item: TrackItem + + constructor(target: T, item: TrackItem) : super(Bundle().apply { + putSerializable(KEY_ITEM_TRACK, item.track) + }) { + targetController = target + this.item = item + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track + val service = Injekt.get().getService(track.sync_id)!! + item = TrackItem(track, service) + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val item = item + + val dialog = MaterialDialog.Builder(activity!!) + .title(R.string.chapters) + .customView(R.layout.track_chapters_dialog, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { dialog, _ -> + val view = dialog.customView + if (view != null) { + // Remove focus to update selected number + val np: NumberPicker = view.findViewById(R.id.chapters_picker) + np.clearFocus() + + (targetController as? Listener)?.setChaptersRead(item, np.value) + } + } + .build() + + val view = dialog.customView + if (view != null) { + val np: NumberPicker = view.findViewById(R.id.chapters_picker) + // Set initial value + np.value = item.track?.last_chapter_read ?: 0 + // Don't allow to go from 0 to 9999 + np.wrapSelectorWheel = false + } + + return dialog + } + + interface Listener { + fun setChaptersRead(item: TrackItem, chaptersRead: Int) + } + + private companion object { + const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track" + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt index 44734f64b..382a29a11 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt @@ -1,80 +1,80 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.app.Dialog -import android.os.Bundle -import android.widget.NumberPicker -import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.Controller -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class SetTrackScoreDialog : DialogController - where T : Controller, T : SetTrackScoreDialog.Listener { - - private val item: TrackItem - - constructor(target: T, item: TrackItem) : super(Bundle().apply { - putSerializable(KEY_ITEM_TRACK, item.track) - }) { - targetController = target - this.item = item - } - - @Suppress("unused") - constructor(bundle: Bundle) : super(bundle) { - val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track - val service = Injekt.get().getService(track.sync_id)!! - item = TrackItem(track, service) - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val item = item - - val dialog = MaterialDialog.Builder(activity!!) - .title(R.string.score) - .customView(R.layout.track_score_dialog, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { dialog, _ -> - val view = dialog.customView - if (view != null) { - // Remove focus to update selected number - val np: NumberPicker = view.findViewById(R.id.score_picker) - np.clearFocus() - - (targetController as? Listener)?.setScore(item, np.value) - } - } - .show() - - val view = dialog.customView - if (view != null) { - val np: NumberPicker = view.findViewById(R.id.score_picker) - val scores = item.service.getScoreList().toTypedArray() - np.maxValue = scores.size - 1 - np.displayedValues = scores - - // Set initial value - val displayedScore = item.service.displayScore(item.track!!) - if (displayedScore != "-") { - val index = scores.indexOf(displayedScore) - np.value = if (index != -1) index else 0 - } - } - - return dialog - } - - interface Listener { - fun setScore(item: TrackItem, score: Int) - } - - private companion object { - const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track" - } - +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import android.widget.NumberPicker +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SetTrackScoreDialog : DialogController + where T : Controller, T : SetTrackScoreDialog.Listener { + + private val item: TrackItem + + constructor(target: T, item: TrackItem) : super(Bundle().apply { + putSerializable(KEY_ITEM_TRACK, item.track) + }) { + targetController = target + this.item = item + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track + val service = Injekt.get().getService(track.sync_id)!! + item = TrackItem(track, service) + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val item = item + + val dialog = MaterialDialog.Builder(activity!!) + .title(R.string.score) + .customView(R.layout.track_score_dialog, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { dialog, _ -> + val view = dialog.customView + if (view != null) { + // Remove focus to update selected number + val np: NumberPicker = view.findViewById(R.id.score_picker) + np.clearFocus() + + (targetController as? Listener)?.setScore(item, np.value) + } + } + .show() + + val view = dialog.customView + if (view != null) { + val np: NumberPicker = view.findViewById(R.id.score_picker) + val scores = item.service.getScoreList().toTypedArray() + np.maxValue = scores.size - 1 + np.displayedValues = scores + + // Set initial value + val displayedScore = item.service.displayScore(item.track!!) + if (displayedScore != "-") { + val index = scores.indexOf(displayedScore) + np.value = if (index != -1) index else 0 + } + } + + return dialog + } + + interface Listener { + fun setScore(item: TrackItem, score: Int) + } + + private companion object { + const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track" + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt index 6ad057951..ad2774c62 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt @@ -1,58 +1,58 @@ -package eu.kanade.tachiyomi.ui.manga.track - -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.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class SetTrackStatusDialog : DialogController - where T : Controller, T : SetTrackStatusDialog.Listener { - - private val item: TrackItem - - constructor(target: T, item: TrackItem) : super(Bundle().apply { - putSerializable(KEY_ITEM_TRACK, item.track) - }) { - targetController = target - this.item = item - } - - @Suppress("unused") - constructor(bundle: Bundle) : super(bundle) { - val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track - val service = Injekt.get().getService(track.sync_id)!! - item = TrackItem(track, service) - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val item = item - val statusList = item.service.getStatusList().orEmpty() - val statusString = statusList.mapNotNull { item.service.getStatus(it) } - val selectedIndex = statusList.indexOf(item.track?.status) - - return MaterialDialog.Builder(activity!!) - .title(R.string.status) - .negativeText(android.R.string.cancel) - .items(statusString) - .itemsCallbackSingleChoice(selectedIndex, { _, _, i, _ -> - (targetController as? Listener)?.setStatus(item, i) - true - }) - .build() - } - - interface Listener { - fun setStatus(item: TrackItem, selection: Int) - } - - private companion object { - const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track" - } - +package eu.kanade.tachiyomi.ui.manga.track + +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.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SetTrackStatusDialog : DialogController + where T : Controller, T : SetTrackStatusDialog.Listener { + + private val item: TrackItem + + constructor(target: T, item: TrackItem) : super(Bundle().apply { + putSerializable(KEY_ITEM_TRACK, item.track) + }) { + targetController = target + this.item = item + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track + val service = Injekt.get().getService(track.sync_id)!! + item = TrackItem(track, service) + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val item = item + val statusList = item.service.getStatusList().orEmpty() + val statusString = statusList.mapNotNull { item.service.getStatus(it) } + val selectedIndex = statusList.indexOf(item.track?.status) + + return MaterialDialog.Builder(activity!!) + .title(R.string.status) + .negativeText(android.R.string.cancel) + .items(statusString) + .itemsCallbackSingleChoice(selectedIndex, { _, _, i, _ -> + (targetController as? Listener)?.setStatus(item, i) + true + }) + .build() + } + + interface Listener { + fun setStatus(item: TrackItem, selection: Int) + } + + private companion object { + const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track" + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt index d1b05cfc8..5a57a1b72 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt @@ -1,45 +1,45 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import androidx.recyclerview.widget.RecyclerView -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.inflate - -class TrackAdapter(controller: TrackController) : RecyclerView.Adapter() { - - var items = emptyList() - set(value) { - if (field !== value) { - field = value - notifyDataSetChanged() - } - } - - val rowClickListener: OnClickListener = controller - - fun getItem(index: Int): TrackItem? { - return items.getOrNull(index) - } - - override fun getItemCount(): Int { - return items.size - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder { - val view = parent.inflate(R.layout.track_item) - return TrackHolder(view, this) - } - - override fun onBindViewHolder(holder: TrackHolder, position: Int) { - holder.bind(items[position]) - } - - interface OnClickListener { - fun onLogoClick(position: Int) - fun onTitleClick(position: Int) - fun onStatusClick(position: Int) - fun onChaptersClick(position: Int) - fun onScoreClick(position: Int) - } - -} +package eu.kanade.tachiyomi.ui.manga.track + +import androidx.recyclerview.widget.RecyclerView +import android.view.ViewGroup +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.inflate + +class TrackAdapter(controller: TrackController) : RecyclerView.Adapter() { + + var items = emptyList() + set(value) { + if (field !== value) { + field = value + notifyDataSetChanged() + } + } + + val rowClickListener: OnClickListener = controller + + fun getItem(index: Int): TrackItem? { + return items.getOrNull(index) + } + + override fun getItemCount(): Int { + return items.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder { + val view = parent.inflate(R.layout.track_item) + return TrackHolder(view, this) + } + + override fun onBindViewHolder(holder: TrackHolder, position: Int) { + holder.bind(items[position]) + } + + interface OnClickListener { + fun onLogoClick(position: Int) + fun onTitleClick(position: Int) + fun onStatusClick(position: Int) + fun onChaptersClick(position: Int) + fun onScoreClick(position: Int) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt index 45fe58e8c..d42b8928f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt @@ -1,142 +1,142 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.content.Intent -import android.net.Uri -import androidx.recyclerview.widget.LinearLayoutManager -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.track_controller.* -import timber.log.Timber - -class TrackController : NucleusController(), - TrackAdapter.OnClickListener, - SetTrackStatusDialog.Listener, - SetTrackChaptersDialog.Listener, - SetTrackScoreDialog.Listener { - - private var adapter: TrackAdapter? = null - - init { - // There's no menu, but this avoids a bug when coming from the catalogue, where the menu - // disappears if the searchview is expanded - setHasOptionsMenu(true) - } - - override fun createPresenter(): TrackPresenter { - return TrackPresenter((parentController as MangaController).manga!!) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.track_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - adapter = TrackAdapter(this) - with(view) { - track_recycler.layoutManager = LinearLayoutManager(context) - track_recycler.adapter = adapter - swipe_refresh.isEnabled = false - swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() } - } - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - fun onNextTrackings(trackings: List) { - val atLeastOneLink = trackings.any { it.track != null } - adapter?.items = trackings - swipe_refresh?.isEnabled = atLeastOneLink - (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) - } - - fun onSearchResults(results: List) { - getSearchDialog()?.onSearchResults(results) - } - - @Suppress("UNUSED_PARAMETER") - fun onSearchResultsError(error: Throwable) { - Timber.e(error) - getSearchDialog()?.onSearchResultsError() - } - - private fun getSearchDialog(): TrackSearchDialog? { - return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog - } - - fun onRefreshDone() { - swipe_refresh?.isRefreshing = false - } - - fun onRefreshError(error: Throwable) { - swipe_refresh?.isRefreshing = false - activity?.toast(error.message) - } - - override fun onLogoClick(position: Int) { - val track = adapter?.getItem(position)?.track ?: return - - if (track.tracking_url.isNullOrBlank()) { - activity?.toast(R.string.url_not_set) - } else { - activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url))) - } - } - - override fun onTitleClick(position: Int) { - val item = adapter?.getItem(position) ?: return - TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER) - } - - override fun onStatusClick(position: Int) { - val item = adapter?.getItem(position) ?: return - if (item.track == null) return - - SetTrackStatusDialog(this, item).showDialog(router) - } - - override fun onChaptersClick(position: Int) { - val item = adapter?.getItem(position) ?: return - if (item.track == null) return - - SetTrackChaptersDialog(this, item).showDialog(router) - } - - override fun onScoreClick(position: Int) { - val item = adapter?.getItem(position) ?: return - if (item.track == null) return - - SetTrackScoreDialog(this, item).showDialog(router) - } - - override fun setStatus(item: TrackItem, selection: Int) { - presenter.setStatus(item, selection) - swipe_refresh?.isRefreshing = true - } - - override fun setScore(item: TrackItem, score: Int) { - presenter.setScore(item, score) - swipe_refresh?.isRefreshing = true - } - - override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { - presenter.setLastChapterRead(item, chaptersRead) - swipe_refresh?.isRefreshing = true - } - - private companion object { - const val TAG_SEARCH_CONTROLLER = "track_search_controller" - } - -} +package eu.kanade.tachiyomi.ui.manga.track + +import android.content.Intent +import android.net.Uri +import androidx.recyclerview.widget.LinearLayoutManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.track_controller.* +import timber.log.Timber + +class TrackController : NucleusController(), + TrackAdapter.OnClickListener, + SetTrackStatusDialog.Listener, + SetTrackChaptersDialog.Listener, + SetTrackScoreDialog.Listener { + + private var adapter: TrackAdapter? = null + + init { + // There's no menu, but this avoids a bug when coming from the catalogue, where the menu + // disappears if the searchview is expanded + setHasOptionsMenu(true) + } + + override fun createPresenter(): TrackPresenter { + return TrackPresenter((parentController as MangaController).manga!!) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.track_controller, container, false) + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + adapter = TrackAdapter(this) + with(view) { + track_recycler.layoutManager = LinearLayoutManager(context) + track_recycler.adapter = adapter + swipe_refresh.isEnabled = false + swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() } + } + } + + override fun onDestroyView(view: View) { + adapter = null + super.onDestroyView(view) + } + + fun onNextTrackings(trackings: List) { + val atLeastOneLink = trackings.any { it.track != null } + adapter?.items = trackings + swipe_refresh?.isEnabled = atLeastOneLink + (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) + } + + fun onSearchResults(results: List) { + getSearchDialog()?.onSearchResults(results) + } + + @Suppress("UNUSED_PARAMETER") + fun onSearchResultsError(error: Throwable) { + Timber.e(error) + getSearchDialog()?.onSearchResultsError() + } + + private fun getSearchDialog(): TrackSearchDialog? { + return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog + } + + fun onRefreshDone() { + swipe_refresh?.isRefreshing = false + } + + fun onRefreshError(error: Throwable) { + swipe_refresh?.isRefreshing = false + activity?.toast(error.message) + } + + override fun onLogoClick(position: Int) { + val track = adapter?.getItem(position)?.track ?: return + + if (track.tracking_url.isNullOrBlank()) { + activity?.toast(R.string.url_not_set) + } else { + activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url))) + } + } + + override fun onTitleClick(position: Int) { + val item = adapter?.getItem(position) ?: return + TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER) + } + + override fun onStatusClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackStatusDialog(this, item).showDialog(router) + } + + override fun onChaptersClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackChaptersDialog(this, item).showDialog(router) + } + + override fun onScoreClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackScoreDialog(this, item).showDialog(router) + } + + override fun setStatus(item: TrackItem, selection: Int) { + presenter.setStatus(item, selection) + swipe_refresh?.isRefreshing = true + } + + override fun setScore(item: TrackItem, score: Int) { + presenter.setScore(item, score) + swipe_refresh?.isRefreshing = true + } + + override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { + presenter.setLastChapterRead(item, chaptersRead) + swipe_refresh?.isRefreshing = true + } + + private companion object { + const val TAG_SEARCH_CONTROLLER = "track_search_controller" + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt index 2f018f19d..4a62c430b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt @@ -1,42 +1,42 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.annotation.SuppressLint -import android.view.View -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder -import kotlinx.android.synthetic.main.track_item.* - -class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { - - init { - val listener = adapter.rowClickListener - logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) } - title_container.setOnClickListener { listener.onTitleClick(adapterPosition) } - status_container.setOnClickListener { listener.onStatusClick(adapterPosition) } - chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) } - score_container.setOnClickListener { listener.onScoreClick(adapterPosition) } - } - - @SuppressLint("SetTextI18n") - @Suppress("DEPRECATION") - fun bind(item: TrackItem) { - val track = item.track - track_logo.setImageResource(item.service.getLogo()) - logo_container.setBackgroundColor(item.service.getLogoColor()) - if (track != null) { - track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary) - track_title.setAllCaps(false) - track_title.text = track.title - track_chapters.text = "${track.last_chapter_read}/" + - if (track.total_chapters > 0) track.total_chapters else "-" - track_status.text = item.service.getStatus(track.status) - track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) - } else { - track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button) - track_title.setText(R.string.action_edit) - track_chapters.text = "" - track_score.text = "" - track_status.text = "" - } - } -} +package eu.kanade.tachiyomi.ui.manga.track + +import android.annotation.SuppressLint +import android.view.View +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder +import kotlinx.android.synthetic.main.track_item.* + +class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { + + init { + val listener = adapter.rowClickListener + logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) } + title_container.setOnClickListener { listener.onTitleClick(adapterPosition) } + status_container.setOnClickListener { listener.onStatusClick(adapterPosition) } + chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) } + score_container.setOnClickListener { listener.onScoreClick(adapterPosition) } + } + + @SuppressLint("SetTextI18n") + @Suppress("DEPRECATION") + fun bind(item: TrackItem) { + val track = item.track + track_logo.setImageResource(item.service.getLogo()) + logo_container.setBackgroundColor(item.service.getLogoColor()) + if (track != null) { + track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary) + track_title.setAllCaps(false) + track_title.text = track.title + track_chapters.text = "${track.last_chapter_read}/" + + if (track.total_chapters > 0) track.total_chapters else "-" + track_status.text = item.service.getStatus(track.status) + track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) + } else { + track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button) + track_title.setText(R.string.action_edit) + track_chapters.text = "" + track_score.text = "" + track_status.text = "" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt index 6e7c3ebec..a751434d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt @@ -1,6 +1,6 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackService - -data class TrackItem(val track: Track?, val service: TrackService) +package eu.kanade.tachiyomi.ui.manga.track + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackService + +data class TrackItem(val track: Track?, val service: TrackService) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt index ac8592ed9..e33f92c3c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt @@ -1,130 +1,130 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.os.Bundle -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.toast -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - - -class TrackPresenter( - val manga: Manga, - preferences: PreferencesHelper = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val trackManager: TrackManager = Injekt.get() -) : BasePresenter() { - - private val context = preferences.context - - private var trackList: List = emptyList() - - private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } - - private var trackSubscription: Subscription? = null - - private var searchSubscription: Subscription? = null - - private var refreshSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - fetchTrackings() - } - - fun fetchTrackings() { - trackSubscription?.let { remove(it) } - trackSubscription = db.getTracks(manga) - .asRxObservable() - .map { tracks -> - loggedServices.map { service -> - TrackItem(tracks.find { it.sync_id == service.id }, service) - } - } - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { trackList = it } - .subscribeLatestCache(TrackController::onNextTrackings) - } - - fun refresh() { - refreshSubscription?.let { remove(it) } - refreshSubscription = Observable.from(trackList) - .filter { it.track != null } - .concatMap { item -> - item.service.refresh(item.track!!) - .flatMap { db.insertTrack(it).asRxObservable() } - .map { item } - .onErrorReturn { item } - } - .toList() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> view.onRefreshDone() }, - TrackController::onRefreshError) - } - - fun search(query: String, service: TrackService) { - searchSubscription?.let { remove(it) } - searchSubscription = service.search(query) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(TrackController::onSearchResults, - TrackController::onSearchResultsError) - } - - fun registerTracking(item: Track?, service: TrackService) { - if (item != null) { - item.manga_id = manga.id!! - add(service.bind(item) - .flatMap { db.insertTrack(item).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ }, - { error -> context.toast(error.message) })) - } else { - db.deleteTrackForManga(manga, service).executeAsBlocking() - } - } - - private fun updateRemote(track: Track, service: TrackService) { - service.update(track) - .flatMap { db.insertTrack(track).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> view.onRefreshDone() }, - { view, error -> - view.onRefreshError(error) - - // Restart on error to set old values - fetchTrackings() - }) - } - - fun setStatus(item: TrackItem, index: Int) { - val track = item.track!! - track.status = item.service.getStatusList()[index] - updateRemote(track, item.service) - } - - fun setScore(item: TrackItem, index: Int) { - val track = item.track!! - track.score = item.service.indexToScore(index) - updateRemote(track, item.service) - } - - fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { - val track = item.track!! - track.last_chapter_read = chapterNumber - updateRemote(track, item.service) - } - +package eu.kanade.tachiyomi.ui.manga.track + +import android.os.Bundle +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.toast +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + + +class TrackPresenter( + val manga: Manga, + preferences: PreferencesHelper = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val trackManager: TrackManager = Injekt.get() +) : BasePresenter() { + + private val context = preferences.context + + private var trackList: List = emptyList() + + private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } + + private var trackSubscription: Subscription? = null + + private var searchSubscription: Subscription? = null + + private var refreshSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + fetchTrackings() + } + + fun fetchTrackings() { + trackSubscription?.let { remove(it) } + trackSubscription = db.getTracks(manga) + .asRxObservable() + .map { tracks -> + loggedServices.map { service -> + TrackItem(tracks.find { it.sync_id == service.id }, service) + } + } + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { trackList = it } + .subscribeLatestCache(TrackController::onNextTrackings) + } + + fun refresh() { + refreshSubscription?.let { remove(it) } + refreshSubscription = Observable.from(trackList) + .filter { it.track != null } + .concatMap { item -> + item.service.refresh(item.track!!) + .flatMap { db.insertTrack(it).asRxObservable() } + .map { item } + .onErrorReturn { item } + } + .toList() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, _ -> view.onRefreshDone() }, + TrackController::onRefreshError) + } + + fun search(query: String, service: TrackService) { + searchSubscription?.let { remove(it) } + searchSubscription = service.search(query) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(TrackController::onSearchResults, + TrackController::onSearchResultsError) + } + + fun registerTracking(item: Track?, service: TrackService) { + if (item != null) { + item.manga_id = manga.id!! + add(service.bind(item) + .flatMap { db.insertTrack(item).asRxObservable() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ }, + { error -> context.toast(error.message) })) + } else { + db.deleteTrackForManga(manga, service).executeAsBlocking() + } + } + + private fun updateRemote(track: Track, service: TrackService) { + service.update(track) + .flatMap { db.insertTrack(track).asRxObservable() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, _ -> view.onRefreshDone() }, + { view, error -> + view.onRefreshError(error) + + // Restart on error to set old values + fetchTrackings() + }) + } + + fun setStatus(item: TrackItem, index: Int) { + val track = item.track!! + track.status = item.service.getStatusList()[index] + updateRemote(track, item.service) + } + + fun setScore(item: TrackItem, index: Int) { + val track = item.track!! + track.score = item.service.indexToScore(index) + updateRemote(track, item.service) + } + + fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { + val track = item.track!! + track.last_chapter_read = chapterNumber + updateRemote(track, item.service) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt index c9b3f3265..930651270 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt @@ -1,79 +1,79 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import com.bumptech.glide.load.engine.DiskCacheStrategy -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.util.gone -import eu.kanade.tachiyomi.util.inflate -import kotlinx.android.synthetic.main.track_search_item.view.* -import java.util.* - -class TrackSearchAdapter(context: Context) - : ArrayAdapter(context, R.layout.track_search_item, ArrayList()) { - - override fun getView(position: Int, view: View?, parent: ViewGroup): View { - var v = view - // Get the data item for this position - val track = getItem(position) - // Check if an existing view is being reused, otherwise inflate the view - val holder: TrackSearchHolder // view lookup cache stored in tag - if (v == null) { - v = parent.inflate(R.layout.track_search_item) - holder = TrackSearchHolder(v) - v.tag = holder - } else { - holder = v.tag as TrackSearchHolder - } - holder.onSetValues(track) - return v - } - - fun setItems(syncs: List) { - setNotifyOnChange(false) - clear() - addAll(syncs) - notifyDataSetChanged() - } - - class TrackSearchHolder(private val view: View) { - - fun onSetValues(track: TrackSearch) { - view.track_search_title.text = track.title - view.track_search_summary.text = track.summary - GlideApp.with(view.context).clear(view.track_search_cover) - if (!track.cover_url.isNullOrEmpty()) { - GlideApp.with(view.context) - .load(track.cover_url) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(view.track_search_cover) - } - - if (track.publishing_status.isNullOrBlank()) { - view.track_search_status.gone() - view.track_search_status_result.gone() - } else { - view.track_search_status_result.text = track.publishing_status.capitalize() - } - - if (track.publishing_type.isNullOrBlank()) { - view.track_search_type.gone() - view.track_search_type_result.gone() - } else { - view.track_search_type_result.text = track.publishing_type.capitalize() - } - - if (track.start_date.isNullOrBlank()) { - view.track_search_start.gone() - view.track_search_start_result.gone() - } else { - view.track_search_start_result.text = track.start_date - } - } - } +package eu.kanade.tachiyomi.ui.manga.track + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.util.gone +import eu.kanade.tachiyomi.util.inflate +import kotlinx.android.synthetic.main.track_search_item.view.* +import java.util.* + +class TrackSearchAdapter(context: Context) + : ArrayAdapter(context, R.layout.track_search_item, ArrayList()) { + + override fun getView(position: Int, view: View?, parent: ViewGroup): View { + var v = view + // Get the data item for this position + val track = getItem(position) + // Check if an existing view is being reused, otherwise inflate the view + val holder: TrackSearchHolder // view lookup cache stored in tag + if (v == null) { + v = parent.inflate(R.layout.track_search_item) + holder = TrackSearchHolder(v) + v.tag = holder + } else { + holder = v.tag as TrackSearchHolder + } + holder.onSetValues(track) + return v + } + + fun setItems(syncs: List) { + setNotifyOnChange(false) + clear() + addAll(syncs) + notifyDataSetChanged() + } + + class TrackSearchHolder(private val view: View) { + + fun onSetValues(track: TrackSearch) { + view.track_search_title.text = track.title + view.track_search_summary.text = track.summary + GlideApp.with(view.context).clear(view.track_search_cover) + if (!track.cover_url.isNullOrEmpty()) { + GlideApp.with(view.context) + .load(track.cover_url) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .into(view.track_search_cover) + } + + if (track.publishing_status.isNullOrBlank()) { + view.track_search_status.gone() + view.track_search_status_result.gone() + } else { + view.track_search_status_result.text = track.publishing_status.capitalize() + } + + if (track.publishing_type.isNullOrBlank()) { + view.track_search_type.gone() + view.track_search_type_result.gone() + } else { + view.track_search_type_result.text = track.publishing_type.capitalize() + } + + if (track.start_date.isNullOrBlank()) { + view.track_search_start.gone() + view.track_search_start_result.gone() + } else { + view.track_search_start_result.text = track.start_date + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt index 9856ce2e5..215ef00b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt @@ -1,144 +1,144 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.app.Dialog -import android.os.Bundle -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog -import com.jakewharton.rxbinding.widget.itemClicks -import com.jakewharton.rxbinding.widget.textChanges -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.util.plusAssign -import kotlinx.android.synthetic.main.track_search_dialog.view.* -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.subscriptions.CompositeSubscription -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.concurrent.TimeUnit - -class TrackSearchDialog : DialogController { - - private var dialogView: View? = null - - private var adapter: TrackSearchAdapter? = null - - private var selectedItem: Track? = null - - private val service: TrackService - - private var subscriptions = CompositeSubscription() - - private var searchTextSubscription: Subscription? = null - - private val trackController - get() = targetController as TrackController - - constructor(target: TrackController, service: TrackService) : super(Bundle().apply { - putInt(KEY_SERVICE, service.id) - }) { - targetController = target - this.service = service - } - - @Suppress("unused") - constructor(bundle: Bundle) : super(bundle) { - service = Injekt.get().getService(bundle.getInt(KEY_SERVICE))!! - } - - override fun onCreateDialog(savedState: Bundle?): Dialog { - val dialog = MaterialDialog.Builder(activity!!) - .customView(R.layout.track_search_dialog, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { _, _ -> onPositiveButtonClick() } - .build() - - if (subscriptions.isUnsubscribed) { - subscriptions = CompositeSubscription() - } - - dialogView = dialog.view - onViewCreated(dialog.view, savedState) - - return dialog - } - - fun onViewCreated(view: View, savedState: Bundle?) { - // Create adapter - val adapter = TrackSearchAdapter(view.context) - this.adapter = adapter - view.track_search_list.adapter = adapter - - // Set listeners - selectedItem = null - - subscriptions += view.track_search_list.itemClicks().subscribe { position -> - selectedItem = adapter.getItem(position) - } - - // Do an initial search based on the manga's title - if (savedState == null) { - val title = trackController.presenter.manga.title - view.track_search.append(title) - search(title) - } - } - - override fun onDestroyView(view: View) { - super.onDestroyView(view) - subscriptions.unsubscribe() - dialogView = null - adapter = null - } - - override fun onAttach(view: View) { - super.onAttach(view) - searchTextSubscription = dialogView!!.track_search.textChanges() - .skip(1) - .debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread()) - .map { it.toString() } - .filter(String::isNotBlank) - .subscribe { search(it) } - } - - override fun onDetach(view: View) { - super.onDetach(view) - searchTextSubscription?.unsubscribe() - } - - private fun search(query: String) { - val view = dialogView ?: return - view.progress.visibility = View.VISIBLE - view.track_search_list.visibility = View.INVISIBLE - trackController.presenter.search(query, service) - } - - fun onSearchResults(results: List) { - selectedItem = null - val view = dialogView ?: return - view.progress.visibility = View.INVISIBLE - view.track_search_list.visibility = View.VISIBLE - adapter?.setItems(results) - } - - fun onSearchResultsError() { - val view = dialogView ?: return - view.progress.visibility = View.VISIBLE - view.track_search_list.visibility = View.INVISIBLE - adapter?.setItems(emptyList()) - } - - private fun onPositiveButtonClick() { - trackController.presenter.registerTracking(selectedItem, service) - } - - private companion object { - const val KEY_SERVICE = "service_id" - } - -} +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import com.afollestad.materialdialogs.MaterialDialog +import com.jakewharton.rxbinding.widget.itemClicks +import com.jakewharton.rxbinding.widget.textChanges +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.util.plusAssign +import kotlinx.android.synthetic.main.track_search_dialog.view.* +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.subscriptions.CompositeSubscription +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.TimeUnit + +class TrackSearchDialog : DialogController { + + private var dialogView: View? = null + + private var adapter: TrackSearchAdapter? = null + + private var selectedItem: Track? = null + + private val service: TrackService + + private var subscriptions = CompositeSubscription() + + private var searchTextSubscription: Subscription? = null + + private val trackController + get() = targetController as TrackController + + constructor(target: TrackController, service: TrackService) : super(Bundle().apply { + putInt(KEY_SERVICE, service.id) + }) { + targetController = target + this.service = service + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + service = Injekt.get().getService(bundle.getInt(KEY_SERVICE))!! + } + + override fun onCreateDialog(savedState: Bundle?): Dialog { + val dialog = MaterialDialog.Builder(activity!!) + .customView(R.layout.track_search_dialog, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { _, _ -> onPositiveButtonClick() } + .build() + + if (subscriptions.isUnsubscribed) { + subscriptions = CompositeSubscription() + } + + dialogView = dialog.view + onViewCreated(dialog.view, savedState) + + return dialog + } + + fun onViewCreated(view: View, savedState: Bundle?) { + // Create adapter + val adapter = TrackSearchAdapter(view.context) + this.adapter = adapter + view.track_search_list.adapter = adapter + + // Set listeners + selectedItem = null + + subscriptions += view.track_search_list.itemClicks().subscribe { position -> + selectedItem = adapter.getItem(position) + } + + // Do an initial search based on the manga's title + if (savedState == null) { + val title = trackController.presenter.manga.title + view.track_search.append(title) + search(title) + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + subscriptions.unsubscribe() + dialogView = null + adapter = null + } + + override fun onAttach(view: View) { + super.onAttach(view) + searchTextSubscription = dialogView!!.track_search.textChanges() + .skip(1) + .debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread()) + .map { it.toString() } + .filter(String::isNotBlank) + .subscribe { search(it) } + } + + override fun onDetach(view: View) { + super.onDetach(view) + searchTextSubscription?.unsubscribe() + } + + private fun search(query: String) { + val view = dialogView ?: return + view.progress.visibility = View.VISIBLE + view.track_search_list.visibility = View.INVISIBLE + trackController.presenter.search(query, service) + } + + fun onSearchResults(results: List) { + selectedItem = null + val view = dialogView ?: return + view.progress.visibility = View.INVISIBLE + view.track_search_list.visibility = View.VISIBLE + adapter?.setItems(results) + } + + fun onSearchResultsError() { + val view = dialogView ?: return + view.progress.visibility = View.VISIBLE + view.track_search_list.visibility = View.INVISIBLE + adapter?.setItems(emptyList()) + } + + private fun onPositiveButtonClick() { + trackController.presenter.registerTracking(selectedItem, service) + } + + private companion object { + const val KEY_SERVICE = "service_id" + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt index 51f6c365b..26b166af5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt @@ -1,333 +1,333 @@ -package eu.kanade.tachiyomi.ui.recent_updates - -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import android.view.* -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController -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 eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.recent_chapters_controller.* -import timber.log.Timber - -/** - * Fragment that shows recent chapters. - * Uses [R.layout.recent_chapters_controller]. - * UI related actions should be called from here. - */ -class RecentChaptersController : NucleusController(), - NoToolbarElevationController, - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - FlexibleAdapter.OnUpdateListener, - ConfirmDeleteChaptersDialog.Listener, - RecentChaptersAdapter.OnCoverClickListener { - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - /** - * Adapter containing the recent chapters. - */ - var adapter: RecentChaptersAdapter? = null - private set - - override fun getTitle(): String? { - return resources?.getString(R.string.label_recent_updates) - } - - override fun createPresenter(): RecentChaptersPresenter { - return RecentChaptersPresenter() - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.recent_chapters_controller, container, false) - } - - /** - * Called when view is created - * @param view created view - */ - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - // Init RecyclerView and adapter - val layoutManager = LinearLayoutManager(view.context) - recycler.layoutManager = layoutManager - recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - recycler.setHasFixedSize(true) - adapter = RecentChaptersAdapter(this@RecentChaptersController) - recycler.adapter = adapter - - recycler.scrollStateChanges().subscribeUntilDestroy { - // Disable swipe refresh when view is not at the top - val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition() - swipe_refresh.isEnabled = firstPos <= 0 - } - - swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt()) - swipe_refresh.refreshes().subscribeUntilDestroy { - if (!LibraryUpdateService.isRunning(view.context)) { - LibraryUpdateService.start(view.context) - view.context.toast(R.string.action_update_library) - } - // It can be a very long operation, so we disable swipe refresh and show a toast. - swipe_refresh.isRefreshing = false - } - } - - override fun onDestroyView(view: View) { - adapter = null - actionMode = null - super.onDestroyView(view) - } - - /** - * Returns selected chapters - * @return list of selected chapters - */ - fun getSelectedChapters(): List { - val adapter = adapter ?: return emptyList() - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem } - } - - /** - * Called when item in list is clicked - * @param position position of clicked item - */ - override fun onItemClick(view: View, position: Int): Boolean { - val adapter = adapter ?: return false - - // Get item from position - val item = adapter.getItem(position) as? RecentChapterItem ?: return false - if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { - toggleSelection(position) - return true - } else { - openChapter(item) - return false - } - } - - /** - * Called when item in list is long clicked - * @param position position of clicked item - */ - override fun onItemLongClick(position: Int) { - if (actionMode == null) - actionMode = (activity as AppCompatActivity).startSupportActionMode(this) - - toggleSelection(position) - } - - /** - * Called to toggle selection - * @param position position of selected item - */ - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - adapter.toggleSelection(position) - actionMode?.invalidate() - } - - /** - * Open chapter in reader - * @param chapter selected chapter - */ - private fun openChapter(item: RecentChapterItem) { - val activity = activity ?: return - val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter) - startActivity(intent) - } - - /** - * Download selected items - * @param chapters list of selected [RecentChapter]s - */ - fun downloadChapters(chapters: List) { - destroyActionModeIfNeeded() - presenter.downloadChapters(chapters) - } - - /** - * Populate adapter with chapters - * @param chapters list of [Any] - */ - fun onNextRecentChapters(chapters: List>) { - destroyActionModeIfNeeded() - adapter?.updateDataSet(chapters) - } - - override fun onUpdateEmptyView(size: Int) { - if (size > 0) { - empty_view?.hide() - } else { - empty_view?.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent) - } - } - - /** - * Update download status of chapter - * @param download [Download] object containing download progress. - */ - fun onChapterStatusChange(download: Download) { - getHolder(download)?.notifyStatus(download.status) - } - - /** - * Returns holder belonging to chapter - * @param download [Download] object containing download progress. - */ - private fun getHolder(download: Download): RecentChapterHolder? { - return recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder - } - - /** - * Mark chapter as read - * @param chapters list of chapters - */ - fun markAsRead(chapters: List) { - presenter.markChapterRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) - } - } - - override fun deleteChapters(chaptersToDelete: List) { - destroyActionModeIfNeeded() - DeletingChaptersDialog().showDialog(router) - presenter.deleteChapters(chaptersToDelete) - } - - /** - * Destory [ActionMode] if it's shown - */ - fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - /** - * Mark chapter as unread - * @param chapters list of selected [RecentChapter] - */ - fun markAsUnread(chapters: List) { - presenter.markChapterRead(chapters, false) - } - - /** - * Start downloading chapter - * @param chapter selected chapter with manga - */ - fun downloadChapter(chapter: RecentChapterItem) { - presenter.downloadChapters(listOf(chapter)) - } - - /** - * Start deleting chapter - * @param chapter selected chapter with manga - */ - fun deleteChapter(chapter: RecentChapterItem) { - DeletingChaptersDialog().showDialog(router) - presenter.deleteChapters(listOf(chapter)) - } - - override fun onCoverClick(position: Int) { - val chapterClicked = adapter?.getItem(position) as? RecentChapterItem ?: return - openManga(chapterClicked) - - } - - fun openManga(chapter: RecentChapterItem) { - router.pushController(MangaController(chapter.manga).withFadeTransaction()) - } - - /** - * Called when chapters are deleted - */ - fun onChaptersDeleted() { - dismissDeletingDialog() - adapter?.notifyDataSetChanged() - } - - /** - * Called when error while deleting - * @param error error message - */ - fun onChaptersDeletedError(error: Throwable) { - dismissDeletingDialog() - Timber.e(error) - } - - /** - * Called to dismiss deleting dialog - */ - fun dismissDeletingDialog() { - router.popControllerWithTag(DeletingChaptersDialog.TAG) - } - - /** - * Called when ActionMode created. - * @param mode the ActionMode object - * @param menu menu object of ActionMode - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu) - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = adapter?.selectedItemCount ?: 0 - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = resources?.getString(R.string.label_selected, count) - } - return false - } - - /** - * Called when ActionMode item clicked - * @param mode the ActionMode object - * @param item item from ActionMode. - */ - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> ConfirmDeleteChaptersDialog(this, getSelectedChapters()) - .showDialog(router) - else -> return false - } - return true - } - - /** - * Called when ActionMode destroyed - * @param mode the ActionMode object - */ - override fun onDestroyActionMode(mode: ActionMode?) { - adapter?.mode = SelectableAdapter.Mode.IDLE - adapter?.clearSelection() - actionMode = null - } - -} +package eu.kanade.tachiyomi.ui.recent_updates + +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import android.view.* +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController +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 eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.recent_chapters_controller.* +import timber.log.Timber + +/** + * Fragment that shows recent chapters. + * Uses [R.layout.recent_chapters_controller]. + * UI related actions should be called from here. + */ +class RecentChaptersController : NucleusController(), + NoToolbarElevationController, + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + FlexibleAdapter.OnUpdateListener, + ConfirmDeleteChaptersDialog.Listener, + RecentChaptersAdapter.OnCoverClickListener { + + /** + * Action mode for multiple selection. + */ + private var actionMode: ActionMode? = null + + /** + * Adapter containing the recent chapters. + */ + var adapter: RecentChaptersAdapter? = null + private set + + override fun getTitle(): String? { + return resources?.getString(R.string.label_recent_updates) + } + + override fun createPresenter(): RecentChaptersPresenter { + return RecentChaptersPresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.recent_chapters_controller, container, false) + } + + /** + * Called when view is created + * @param view created view + */ + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + // Init RecyclerView and adapter + val layoutManager = LinearLayoutManager(view.context) + recycler.layoutManager = layoutManager + recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + recycler.setHasFixedSize(true) + adapter = RecentChaptersAdapter(this@RecentChaptersController) + recycler.adapter = adapter + + recycler.scrollStateChanges().subscribeUntilDestroy { + // Disable swipe refresh when view is not at the top + val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition() + swipe_refresh.isEnabled = firstPos <= 0 + } + + swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt()) + swipe_refresh.refreshes().subscribeUntilDestroy { + if (!LibraryUpdateService.isRunning(view.context)) { + LibraryUpdateService.start(view.context) + view.context.toast(R.string.action_update_library) + } + // It can be a very long operation, so we disable swipe refresh and show a toast. + swipe_refresh.isRefreshing = false + } + } + + override fun onDestroyView(view: View) { + adapter = null + actionMode = null + super.onDestroyView(view) + } + + /** + * Returns selected chapters + * @return list of selected chapters + */ + fun getSelectedChapters(): List { + val adapter = adapter ?: return emptyList() + return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem } + } + + /** + * Called when item in list is clicked + * @param position position of clicked item + */ + override fun onItemClick(view: View, position: Int): Boolean { + val adapter = adapter ?: return false + + // Get item from position + val item = adapter.getItem(position) as? RecentChapterItem ?: return false + if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { + toggleSelection(position) + return true + } else { + openChapter(item) + return false + } + } + + /** + * Called when item in list is long clicked + * @param position position of clicked item + */ + override fun onItemLongClick(position: Int) { + if (actionMode == null) + actionMode = (activity as AppCompatActivity).startSupportActionMode(this) + + toggleSelection(position) + } + + /** + * Called to toggle selection + * @param position position of selected item + */ + private fun toggleSelection(position: Int) { + val adapter = adapter ?: return + adapter.toggleSelection(position) + actionMode?.invalidate() + } + + /** + * Open chapter in reader + * @param chapter selected chapter + */ + private fun openChapter(item: RecentChapterItem) { + val activity = activity ?: return + val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter) + startActivity(intent) + } + + /** + * Download selected items + * @param chapters list of selected [RecentChapter]s + */ + fun downloadChapters(chapters: List) { + destroyActionModeIfNeeded() + presenter.downloadChapters(chapters) + } + + /** + * Populate adapter with chapters + * @param chapters list of [Any] + */ + fun onNextRecentChapters(chapters: List>) { + destroyActionModeIfNeeded() + adapter?.updateDataSet(chapters) + } + + override fun onUpdateEmptyView(size: Int) { + if (size > 0) { + empty_view?.hide() + } else { + empty_view?.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent) + } + } + + /** + * Update download status of chapter + * @param download [Download] object containing download progress. + */ + fun onChapterStatusChange(download: Download) { + getHolder(download)?.notifyStatus(download.status) + } + + /** + * Returns holder belonging to chapter + * @param download [Download] object containing download progress. + */ + private fun getHolder(download: Download): RecentChapterHolder? { + return recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder + } + + /** + * Mark chapter as read + * @param chapters list of chapters + */ + fun markAsRead(chapters: List) { + presenter.markChapterRead(chapters, true) + if (presenter.preferences.removeAfterMarkedAsRead()) { + deleteChapters(chapters) + } + } + + override fun deleteChapters(chaptersToDelete: List) { + destroyActionModeIfNeeded() + DeletingChaptersDialog().showDialog(router) + presenter.deleteChapters(chaptersToDelete) + } + + /** + * Destory [ActionMode] if it's shown + */ + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + /** + * Mark chapter as unread + * @param chapters list of selected [RecentChapter] + */ + fun markAsUnread(chapters: List) { + presenter.markChapterRead(chapters, false) + } + + /** + * Start downloading chapter + * @param chapter selected chapter with manga + */ + fun downloadChapter(chapter: RecentChapterItem) { + presenter.downloadChapters(listOf(chapter)) + } + + /** + * Start deleting chapter + * @param chapter selected chapter with manga + */ + fun deleteChapter(chapter: RecentChapterItem) { + DeletingChaptersDialog().showDialog(router) + presenter.deleteChapters(listOf(chapter)) + } + + override fun onCoverClick(position: Int) { + val chapterClicked = adapter?.getItem(position) as? RecentChapterItem ?: return + openManga(chapterClicked) + + } + + fun openManga(chapter: RecentChapterItem) { + router.pushController(MangaController(chapter.manga).withFadeTransaction()) + } + + /** + * Called when chapters are deleted + */ + fun onChaptersDeleted() { + dismissDeletingDialog() + adapter?.notifyDataSetChanged() + } + + /** + * Called when error while deleting + * @param error error message + */ + fun onChaptersDeletedError(error: Throwable) { + dismissDeletingDialog() + Timber.e(error) + } + + /** + * Called to dismiss deleting dialog + */ + fun dismissDeletingDialog() { + router.popControllerWithTag(DeletingChaptersDialog.TAG) + } + + /** + * Called when ActionMode created. + * @param mode the ActionMode object + * @param menu menu object of ActionMode + */ + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu) + adapter?.mode = SelectableAdapter.Mode.MULTI + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = adapter?.selectedItemCount ?: 0 + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = resources?.getString(R.string.label_selected, count) + } + return false + } + + /** + * Called when ActionMode item clicked + * @param mode the ActionMode object + * @param item item from ActionMode. + */ + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) + R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) + R.id.action_download -> downloadChapters(getSelectedChapters()) + R.id.action_delete -> ConfirmDeleteChaptersDialog(this, getSelectedChapters()) + .showDialog(router) + else -> return false + } + return true + } + + /** + * Called when ActionMode destroyed + * @param mode the ActionMode object + */ + override fun onDestroyActionMode(mode: ActionMode?) { + adapter?.mode = SelectableAdapter.Mode.IDLE + adapter?.clearSelection() + actionMode = null + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt index 45f97eebb..6725ebb8f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt @@ -1,87 +1,87 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.content.Context -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.preference.PreferenceController -import androidx.preference.PreferenceScreen -import android.util.TypedValue -import android.view.ContextThemeWrapper -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.bluelinelabs.conductor.ControllerChangeHandler -import com.bluelinelabs.conductor.ControllerChangeType -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.ui.base.controller.BaseController -import rx.Observable -import rx.Subscription -import rx.subscriptions.CompositeSubscription -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -abstract class SettingsController : PreferenceController() { - - val preferences: PreferencesHelper = Injekt.get() - - var untilDestroySubscriptions = CompositeSubscription() - private set - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View { - if (untilDestroySubscriptions.isUnsubscribed) { - untilDestroySubscriptions = CompositeSubscription() - } - return super.onCreateView(inflater, container, savedInstanceState) - } - - override fun onDestroyView(view: View) { - super.onDestroyView(view) - untilDestroySubscriptions.unsubscribe() - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - val screen = preferenceManager.createPreferenceScreen(getThemedContext()) - preferenceScreen = screen - setupPreferenceScreen(screen) - } - - abstract fun setupPreferenceScreen(screen: PreferenceScreen): Any? - - private fun getThemedContext(): Context { - val tv = TypedValue() - activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true) - return ContextThemeWrapper(activity, tv.resourceId) - } - - open fun getTitle(): String? { - return preferenceScreen?.title?.toString() - } - - 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() - } - - override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { - if (type.isEnter) { - setTitle() - } - super.onChangeStarted(handler, type) - } - - fun Observable.subscribeUntilDestroy(): Subscription { - return subscribe().also { untilDestroySubscriptions.add(it) } - } - - fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription { - return subscribe(onNext).also { untilDestroySubscriptions.add(it) } - } -} +package eu.kanade.tachiyomi.ui.setting + +import android.content.Context +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceController +import androidx.preference.PreferenceScreen +import android.util.TypedValue +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.base.controller.BaseController +import rx.Observable +import rx.Subscription +import rx.subscriptions.CompositeSubscription +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +abstract class SettingsController : PreferenceController() { + + val preferences: PreferencesHelper = Injekt.get() + + var untilDestroySubscriptions = CompositeSubscription() + private set + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View { + if (untilDestroySubscriptions.isUnsubscribed) { + untilDestroySubscriptions = CompositeSubscription() + } + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + untilDestroySubscriptions.unsubscribe() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val screen = preferenceManager.createPreferenceScreen(getThemedContext()) + preferenceScreen = screen + setupPreferenceScreen(screen) + } + + abstract fun setupPreferenceScreen(screen: PreferenceScreen): Any? + + private fun getThemedContext(): Context { + val tv = TypedValue() + activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true) + return ContextThemeWrapper(activity, tv.resourceId) + } + + open fun getTitle(): String? { + return preferenceScreen?.title?.toString() + } + + 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() + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + if (type.isEnter) { + setTitle() + } + super.onChangeStarted(handler, type) + } + + fun Observable.subscribeUntilDestroy(): Subscription { + return subscribe().also { untilDestroySubscriptions.add(it) } + } + + fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription { + return subscribe(onNext).also { untilDestroySubscriptions.add(it) } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index 65e98e8a3..f465fe22b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -1,61 +1,61 @@ -package eu.kanade.tachiyomi.ui.setting - -import androidx.preference.PreferenceScreen -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.util.getResourceColor - -class SettingsMainController : SettingsController() { - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.label_settings - - val tintColor = context.getResourceColor(R.attr.colorAccent) - - preference { - iconRes = R.drawable.ic_tune_black_24dp - iconTint = tintColor - titleRes = R.string.pref_category_general - onClick { navigateTo(SettingsGeneralController()) } - } - preference { - iconRes = R.drawable.ic_chrome_reader_mode_black_24dp - iconTint = tintColor - titleRes = R.string.pref_category_reader - onClick { navigateTo(SettingsReaderController()) } - } - preference { - iconRes = R.drawable.ic_file_download_black_24dp - iconTint = tintColor - titleRes = R.string.pref_category_downloads - onClick { navigateTo(SettingsDownloadController()) } - } - preference { - iconRes = R.drawable.ic_sync_black_24dp - iconTint = tintColor - titleRes = R.string.pref_category_tracking - onClick { navigateTo(SettingsTrackingController()) } - } - preference { - iconRes = R.drawable.ic_backup_black_24dp - iconTint = tintColor - titleRes = R.string.backup - onClick { navigateTo(SettingsBackupController()) } - } - preference { - iconRes = R.drawable.ic_code_black_24dp - iconTint = tintColor - titleRes = R.string.pref_category_advanced - onClick { navigateTo(SettingsAdvancedController()) } - } - preference { - iconRes = R.drawable.ic_help_black_24dp - iconTint = tintColor - titleRes = R.string.pref_category_about - onClick { navigateTo(SettingsAboutController()) } - } - } - - private fun navigateTo(controller: SettingsController) { - router.pushController(controller.withFadeTransaction()) - } -} +package eu.kanade.tachiyomi.ui.setting + +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.util.getResourceColor + +class SettingsMainController : SettingsController() { + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.label_settings + + val tintColor = context.getResourceColor(R.attr.colorAccent) + + preference { + iconRes = R.drawable.ic_tune_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_general + onClick { navigateTo(SettingsGeneralController()) } + } + preference { + iconRes = R.drawable.ic_chrome_reader_mode_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_reader + onClick { navigateTo(SettingsReaderController()) } + } + preference { + iconRes = R.drawable.ic_file_download_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_downloads + onClick { navigateTo(SettingsDownloadController()) } + } + preference { + iconRes = R.drawable.ic_sync_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_tracking + onClick { navigateTo(SettingsTrackingController()) } + } + preference { + iconRes = R.drawable.ic_backup_black_24dp + iconTint = tintColor + titleRes = R.string.backup + onClick { navigateTo(SettingsBackupController()) } + } + preference { + iconRes = R.drawable.ic_code_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_advanced + onClick { navigateTo(SettingsAdvancedController()) } + } + preference { + iconRes = R.drawable.ic_help_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_about + onClick { navigateTo(SettingsAboutController()) } + } + } + + private fun navigateTo(controller: SettingsController) { + router.pushController(controller.withFadeTransaction()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt index db8c73ee2..de0e05790 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt @@ -1,239 +1,239 @@ -package eu.kanade.tachiyomi.widget - -import android.content.Context -import android.graphics.drawable.Drawable -import androidx.annotation.CallSuper -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import android.util.AttributeSet -import android.view.View -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.getResourceColor - -/** - * An alternative implementation of [android.support.design.widget.NavigationView], without menu - * inflation and allowing customizable items (multiple selections, custom views, etc). - */ -open class ExtendedNavigationView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0) - : SimpleNavigationView(context, attrs, defStyleAttr) { - - /** - * Every item of the nav view. Generic items must belong to this list, custom items could be - * implemented by an abstract class. If more customization is needed in the future, this can be - * changed to an interface instead of sealed class. - */ - sealed class Item { - /** - * A view separator. - */ - class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item() - - /** - * A header with a title. - */ - class Header(val resTitle: Int) : Item() - - /** - * A checkbox. - */ - open class Checkbox(val resTitle: Int, var checked: Boolean = false) : Item() - - /** - * A checkbox belonging to a group. The group must handle selections and restrictions. - */ - class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false) - : Checkbox(resTitle, checked), GroupedItem - - /** - * A radio belonging to a group (a sole radio makes no sense). The group must handle - * selections and restrictions. - */ - class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false) - : Item(), GroupedItem - - /** - * An item with which needs more than two states (selected/deselected). - */ - abstract class MultiState(val resTitle: Int, var state: Int = 0) : Item() { - - /** - * Returns the drawable associated to every possible each state. - */ - abstract fun getStateDrawable(context: Context): Drawable? - - /** - * Creates a vector tinted with the accent color. - * - * @param context any context. - * @param resId the vector resource to load and tint - */ - fun tintVector(context: Context, resId: Int): Drawable { - return VectorDrawableCompat.create(context.resources, resId, context.theme)!!.apply { - setTint(context.getResourceColor(R.attr.colorAccent)) - } - } - } - - /** - * An item with which needs more than two states (selected/deselected) belonging to a group. - * The group must handle selections and restrictions. - */ - abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0) - : MultiState(resTitle, state), GroupedItem - - /** - * A multistate item for sorting lists (unselected, ascending, descending). - */ - class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) { - - companion object { - const val SORT_NONE = 0 - const val SORT_ASC = 1 - const val SORT_DESC = 2 - } - - override fun getStateDrawable(context: Context): Drawable? { - return when (state) { - SORT_ASC -> tintVector(context, R.drawable.ic_arrow_up_white_32dp) - SORT_DESC -> tintVector(context, R.drawable.ic_arrow_down_white_32dp) - SORT_NONE -> ContextCompat.getDrawable(context, R.drawable.empty_drawable_32dp) - else -> null - } - } - - } - } - - /** - * Interface for an item belonging to a group. - */ - interface GroupedItem { - val group: Group - } - - /** - * A group containing a list of items. - */ - interface Group { - - /** - * An optional header for the group, typically a [Item.Header]. - */ - val header: Item? - - /** - * An optional footer for the group, typically a [Item.Separator]. - */ - val footer: Item? - - /** - * The items of the group, excluding header and footer. - */ - val items: List - - /** - * Creates all the elements of this group. Implementations can override this method for more - * customization. - */ - fun createItems() = (mutableListOf() + header + items + footer).filterNotNull() - - /** - * Called after creating the list of items. Implementations should load the current values - * into the models. - */ - fun initModels() - - /** - * Called when an item of this group is clicked. The group is responsible for all the - * selections of its items. - */ - fun onItemClicked(item: Item) - - } - - /** - * Base adapter for the navigation view. It knows how to create and render every subclass of - * [Item]. - */ - abstract inner class Adapter(private val items: List) : RecyclerView.Adapter() { - - private val onClick = View.OnClickListener { - val pos = recycler.getChildAdapterPosition(it) - val item = items[pos] - onItemClicked(item) - } - - fun notifyItemChanged(item: Item) { - val pos = items.indexOf(item) - if (pos != -1) notifyItemChanged(pos) - } - - override fun getItemCount(): Int { - return items.size - } - - @CallSuper - override fun getItemViewType(position: Int): Int { - val item = items[position] - return when (item) { - is Item.Header -> VIEW_TYPE_HEADER - is Item.Separator -> VIEW_TYPE_SEPARATOR - is Item.Radio -> VIEW_TYPE_RADIO - is Item.Checkbox -> VIEW_TYPE_CHECKBOX - is Item.MultiState -> VIEW_TYPE_MULTISTATE - } - } - - @CallSuper - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return when (viewType) { - VIEW_TYPE_HEADER -> HeaderHolder(parent) - VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent) - VIEW_TYPE_RADIO -> RadioHolder(parent, onClick) - VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick) - VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick) - else -> throw Exception("Unknown view type") - } - } - - @CallSuper - override fun onBindViewHolder(holder: Holder, position: Int) { - when (holder) { - is HeaderHolder -> { - val item = items[position] as Item.Header - holder.title.setText(item.resTitle) - } - is SeparatorHolder -> { - val view = holder.itemView - val item = items[position] as Item.Separator - view.setPadding(0, item.paddingTop, 0, item.paddingBottom) - } - is RadioHolder -> { - val item = items[position] as Item.Radio - holder.radio.setText(item.resTitle) - holder.radio.isChecked = item.checked - } - is CheckboxHolder -> { - val item = items[position] as Item.CheckboxGroup - holder.check.setText(item.resTitle) - holder.check.isChecked = item.checked - } - is MultiStateHolder -> { - val item = items[position] as Item.MultiStateGroup - val drawable = item.getStateDrawable(context) - holder.text.setText(item.resTitle) - holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) - } - } - } - - abstract fun onItemClicked(item: Item) - - } - -} +package eu.kanade.tachiyomi.widget + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.annotation.CallSuper +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceColor + +/** + * An alternative implementation of [android.support.design.widget.NavigationView], without menu + * inflation and allowing customizable items (multiple selections, custom views, etc). + */ +open class ExtendedNavigationView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : SimpleNavigationView(context, attrs, defStyleAttr) { + + /** + * Every item of the nav view. Generic items must belong to this list, custom items could be + * implemented by an abstract class. If more customization is needed in the future, this can be + * changed to an interface instead of sealed class. + */ + sealed class Item { + /** + * A view separator. + */ + class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item() + + /** + * A header with a title. + */ + class Header(val resTitle: Int) : Item() + + /** + * A checkbox. + */ + open class Checkbox(val resTitle: Int, var checked: Boolean = false) : Item() + + /** + * A checkbox belonging to a group. The group must handle selections and restrictions. + */ + class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false) + : Checkbox(resTitle, checked), GroupedItem + + /** + * A radio belonging to a group (a sole radio makes no sense). The group must handle + * selections and restrictions. + */ + class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false) + : Item(), GroupedItem + + /** + * An item with which needs more than two states (selected/deselected). + */ + abstract class MultiState(val resTitle: Int, var state: Int = 0) : Item() { + + /** + * Returns the drawable associated to every possible each state. + */ + abstract fun getStateDrawable(context: Context): Drawable? + + /** + * Creates a vector tinted with the accent color. + * + * @param context any context. + * @param resId the vector resource to load and tint + */ + fun tintVector(context: Context, resId: Int): Drawable { + return VectorDrawableCompat.create(context.resources, resId, context.theme)!!.apply { + setTint(context.getResourceColor(R.attr.colorAccent)) + } + } + } + + /** + * An item with which needs more than two states (selected/deselected) belonging to a group. + * The group must handle selections and restrictions. + */ + abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0) + : MultiState(resTitle, state), GroupedItem + + /** + * A multistate item for sorting lists (unselected, ascending, descending). + */ + class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) { + + companion object { + const val SORT_NONE = 0 + const val SORT_ASC = 1 + const val SORT_DESC = 2 + } + + override fun getStateDrawable(context: Context): Drawable? { + return when (state) { + SORT_ASC -> tintVector(context, R.drawable.ic_arrow_up_white_32dp) + SORT_DESC -> tintVector(context, R.drawable.ic_arrow_down_white_32dp) + SORT_NONE -> ContextCompat.getDrawable(context, R.drawable.empty_drawable_32dp) + else -> null + } + } + + } + } + + /** + * Interface for an item belonging to a group. + */ + interface GroupedItem { + val group: Group + } + + /** + * A group containing a list of items. + */ + interface Group { + + /** + * An optional header for the group, typically a [Item.Header]. + */ + val header: Item? + + /** + * An optional footer for the group, typically a [Item.Separator]. + */ + val footer: Item? + + /** + * The items of the group, excluding header and footer. + */ + val items: List + + /** + * Creates all the elements of this group. Implementations can override this method for more + * customization. + */ + fun createItems() = (mutableListOf() + header + items + footer).filterNotNull() + + /** + * Called after creating the list of items. Implementations should load the current values + * into the models. + */ + fun initModels() + + /** + * Called when an item of this group is clicked. The group is responsible for all the + * selections of its items. + */ + fun onItemClicked(item: Item) + + } + + /** + * Base adapter for the navigation view. It knows how to create and render every subclass of + * [Item]. + */ + abstract inner class Adapter(private val items: List) : RecyclerView.Adapter() { + + private val onClick = View.OnClickListener { + val pos = recycler.getChildAdapterPosition(it) + val item = items[pos] + onItemClicked(item) + } + + fun notifyItemChanged(item: Item) { + val pos = items.indexOf(item) + if (pos != -1) notifyItemChanged(pos) + } + + override fun getItemCount(): Int { + return items.size + } + + @CallSuper + override fun getItemViewType(position: Int): Int { + val item = items[position] + return when (item) { + is Item.Header -> VIEW_TYPE_HEADER + is Item.Separator -> VIEW_TYPE_SEPARATOR + is Item.Radio -> VIEW_TYPE_RADIO + is Item.Checkbox -> VIEW_TYPE_CHECKBOX + is Item.MultiState -> VIEW_TYPE_MULTISTATE + } + } + + @CallSuper + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + return when (viewType) { + VIEW_TYPE_HEADER -> HeaderHolder(parent) + VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent) + VIEW_TYPE_RADIO -> RadioHolder(parent, onClick) + VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick) + VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick) + else -> throw Exception("Unknown view type") + } + } + + @CallSuper + override fun onBindViewHolder(holder: Holder, position: Int) { + when (holder) { + is HeaderHolder -> { + val item = items[position] as Item.Header + holder.title.setText(item.resTitle) + } + is SeparatorHolder -> { + val view = holder.itemView + val item = items[position] as Item.Separator + view.setPadding(0, item.paddingTop, 0, item.paddingBottom) + } + is RadioHolder -> { + val item = items[position] as Item.Radio + holder.radio.setText(item.resTitle) + holder.radio.isChecked = item.checked + } + is CheckboxHolder -> { + val item = items[position] as Item.CheckboxGroup + holder.check.setText(item.resTitle) + holder.check.isChecked = item.checked + } + is MultiStateHolder -> { + val item = items[position] as Item.MultiStateGroup + val drawable = item.getStateDrawable(context) + holder.text.setText(item.resTitle) + holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) + } + } + } + + abstract fun onItemClicked(item: Item) + + } + +} diff --git a/app/src/main/res/drawable/empty_drawable_32dp.xml b/app/src/main/res/drawable/empty_drawable_32dp.xml index de7699cab..09b50315d 100644 --- a/app/src/main/res/drawable/empty_drawable_32dp.xml +++ b/app/src/main/res/drawable/empty_drawable_32dp.xml @@ -1,8 +1,8 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_done_white_18dp.xml b/app/src/main/res/drawable/ic_done_white_18dp.xml index 3bd793040..3e9103eb0 100644 --- a/app/src/main/res/drawable/ic_done_white_18dp.xml +++ b/app/src/main/res/drawable/ic_done_white_18dp.xml @@ -1,9 +1,9 @@ - - - + + + diff --git a/app/src/main/res/drawable/ic_watch_later_black_24dp.xml b/app/src/main/res/drawable/ic_watch_later_black_24dp.xml index 6032098bd..e9f85c873 100644 --- a/app/src/main/res/drawable/ic_watch_later_black_24dp.xml +++ b/app/src/main/res/drawable/ic_watch_later_black_24dp.xml @@ -1,9 +1,9 @@ - - - + + + diff --git a/app/src/main/res/layout/navigation_view_checkbox.xml b/app/src/main/res/layout/navigation_view_checkbox.xml index 18a345f58..ecc547fc5 100644 --- a/app/src/main/res/layout/navigation_view_checkbox.xml +++ b/app/src/main/res/layout/navigation_view_checkbox.xml @@ -1,23 +1,23 @@ - - - - - - + + + + + + diff --git a/app/src/main/res/layout/navigation_view_group.xml b/app/src/main/res/layout/navigation_view_group.xml index 10b43e851..d3399c50f 100644 --- a/app/src/main/res/layout/navigation_view_group.xml +++ b/app/src/main/res/layout/navigation_view_group.xml @@ -1,30 +1,30 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pref_item_source.xml b/app/src/main/res/layout/pref_item_source.xml index 27ff9b02e..88680c72e 100644 --- a/app/src/main/res/layout/pref_item_source.xml +++ b/app/src/main/res/layout/pref_item_source.xml @@ -1,62 +1,62 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/track_item.xml b/app/src/main/res/layout/track_item.xml index 27cc0d5ea..3dff2a779 100644 --- a/app/src/main/res/layout/track_item.xml +++ b/app/src/main/res/layout/track_item.xml @@ -1,191 +1,191 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From faedd325be78ebb1775c22436c17c5d3e780c320 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 5 Jan 2020 15:53:17 -0500 Subject: [PATCH 009/675] Remove unnecessary legacy-support-v4 dependency --- app/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0a1866f6c..91153d261 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -108,7 +108,6 @@ dependencies { implementation 'com.github.inorichi:junrar-android:634c1f5' // Android support library - implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'com.google.android.material:material:1.0.0' From df14e6d43e9cff6b8210cddb977affc8443f5dd9 Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 5 Jan 2020 16:36:23 -0500 Subject: [PATCH 010/675] fix DOWNLOADED text showing after chapters are marked as read (#2434) * fix DOWNLOADED text showing after chapters are marked as read --- .../tachiyomi/ui/manga/chapter/ChaptersController.kt | 10 +++++++--- .../tachiyomi/ui/manga/chapter/ChaptersPresenter.kt | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt index 315985106..a6fe7cddf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt @@ -5,12 +5,12 @@ import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint import android.app.Activity import android.content.Intent -import com.google.android.material.snackbar.Snackbar +import android.view.* import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager -import android.view.* +import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding.support.v4.widget.refreshes import com.jakewharton.rxbinding.view.clicks import eu.davidea.flexibleadapter.FlexibleAdapter @@ -404,8 +404,12 @@ class ChaptersController : NucleusController(), presenter.deleteChapters(chapters) } - fun onChaptersDeleted() { + fun onChaptersDeleted(chapters: List) { dismissDeletingDialog() + //this is needed so the downloaded text gets removed from the item + chapters.forEach { + adapter?.updateItem(it) + } adapter?.notifyDataSetChanged() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index b04369c34..271a58605 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -278,7 +278,7 @@ class ChaptersPresenter( .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeFirst({ view, _ -> - view.onChaptersDeleted() + view.onChaptersDeleted(chapters) }, ChaptersController::onChaptersDeletedError) } From 708525ef9dbeeeadeacd6952527382d40bb63178 Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 5 Jan 2020 17:59:05 -0500 Subject: [PATCH 011/675] match transition text used by other readers (#2439) --- .../ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index f5948b8f6..2ef3acb51 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon import android.graphics.Typeface -import androidx.appcompat.widget.AppCompatButton -import androidx.appcompat.widget.AppCompatTextView import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.StyleSpan @@ -12,6 +10,8 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView +import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.AppCompatTextView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter @@ -36,7 +36,10 @@ class WebtoonTransitionHolder( /** * Text view used to display the text of the current and next/prev chapters. */ - private var textView = TextView(context) + private var textView = TextView(context).apply { + textSize = 17.5F + wrapContent() + } /** * View container of the current status of the transition page. Child views will be added From b3f1714ba9ca804e849b3b8f50ee6a873458bb18 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 5 Jan 2020 19:39:25 -0500 Subject: [PATCH 012/675] Convert remaining Java files (#2435) --- .../ui/base/controller/DialogController.java | 139 ------------------ .../ui/base/controller/DialogController.kt | 118 +++++++++++++++ .../ui/base/controller/NucleusController.kt | 2 +- .../presenter/NucleusConductorDelegate.java | 61 -------- .../presenter/NucleusConductorDelegate.kt | 45 ++++++ .../NucleusConductorLifecycleListener.java | 44 ------ .../NucleusConductorLifecycleListener.kt | 33 +++++ .../tachiyomi/ui/reader/ReaderActivity.kt | 2 +- .../java/eu/kanade/tachiyomi/util/GLUtil.java | 54 ------- .../java/eu/kanade/tachiyomi/util/GLUtil.kt | 54 +++++++ 10 files changed, 252 insertions(+), 300 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/GLUtil.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/GLUtil.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.java deleted file mode 100644 index fa4b62846..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.java +++ /dev/null @@ -1,139 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.controller; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.bluelinelabs.conductor.RestoreViewOnCreateController; -import com.bluelinelabs.conductor.Router; -import com.bluelinelabs.conductor.RouterTransaction; -import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler; - -/** - * A controller that displays a dialog window, floating on top of its activity's window. - * This is a wrapper over {@link Dialog} object like {@link android.app.DialogFragment}. - * - *

Implementations should override this class and implement {@link #onCreateDialog(Bundle)} to create a custom dialog, such as an {@link android.app.AlertDialog} - */ -public abstract class DialogController extends RestoreViewOnCreateController { - - private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState"; - - private Dialog dialog; - private boolean dismissed; - - /** - * Convenience constructor for use when no arguments are needed. - */ - protected DialogController() { - super(null); - } - - /** - * Constructor that takes arguments that need to be retained across restarts. - * - * @param args Any arguments that need to be retained. - */ - protected DialogController(@Nullable Bundle args) { - super(args); - } - - @NonNull - @Override - final protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) { - dialog = onCreateDialog(savedViewState); - //noinspection ConstantConditions - dialog.setOwnerActivity(getActivity()); - dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - dismissDialog(); - } - }); - if (savedViewState != null) { - Bundle dialogState = savedViewState.getBundle(SAVED_DIALOG_STATE_TAG); - if (dialogState != null) { - dialog.onRestoreInstanceState(dialogState); - } - } - return new View(getActivity());//stub view - } - - @Override - protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { - super.onSaveViewState(view, outState); - Bundle dialogState = dialog.onSaveInstanceState(); - outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); - } - - @Override - protected void onAttach(@NonNull View view) { - super.onAttach(view); - dialog.show(); - } - - @Override - protected void onDetach(@NonNull View view) { - super.onDetach(view); - dialog.hide(); - } - - @Override - protected void onDestroyView(@NonNull View view) { - super.onDestroyView(view); - dialog.setOnDismissListener(null); - dialog.dismiss(); - dialog = null; - } - - /** - * Display the dialog, create a transaction and pushing the controller. - * @param router The router on which the transaction will be applied - */ - public void showDialog(@NonNull Router router) { - showDialog(router, null); - } - - /** - * Display the dialog, create a transaction and pushing the controller. - * @param router The router on which the transaction will be applied - * @param tag The tag for this controller - */ - public void showDialog(@NonNull Router router, @Nullable String tag) { - dismissed = false; - router.pushController(RouterTransaction.with(this) - .pushChangeHandler(new SimpleSwapChangeHandler(false)) - .popChangeHandler(new SimpleSwapChangeHandler(false)) - .tag(tag)); - } - - /** - * Dismiss the dialog and pop this controller - */ - public void dismissDialog() { - if (dismissed) { - return; - } - getRouter().popController(this); - dismissed = true; - } - - @Nullable - protected Dialog getDialog() { - return dialog; - } - - /** - * Build your own custom Dialog container such as an {@link android.app.AlertDialog} - * - * @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)} or {@code null} if no saved state exists. - * @return Return a new Dialog instance to be displayed by the Controller - */ - @NonNull - protected abstract Dialog onCreateDialog(@Nullable Bundle savedViewState); -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt new file mode 100644 index 000000000..7d59b864c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt @@ -0,0 +1,118 @@ +package eu.kanade.tachiyomi.ui.base.controller + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.bluelinelabs.conductor.RestoreViewOnCreateController +import com.bluelinelabs.conductor.Router +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler + +/** + * A controller that displays a dialog window, floating on top of its activity's window. + * This is a wrapper over [Dialog] object like [android.app.DialogFragment]. + * + * + * Implementations should override this class and implement [.onCreateDialog] to create a custom dialog, such as an [android.app.AlertDialog] + */ +abstract class DialogController : RestoreViewOnCreateController { + + protected var dialog: Dialog? = null + private set + + private var dismissed = false + + /** + * Convenience constructor for use when no arguments are needed. + */ + protected constructor() : super(null) + + /** + * Constructor that takes arguments that need to be retained across restarts. + * + * @param args Any arguments that need to be retained. + */ + protected constructor(args: Bundle?) : super(args) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { + dialog = onCreateDialog(savedViewState) + dialog!!.ownerActivity = activity + dialog!!.setOnDismissListener { dismissDialog() } + if (savedViewState != null) { + val dialogState = savedViewState.getBundle(SAVED_DIALOG_STATE_TAG) + if (dialogState != null) { + dialog!!.onRestoreInstanceState(dialogState) + } + } + return View(activity) //stub view + } + + override fun onSaveViewState(view: View, outState: Bundle) { + super.onSaveViewState(view, outState) + val dialogState = dialog!!.onSaveInstanceState() + outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState) + } + + override fun onAttach(view: View) { + super.onAttach(view) + dialog!!.show() + } + + override fun onDetach(view: View) { + super.onDetach(view) + dialog!!.hide() + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + dialog!!.setOnDismissListener(null) + dialog!!.dismiss() + dialog = null + } + + /** + * Display the dialog, create a transaction and pushing the controller. + * @param router The router on which the transaction will be applied + */ + open fun showDialog(router: Router) { + showDialog(router, null) + } + + /** + * Display the dialog, create a transaction and pushing the controller. + * @param router The router on which the transaction will be applied + * @param tag The tag for this controller + */ + fun showDialog(router: Router, tag: String?) { + dismissed = false + router.pushController(RouterTransaction.with(this) + .pushChangeHandler(SimpleSwapChangeHandler(false)) + .popChangeHandler(SimpleSwapChangeHandler(false)) + .tag(tag)) + } + + /** + * Dismiss the dialog and pop this controller + */ + fun dismissDialog() { + if (dismissed) { + return + } + router.popController(this) + dismissed = true + } + + /** + * Build your own custom Dialog container such as an [android.app.AlertDialog] + * + * @param savedViewState A bundle for the view's state, which would have been created in [.onSaveViewState] or `null` if no saved state exists. + * @return Return a new Dialog instance to be displayed by the Controller + */ + protected abstract fun onCreateDialog(savedViewState: Bundle?): Dialog + + companion object { + private const val SAVED_DIALOG_STATE_TAG = "android:savedDialogState" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt index 4df8dbd3f..5a9250d87 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt @@ -13,7 +13,7 @@ abstract class NucleusController

>(val bundle: Bundle? = null) : private val delegate = NucleusConductorDelegate(this) val presenter: P - get() = delegate.presenter + get() = delegate.presenter!! init { addLifecycleListener(NucleusConductorLifecycleListener(delegate)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java deleted file mode 100644 index 46034fbf8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java +++ /dev/null @@ -1,61 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.presenter; - -import android.os.Bundle; -import androidx.annotation.Nullable; - -import nucleus.factory.PresenterFactory; -import nucleus.presenter.Presenter; - -public class NucleusConductorDelegate

{ - - @Nullable private P presenter; - @Nullable private Bundle bundle; - - private PresenterFactory

factory; - - public NucleusConductorDelegate(PresenterFactory

creator) { - this.factory = creator; - } - - public P getPresenter() { - if (presenter == null) { - presenter = factory.createPresenter(); - presenter.create(bundle); - bundle = null; - } - return presenter; - } - - Bundle onSaveInstanceState() { - Bundle bundle = new Bundle(); -// getPresenter(); // Workaround a crash related to saving instance state with child routers - if (presenter != null) { - presenter.save(bundle); - } - return bundle; - } - - void onRestoreInstanceState(Bundle presenterState) { - bundle = presenterState; - } - - void onTakeView(Object view) { - getPresenter(); - if (presenter != null) { - //noinspection unchecked - presenter.takeView(view); - } - } - - void onDropView() { - if (presenter != null) { - presenter.dropView(); - } - } - - void onDestroy() { - if (presenter != null) { - presenter.destroy(); - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt new file mode 100644 index 000000000..02ad4f966 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.ui.base.presenter + +import android.os.Bundle +import nucleus.factory.PresenterFactory +import nucleus.presenter.Presenter + +class NucleusConductorDelegate

>(private val factory: PresenterFactory

) { + + var presenter: P? = null + get() { + if (field == null) { + field = factory.createPresenter() + field!!.create(bundle) + bundle = null + } + return field + } + + private var bundle: Bundle? = null + + fun onSaveInstanceState(): Bundle { + val bundle = Bundle() + // getPresenter(); // Workaround a crash related to saving instance state with child routers + presenter?.save(bundle) + return bundle + } + + fun onRestoreInstanceState(presenterState: Bundle?) { + bundle = presenterState + } + + @Suppress("TYPE_MISMATCH") + fun onTakeView(view: Any) { + presenter?.takeView(view) + } + + fun onDropView() { + presenter?.dropView() + } + + fun onDestroy() { + presenter?.destroy() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java deleted file mode 100644 index 90d94e5d4..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java +++ /dev/null @@ -1,44 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.presenter; - -import android.os.Bundle; -import androidx.annotation.NonNull; -import android.view.View; - -import com.bluelinelabs.conductor.Controller; - -public class NucleusConductorLifecycleListener extends Controller.LifecycleListener { - - private static final String PRESENTER_STATE_KEY = "presenter_state"; - - private NucleusConductorDelegate delegate; - - public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) { - this.delegate = delegate; - } - - @Override - public void postCreateView(@NonNull Controller controller, @NonNull View view) { - delegate.onTakeView(controller); - } - - @Override - public void preDestroyView(@NonNull Controller controller, @NonNull View view) { - delegate.onDropView(); - } - - @Override - public void preDestroy(@NonNull Controller controller) { - delegate.onDestroy(); - } - - @Override - public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) { - outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState()); - } - - @Override - public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) { - delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY)); - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt new file mode 100644 index 000000000..0e2502033 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.ui.base.presenter + +import android.os.Bundle +import android.view.View +import com.bluelinelabs.conductor.Controller + +class NucleusConductorLifecycleListener(private val delegate: NucleusConductorDelegate<*>) : Controller.LifecycleListener() { + + override fun postCreateView(controller: Controller, view: View) { + delegate.onTakeView(controller) + } + + override fun preDestroyView(controller: Controller, view: View) { + delegate.onDropView() + } + + override fun preDestroy(controller: Controller) { + delegate.onDestroy() + } + + override fun onSaveInstanceState(controller: Controller, outState: Bundle) { + outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState()) + } + + override fun onRestoreInstanceState(controller: Controller, savedInstanceState: Bundle) { + delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY)) + } + + companion object { + private const val PRESENTER_STATE_KEY = "presenter_state" + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index beb7783ff..c245f3375 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -62,7 +62,7 @@ class ReaderActivity : BaseRxActivity() { /** * The maximum bitmap size supported by the device. */ - val maxBitmapSize by lazy { GLUtil.getMaxTextureSize() } + val maxBitmapSize by lazy { GLUtil.maxTextureSize } /** * Viewer used to display the pages (pager, webtoon, ...). diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/GLUtil.java b/app/src/main/java/eu/kanade/tachiyomi/util/GLUtil.java deleted file mode 100644 index f60376524..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/util/GLUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -package eu.kanade.tachiyomi.util; - -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; - -public final class GLUtil { - - private GLUtil() throws InstantiationException { - throw new InstantiationException("This class is not for instantiation"); - } - - public static int getMaxTextureSize() { - // Safe minimum default size - final int IMAGE_MAX_BITMAP_DIMENSION = 2048; - - // Get EGL Display - EGL10 egl = (EGL10) EGLContext.getEGL(); - EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); - - // Initialise - int[] version = new int[2]; - egl.eglInitialize(display, version); - - // Query total number of configurations - int[] totalConfigurations = new int[1]; - egl.eglGetConfigs(display, null, 0, totalConfigurations); - - // Query actual list configurations - EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]]; - egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations); - - int[] textureSize = new int[1]; - int maximumTextureSize = 0; - - // Iterate through all the configurations to located the maximum texture size - for (int i = 0; i < totalConfigurations[0]; i++) { - // Only need to check for width since opengl textures are always squared - egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize); - - // Keep track of the maximum texture size - if (maximumTextureSize < textureSize[0]) - maximumTextureSize = textureSize[0]; - } - - // Release - egl.eglTerminate(display); - - // Return largest texture size found, or default - return Math.max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION); - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/GLUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/GLUtil.kt new file mode 100644 index 000000000..a481db337 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/GLUtil.kt @@ -0,0 +1,54 @@ +package eu.kanade.tachiyomi.util + +import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.egl.EGLContext +import kotlin.math.max + +class GLUtil private constructor() { + companion object { + // Safe minimum default size + private const val IMAGE_MAX_BITMAP_DIMENSION = 2048 + + val maxTextureSize: Int + get() { + // Get EGL Display + val egl = EGLContext.getEGL() as EGL10 + val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) + + // Initialise + val version = IntArray(2) + egl.eglInitialize(display, version) + + // Query total number of configurations + val totalConfigurations = IntArray(1) + egl.eglGetConfigs(display, null, 0, totalConfigurations) + + // Query actual list configurations + val configurationsList = arrayOfNulls(totalConfigurations[0]) + egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations) + + val textureSize = IntArray(1) + var maximumTextureSize = 0 + + // Iterate through all the configurations to located the maximum texture size + for (i in 0 until totalConfigurations[0]) { + // Only need to check for width since opengl textures are always squared + egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize) + + // Keep track of the maximum texture size + if (maximumTextureSize < textureSize[0]) maximumTextureSize = textureSize[0] + } + + // Release + egl.eglTerminate(display) + + // Return largest texture size found, or default + return max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION) + } + } + + init { + throw InstantiationException("This class is not for instantiation") + } +} From 39d509a756b7ecabe40d4a6c2a49f3cca0815269 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 5 Jan 2020 20:42:58 -0500 Subject: [PATCH 013/675] Remove repository for Conductor snapshot (#2441) --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 373aaa285..65628f088 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,6 @@ allprojects { repositories { google() maven { url "https://www.jitpack.io" } - maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } jcenter() } } From eb5382e0de3232a2b18441a8a22d2c5b163771cf Mon Sep 17 00:00:00 2001 From: mutsumi <4182301+mutsumi63@users.noreply.github.com> Date: Tue, 7 Jan 2020 09:02:28 +0800 Subject: [PATCH 014/675] fix bangumi tracker crash in searching english manga title (#2452) fix bangumi tracker crash in searching english manga title --- .../eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index c678372c6..7180156aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -84,10 +84,13 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept return authClient.newCall(request) .asObservableSuccess() .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() + var responseBody = netResponse.body?.string().orEmpty() if (responseBody.isEmpty()) { throw Exception("Null Response") } + if(responseBody.contains("\"code\":404")){ + responseBody = "{\"results\":0,\"list\":[]}" + } val response = parser.parse(responseBody).obj["list"]?.array response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) } } From b55814a1c053e73d39e03e7ffbd8489644763270 Mon Sep 17 00:00:00 2001 From: andrecsilva <12188364+andrecsilva@users.noreply.github.com> Date: Tue, 7 Jan 2020 20:46:08 -0300 Subject: [PATCH 015/675] Made 'Default' category selectable in global update settings (#2318) --- .../tachiyomi/data/library/LibraryUpdateService.kt | 1 - .../tachiyomi/ui/setting/SettingsGeneralController.kt | 10 ++++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 0b9517c5b..b43a0274c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -249,7 +249,6 @@ class LibraryUpdateService( else db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } } - if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt index 04c25d182..f50b97d2f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt @@ -141,17 +141,17 @@ class SettingsGeneralController : SettingsController() { } val dbCategories = db.getCategories().executeAsBlocking() + val categories = listOf(Category.createDefault()) + dbCategories multiSelectListPreference { key = Keys.libraryUpdateCategories titleRes = R.string.pref_library_update_categories - entries = dbCategories.map { it.name }.toTypedArray() - entryValues = dbCategories.map { it.id.toString() }.toTypedArray() - + entries = categories.map { it.name }.toTypedArray() + entryValues = categories.map { it.id.toString() }.toTypedArray() preferences.libraryUpdateCategories().asObservable() .subscribeUntilDestroy { val selectedCategories = it - .mapNotNull { id -> dbCategories.find { it.id == id.toInt() } } + .mapNotNull { id -> categories.find { it.id == id.toInt() } } .sortedBy { it.order } summary = if (selectedCategories.isEmpty()) @@ -180,8 +180,6 @@ class SettingsGeneralController : SettingsController() { key = Keys.defaultCategory titleRes = R.string.default_category - val categories = listOf(Category.createDefault()) + dbCategories - val selectedCategory = categories.find { it.id == preferences.defaultCategory() } entries = arrayOf(context.getString(R.string.default_category_summary)) + categories.map { it.name }.toTypedArray() From 0d5099f23028c6c378d1907a61367537c1083d01 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 7 Jan 2020 18:46:31 -0500 Subject: [PATCH 016/675] Drop support for Android 4.x (#2440) * Bump minSdkVersion * Remove Android 4.x specific logic * Consolidate res assets * Add note about minimum Android version to README * Restore incorrectly removed method, remove unneeded Lollipop TargetApi annotations --- README.md | 2 +- app/build.gradle | 2 +- .../tachiyomi/network/AndroidCookieJar.kt | 30 +----- .../network/CloudflareInterceptor.kt | 6 +- .../kanade/tachiyomi/network/NetworkHelper.kt | 98 +------------------ .../tachiyomi/ui/base/holder/SlicedHolder.kt | 7 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 16 +-- .../viewer/webtoon/WebtoonRecyclerView.kt | 7 +- .../ui/setting/SettingsBackupController.kt | 64 ++++-------- .../ui/setting/SettingsDownloadController.kt | 28 ++---- .../java/eu/kanade/tachiyomi/util/DiskUtil.kt | 14 +-- .../tachiyomi/util/WebViewClientCompat.kt | 1 - .../tachiyomi/widget/ElevationAppBarLayout.kt | 28 +++--- .../tachiyomi/widget/RevealAnimationView.kt | 61 +++++------- .../library_item_selector_amoled.xml | 21 ---- .../library_item_selector_dark.xml | 21 ---- .../library_item_selector_light.xml | 21 ---- .../list_item_selector_amoled.xml | 19 ---- .../drawable-v21/list_item_selector_dark.xml | 19 ---- .../drawable-v21/list_item_selector_light.xml | 19 ---- .../drawable/library_item_selector_amoled.xml | 23 +++-- .../drawable/library_item_selector_dark.xml | 23 +++-- .../drawable/library_item_selector_light.xml | 23 +++-- .../drawable/list_item_selector_amoled.xml | 23 +++-- .../res/drawable/list_item_selector_dark.xml | 23 +++-- .../res/drawable/list_item_selector_light.xml | 23 +++-- app/src/main/res/values-v21/dimens.xml | 5 - app/src/main/res/values-v21/keys.xml | 6 -- app/src/main/res/values-v21/themes.xml | 58 ----------- app/src/main/res/values/dimens.xml | 3 +- app/src/main/res/values/keys.xml | 7 -- app/src/main/res/values/styles.xml | 4 +- app/src/main/res/values/themes.xml | 25 ++++- 33 files changed, 200 insertions(+), 530 deletions(-) delete mode 100644 app/src/main/res/drawable-v21/library_item_selector_amoled.xml delete mode 100644 app/src/main/res/drawable-v21/library_item_selector_dark.xml delete mode 100644 app/src/main/res/drawable-v21/library_item_selector_light.xml delete mode 100644 app/src/main/res/drawable-v21/list_item_selector_amoled.xml delete mode 100644 app/src/main/res/drawable-v21/list_item_selector_dark.xml delete mode 100644 app/src/main/res/drawable-v21/list_item_selector_light.xml delete mode 100644 app/src/main/res/values-v21/dimens.xml delete mode 100644 app/src/main/res/values-v21/keys.xml delete mode 100644 app/src/main/res/values-v21/themes.xml delete mode 100644 app/src/main/res/values/keys.xml diff --git a/README.md b/README.md index ea0b8fb32..21f8adffb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # ![app icon](./.github/readme-images/app-icon.png)Tachiyomi -Tachiyomi is a free and open source manga reader for Android. +Tachiyomi is a free and open source manga reader for Android 5.0 and above. ![screenshots of app](./.github/readme-images/screens.png) diff --git a/app/build.gradle b/app/build.gradle index 91153d261..53b39b876 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,7 +35,7 @@ android { defaultConfig { applicationId "eu.kanade.tachiyomi" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 28 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" versionCode 41 diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt index ff231ed42..7db589a55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt @@ -1,35 +1,20 @@ package eu.kanade.tachiyomi.network -import android.content.Context -import android.os.Build import android.webkit.CookieManager -import android.webkit.CookieSyncManager import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl -class AndroidCookieJar(context: Context) : CookieJar { +class AndroidCookieJar : CookieJar { private val manager = CookieManager.getInstance() - private val syncManager by lazy { CookieSyncManager.createInstance(context) } - - init { - // Init sync manager when using anything below L - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - syncManager - } - } - override fun saveFromResponse(url: HttpUrl, cookies: List) { val urlString = url.toString() for (cookie in cookies) { manager.setCookie(urlString, cookie.toString()) } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - syncManager.sync() - } } override fun loadForRequest(url: HttpUrl): List { @@ -39,7 +24,7 @@ class AndroidCookieJar(context: Context) : CookieJar { fun get(url: HttpUrl): List { val cookies = manager.getCookie(url.toString()) - return if (cookies != null && !cookies.isEmpty()) { + return if (cookies != null && cookies.isNotEmpty()) { cookies.split(";").mapNotNull { Cookie.parse(url, it) } } else { emptyList() @@ -53,19 +38,10 @@ class AndroidCookieJar(context: Context) : CookieJar { cookies.split(";") .map { it.substringBefore("=") } .onEach { manager.setCookie(urlString, "$it=;Max-Age=-1") } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - syncManager.sync() - } } fun removeAll() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - manager.removeAllCookies {} - } else { - manager.removeAllCookie() - syncManager.sync() - } + manager.removeAllCookies {} } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt index 7a320ba49..ec66d2df4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -28,11 +28,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { * Application class. */ private val initWebView by lazy { - if (Build.VERSION.SDK_INT >= 17) { - WebSettings.getDefaultUserAgent(context) - } else { - null - } + WebSettings.getDefaultUserAgent(context) } @Synchronized diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index fa0b70660..21445593e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -1,17 +1,9 @@ package eu.kanade.tachiyomi.network import android.content.Context -import android.os.Build -import okhttp3.* +import okhttp3.Cache +import okhttp3.OkHttpClient import java.io.File -import java.io.IOException -import java.net.InetAddress -import java.net.Socket -import java.net.UnknownHostException -import java.security.KeyManagementException -import java.security.KeyStore -import java.security.NoSuchAlgorithmException -import javax.net.ssl.* class NetworkHelper(context: Context) { @@ -19,99 +11,15 @@ class NetworkHelper(context: Context) { private val cacheSize = 5L * 1024 * 1024 // 5 MiB - val cookieManager = AndroidCookieJar(context) + val cookieManager = AndroidCookieJar() val client = OkHttpClient.Builder() .cookieJar(cookieManager) .cache(Cache(cacheDir, cacheSize)) - .enableTLS12() .build() val cloudflareClient = client.newBuilder() .addInterceptor(CloudflareInterceptor(context)) .build() - private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - return this - } - - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - trustManagerFactory.init(null as KeyStore?) - val trustManagers = trustManagerFactory.trustManagers - if (trustManagers.size == 1 && trustManagers[0] is X509TrustManager) { - class TLSSocketFactory @Throws(KeyManagementException::class, NoSuchAlgorithmException::class) - constructor() : SSLSocketFactory() { - - private val internalSSLSocketFactory: SSLSocketFactory - - init { - val context = SSLContext.getInstance("TLS") - context.init(null, null, null) - internalSSLSocketFactory = context.socketFactory - } - - override fun getDefaultCipherSuites(): Array { - return internalSSLSocketFactory.defaultCipherSuites - } - - override fun getSupportedCipherSuites(): Array { - return internalSSLSocketFactory.supportedCipherSuites - } - - @Throws(IOException::class) - override fun createSocket(): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket()) - } - - @Throws(IOException::class) - override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)) - } - - @Throws(IOException::class, UnknownHostException::class) - override fun createSocket(host: String, port: Int): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) - } - - @Throws(IOException::class, UnknownHostException::class) - override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)) - } - - @Throws(IOException::class) - override fun createSocket(host: InetAddress, port: Int): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) - } - - @Throws(IOException::class) - override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)) - } - - private fun enableTLSOnSocket(socket: Socket?): Socket? { - if (socket != null && socket is SSLSocket) { - socket.enabledProtocols = socket.supportedProtocols - } - return socket - } - } - - sslSocketFactory(TLSSocketFactory(), trustManagers[0] as X509TrustManager) - } - - val specCompat = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) - .cipherSuites( - *ConnectionSpec.MODERN_TLS.cipherSuites.orEmpty().toTypedArray(), - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA - ) - .build() - - val specs = listOf(specCompat, ConnectionSpec.CLEARTEXT) - connectionSpecs(specs) - - return this - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt index b2fc8fd26..2d7efa248 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.base.holder -import android.os.Build import android.view.View import android.view.ViewGroup import eu.davidea.flexibleadapter.FlexibleAdapter @@ -51,10 +50,6 @@ interface SlicedHolder { 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) } @@ -68,4 +63,4 @@ interface SlicedHolder { val margin get() = 8.dpToPx -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index c245f3375..d72b993a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -8,7 +8,6 @@ import android.content.pm.ActivityInfo import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color -import android.os.Build import android.os.Bundle import android.view.* import android.view.animation.Animation @@ -21,9 +20,7 @@ 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.activity.BaseRxActivity -import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst -import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error -import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success +import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.* import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters @@ -276,10 +273,7 @@ class ReaderActivity : BaseRxActivity() { toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() { override fun onAnimationStart(animation: Animation) { // Fix status bar being translucent the first time it's opened. - if (Build.VERSION.SDK_INT >= 21) { - window.addFlags( - WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - } + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) } }) toolbar.startAnimation(toolbarAnimation) @@ -637,11 +631,7 @@ class ReaderActivity : BaseRxActivity() { */ private fun setFullscreen(enabled: Boolean) { systemUi = if (enabled) { - val level = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - SystemUiHelper.LEVEL_IMMERSIVE - } else { - SystemUiHelper.LEVEL_HIDE_STATUS_BAR - } + val level = SystemUiHelper.LEVEL_IMMERSIVE val flags = SystemUiHelper.FLAG_IMMERSIVE_STICKY or SystemUiHelper.FLAG_LAYOUT_IN_SCREEN_OLDER_DEVICES diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt index 52600d0e2..b6597cba3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt @@ -3,16 +3,14 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon import android.animation.Animator import android.animation.AnimatorSet import android.animation.ValueAnimator -import android.annotation.TargetApi import android.content.Context -import android.os.Build -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import android.util.AttributeSet import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.ViewConfiguration import android.view.animation.DecelerateInterpolator +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap /** @@ -58,7 +56,6 @@ open class WebtoonRecyclerView @JvmOverloads constructor( firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() } - @TargetApi(Build.VERSION_CODES.KITKAT) override fun onScrollStateChanged(state: Int) { super.onScrollStateChanged(state) val layoutManager = layoutManager diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt index 8ba0798fe..005ec1aaf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt @@ -5,10 +5,9 @@ import android.app.Activity import android.app.Dialog import android.content.* import android.net.Uri -import android.os.Build import android.os.Bundle -import androidx.preference.PreferenceScreen import android.view.View +import androidx.preference.PreferenceScreen import com.afollestad.materialdialogs.MaterialDialog import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.R @@ -106,21 +105,12 @@ class SettingsBackupController : SettingsController() { onClick { val currentDir = preferences.backupsDirectory().getOrDefault() try{ - val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - // Custom dir selected, open directory selector - preferences.context.getFilePicker(currentDir) - } else { - Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - } - + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(intent, CODE_BACKUP_DIR) } 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) - } + // Fall back to custom picker on error + startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR) } - } preferences.backupsDirectory().asObservable() @@ -154,37 +144,27 @@ class SettingsBackupController : SettingsController() { // Get uri of backup folder. val uri = data.data - // Get UriPermission so it's possible to write files post kitkat. - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION + // Get UriPermission so it's possible to write files + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION - activity.contentResolver.takePersistableUriPermission(uri, flags) - } + activity.contentResolver.takePersistableUriPermission(uri, flags) - // Set backup Uri. + // Set backup Uri preferences.backupsDirectory().set(uri.toString()) } CODE_BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) { val activity = activity ?: return - val uri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - val dir = data.data.path - val file = File(dir, Backup.getDefaultFilename()) - Uri.fromFile(file) - } else { - val uri = data.data - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION + val uri = data.data + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION - activity.contentResolver.takePersistableUriPermission(uri, flags) - val file = UniFile.fromUri(activity, uri) - - file.uri - } + activity.contentResolver.takePersistableUriPermission(uri, flags) + val file = UniFile.fromUri(activity, uri) CreatingBackupDialog().showDialog(router, TAG_CREATING_BACKUP_DIALOG) - BackupCreateService.makeBackup(activity, uri, backupFlags) + BackupCreateService.makeBackup(activity, file.uri, backupFlags) } CODE_BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) { val uri = data.data @@ -201,25 +181,17 @@ class SettingsBackupController : SettingsController() { val currentDir = preferences.backupsDirectory().getOrDefault() try { - // If API is lower than Lollipop use custom picker - val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - preferences.context.getFilePicker(currentDir) - } else { - // Use Androids build in file creator - Intent(Intent.ACTION_CREATE_DOCUMENT) + // Use Android's built-in file creator + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .setType("application/*") .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(preferences.context.getFilePicker(currentDir), CODE_BACKUP_CREATE) } - } class CreateBackupDialog : DialogController() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt index e4531de8d..cd2b41a49 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -5,7 +5,6 @@ import android.app.Dialog import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle import android.os.Environment import androidx.core.content.ContextCompat @@ -107,11 +106,7 @@ class SettingsDownloadController : SettingsController() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { - DOWNLOAD_DIR_PRE_L -> if (data != null && resultCode == Activity.RESULT_OK) { - val uri = Uri.fromFile(File(data.data.path)) - preferences.downloadsDirectory().set(uri.toString()) - } - DOWNLOAD_DIR_L -> if (data != null && resultCode == Activity.RESULT_OK) { + DOWNLOAD_DIR -> if (data != null && resultCode == Activity.RESULT_OK) { val context = applicationContext ?: return val uri = data.data val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or @@ -132,19 +127,11 @@ class SettingsDownloadController : SettingsController() { } fun customDirectorySelected(currentDir: String) { - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR_PRE_L) - } else { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - 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) - } - } - + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + try { + startActivityForResult(intent, DOWNLOAD_DIR) + } catch (e: ActivityNotFoundException) { + startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR) } } @@ -183,7 +170,6 @@ class SettingsDownloadController : SettingsController() { } private companion object { - const val DOWNLOAD_DIR_PRE_L = 103 - const val DOWNLOAD_DIR_L = 104 + const val DOWNLOAD_DIR = 104 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt index 4f0375e42..e089c9ebc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.util import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Environment import androidx.core.content.ContextCompat import androidx.core.os.EnvironmentCompat @@ -45,13 +44,6 @@ object DiskUtil { } } - if (Build.VERSION.SDK_INT < 21) { - val extStorages = System.getenv("SECONDARY_STORAGE") - if (extStorages != null) { - directories += extStorages.split(":").map(::File) - } - } - return directories } @@ -79,11 +71,7 @@ object DiskUtil { * Scans the given file so that it can be shown in gallery apps, for example. */ fun scanMedia(context: Context, uri: Uri) { - val action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - Intent.ACTION_MEDIA_MOUNTED - } else { - Intent.ACTION_MEDIA_SCANNER_SCAN_FILE - } + val action = Intent.ACTION_MEDIA_SCANNER_SCAN_FILE val mediaScanIntent = Intent(action) mediaScanIntent.data = uri context.sendBroadcast(mediaScanIntent) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/WebViewClientCompat.kt b/app/src/main/java/eu/kanade/tachiyomi/util/WebViewClientCompat.kt index 977dca5e6..911a40714 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/WebViewClientCompat.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/WebViewClientCompat.kt @@ -36,7 +36,6 @@ abstract class WebViewClientCompat : WebViewClient() { return shouldOverrideUrlCompat(view, url) } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) final override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt index 32a959e66..8ae15900f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt @@ -16,32 +16,26 @@ class ElevationAppBarLayout @JvmOverloads constructor( private var origStateAnimator: StateListAnimator? = null init { - if (Build.VERSION.SDK_INT >= 21) { - origStateAnimator = stateListAnimator - } + origStateAnimator = stateListAnimator } fun enableElevation() { - if (Build.VERSION.SDK_INT >= 21) { - stateListAnimator = origStateAnimator - } + stateListAnimator = origStateAnimator } fun disableElevation() { - if (Build.VERSION.SDK_INT >= 21) { - stateListAnimator = StateListAnimator().apply { - val objAnimator = ObjectAnimator.ofFloat(this, "elevation", 0f) + stateListAnimator = StateListAnimator().apply { + val objAnimator = ObjectAnimator.ofFloat(this, "elevation", 0f) - // Enabled and collapsible, but not collapsed means not elevated - addState(intArrayOf(android.R.attr.enabled, R.attr.state_collapsible, -R.attr.state_collapsed), - objAnimator) + // Enabled and collapsible, but not collapsed means not elevated + addState(intArrayOf(android.R.attr.enabled, R.attr.state_collapsible, -R.attr.state_collapsed), + objAnimator) - // Default enabled state - addState(intArrayOf(android.R.attr.enabled), objAnimator) + // Default enabled state + addState(intArrayOf(android.R.attr.enabled), objAnimator) - // Disabled state - addState(IntArray(0), objAnimator) - } + // Disabled state + addState(IntArray(0), objAnimator) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt index f5718e75d..a78a67803 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt @@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.widget import android.animation.Animator import android.animation.AnimatorListenerAdapter -import android.annotation.TargetApi import android.content.Context -import android.os.Build import android.util.AttributeSet import android.view.View import android.view.ViewAnimationUtils -@TargetApi(Build.VERSION_CODES.LOLLIPOP) class RevealAnimationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : View(context, attrs) { @@ -21,28 +18,25 @@ class RevealAnimationView @JvmOverloads constructor(context: Context, attrs: Att * @param initialRadius size of radius of animation */ fun hideRevealEffect(centerX: Int, centerY: Int, initialRadius: Int) { - if (Build.VERSION.SDK_INT >= 21) { + // Make the view visible. + this.visibility = View.VISIBLE - // Make the view visible. - this.visibility = View.VISIBLE + // Create the animation (the final radius is zero). + val anim = ViewAnimationUtils.createCircularReveal( + this, centerX, centerY, initialRadius.toFloat(), 0f) - // Create the animation (the final radius is zero). - val anim = ViewAnimationUtils.createCircularReveal( - this, centerX, centerY, initialRadius.toFloat(), 0f) + // Set duration of animation. + anim.duration = 500 - // Set duration of animation. - anim.duration = 500 + // make the view invisible when the animation is done + anim.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + this@RevealAnimationView.visibility = View.INVISIBLE + } + }) - // make the view invisible when the animation is done - anim.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - super.onAnimationEnd(animation) - this@RevealAnimationView.visibility = View.INVISIBLE - } - }) - - anim.start() - } + anim.start() } /** @@ -55,25 +49,20 @@ class RevealAnimationView @JvmOverloads constructor(context: Context, attrs: Att * @return sdk version lower then 21 */ fun showRevealEffect(centerX: Int, centerY: Int, listener: Animator.AnimatorListener): Boolean { - if (Build.VERSION.SDK_INT >= 21) { + this.visibility = View.VISIBLE - this.visibility = View.VISIBLE + val height = this.height - val height = this.height + // Create animation + val anim = ViewAnimationUtils.createCircularReveal( + this, centerX, centerY, 0f, height.toFloat()) - // Create animation - val anim = ViewAnimationUtils.createCircularReveal( - this, centerX, centerY, 0f, height.toFloat()) + // Set duration of animation + anim.duration = 350 - // Set duration of animation - anim.duration = 350 - - anim.addListener(listener) - anim.start() - return true - } - return false + anim.addListener(listener) + anim.start() + return true } - } diff --git a/app/src/main/res/drawable-v21/library_item_selector_amoled.xml b/app/src/main/res/drawable-v21/library_item_selector_amoled.xml deleted file mode 100644 index b21f488c2..000000000 --- a/app/src/main/res/drawable-v21/library_item_selector_amoled.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/library_item_selector_dark.xml b/app/src/main/res/drawable-v21/library_item_selector_dark.xml deleted file mode 100644 index 82a72da4a..000000000 --- a/app/src/main/res/drawable-v21/library_item_selector_dark.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/library_item_selector_light.xml b/app/src/main/res/drawable-v21/library_item_selector_light.xml deleted file mode 100644 index 1f2e8bf89..000000000 --- a/app/src/main/res/drawable-v21/library_item_selector_light.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/list_item_selector_amoled.xml b/app/src/main/res/drawable-v21/list_item_selector_amoled.xml deleted file mode 100644 index 0fce81a34..000000000 --- a/app/src/main/res/drawable-v21/list_item_selector_amoled.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/list_item_selector_dark.xml b/app/src/main/res/drawable-v21/list_item_selector_dark.xml deleted file mode 100644 index 07b9ef6d5..000000000 --- a/app/src/main/res/drawable-v21/list_item_selector_dark.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/list_item_selector_light.xml b/app/src/main/res/drawable-v21/list_item_selector_light.xml deleted file mode 100644 index 942446ef0..000000000 --- a/app/src/main/res/drawable-v21/list_item_selector_light.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/library_item_selector_amoled.xml b/app/src/main/res/drawable/library_item_selector_amoled.xml index 1cf05bdc9..18b0fb40e 100644 --- a/app/src/main/res/drawable/library_item_selector_amoled.xml +++ b/app/src/main/res/drawable/library_item_selector_amoled.xml @@ -1,10 +1,19 @@ - + + + + + + - - - - + + + - \ No newline at end of file + + + + + + diff --git a/app/src/main/res/drawable/library_item_selector_dark.xml b/app/src/main/res/drawable/library_item_selector_dark.xml index 9880c4b38..7e2dc8b74 100644 --- a/app/src/main/res/drawable/library_item_selector_dark.xml +++ b/app/src/main/res/drawable/library_item_selector_dark.xml @@ -1,10 +1,19 @@ - + + + + + + - - - - + + + - \ No newline at end of file + + + + + + diff --git a/app/src/main/res/drawable/library_item_selector_light.xml b/app/src/main/res/drawable/library_item_selector_light.xml index 70f7b85b4..e51877cc9 100644 --- a/app/src/main/res/drawable/library_item_selector_light.xml +++ b/app/src/main/res/drawable/library_item_selector_light.xml @@ -1,10 +1,19 @@ - + + + + + + - - - - + + + - \ No newline at end of file + + + + + + diff --git a/app/src/main/res/drawable/list_item_selector_amoled.xml b/app/src/main/res/drawable/list_item_selector_amoled.xml index 9bbf56578..f3a9c3e06 100644 --- a/app/src/main/res/drawable/list_item_selector_amoled.xml +++ b/app/src/main/res/drawable/list_item_selector_amoled.xml @@ -1,10 +1,19 @@ - + + + + + + - - - - + + + - \ No newline at end of file + + + + + + diff --git a/app/src/main/res/drawable/list_item_selector_dark.xml b/app/src/main/res/drawable/list_item_selector_dark.xml index 60034f818..5b08a69f7 100644 --- a/app/src/main/res/drawable/list_item_selector_dark.xml +++ b/app/src/main/res/drawable/list_item_selector_dark.xml @@ -1,10 +1,19 @@ - + + + + + + - - - - + + + - \ No newline at end of file + + + + + + diff --git a/app/src/main/res/drawable/list_item_selector_light.xml b/app/src/main/res/drawable/list_item_selector_light.xml index 92bed9fc9..3fa9224b9 100644 --- a/app/src/main/res/drawable/list_item_selector_light.xml +++ b/app/src/main/res/drawable/list_item_selector_light.xml @@ -1,10 +1,19 @@ - + + + + + + - - - - + + + - \ No newline at end of file + + + + + + diff --git a/app/src/main/res/values-v21/dimens.xml b/app/src/main/res/values-v21/dimens.xml deleted file mode 100644 index 8b96d2cad..000000000 --- a/app/src/main/res/values-v21/dimens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - 41dp - \ No newline at end of file diff --git a/app/src/main/res/values-v21/keys.xml b/app/src/main/res/values-v21/keys.xml deleted file mode 100644 index 0a74c875b..000000000 --- a/app/src/main/res/values-v21/keys.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - sans-serif-medium - sans-serif-regular - \ No newline at end of file diff --git a/app/src/main/res/values-v21/themes.xml b/app/src/main/res/values-v21/themes.xml deleted file mode 100644 index a7ff1d5c4..000000000 --- a/app/src/main/res/values-v21/themes.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 77114a247..8b6231cdf 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -20,10 +20,9 @@ 16sp 14sp - 158dp - 16dp + 41dp 0dp diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml deleted file mode 100644 index 6f662810f..000000000 --- a/app/src/main/res/values/keys.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - sans-serif - sans-serif - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d52818314..d2100ea3b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -50,7 +50,7 @@ + + + @@ -61,6 +66,10 @@ @color/dividerDark @drawable/line_divider_dark + true + @android:color/transparent + @color/colorDarkPrimaryDark + true @style/ThemeOverlay.AppCompat.Dark.ActionBar @@ -86,6 +95,10 @@ @@ -96,6 +109,10 @@ @color/colorAmoledPrimary @color/md_black_1000 + true + @android:color/transparent + @android:color/transparent + @drawable/list_item_selector_amoled @drawable/library_item_selector_amoled @@ -113,12 +130,18 @@ @color/colorDarkPrimary @color/colorDarkPrimaryDark @android:color/black + + ?colorPrimaryDark + ?colorPrimaryDark + + From 07caea8b4e796c647122825a5bb9f0960f1e7498 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 8 Jan 2020 22:38:19 -0500 Subject: [PATCH 028/675] Clean up splash screen code --- app/src/main/AndroidManifest.xml | 2 +- app/src/main/res/drawable/branded_logo.xml | 10 ---------- app/src/main/res/drawable/branded_logo_icon.png | Bin 22254 -> 0 bytes app/src/main/res/drawable/splash_background.xml | 7 +++---- .../main/res/layout/recent_chapters_item.xml | 2 +- app/src/main/res/layout/track_search_item.xml | 2 +- app/src/main/res/values/colors.xml | 4 ++-- app/src/main/res/values/styles.xml | 10 ---------- app/src/main/res/values/themes.xml | 4 ++-- 9 files changed, 10 insertions(+), 31 deletions(-) delete mode 100644 app/src/main/res/drawable/branded_logo.xml delete mode 100644 app/src/main/res/drawable/branded_logo_icon.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3305a16f0..004c2e168 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ + android:theme="@style/Theme.Splash"> diff --git a/app/src/main/res/drawable/branded_logo.xml b/app/src/main/res/drawable/branded_logo.xml deleted file mode 100644 index 3d0d69d42..000000000 --- a/app/src/main/res/drawable/branded_logo.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/branded_logo_icon.png b/app/src/main/res/drawable/branded_logo_icon.png deleted file mode 100644 index ddcebeafa5f972251a02e39e666d5a34eba6b539..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22254 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�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 w*4A(EXMRrObQm7pK^n~R|Kb1CI<&&o - + - + android:drawable="@drawable/tachiyomi_circle" + android:gravity="center"> diff --git a/app/src/main/res/layout/recent_chapters_item.xml b/app/src/main/res/layout/recent_chapters_item.xml index c8d275dbb..4fc025ad3 100644 --- a/app/src/main/res/layout/recent_chapters_item.xml +++ b/app/src/main/res/layout/recent_chapters_item.xml @@ -18,7 +18,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@drawable/branded_logo_icon" /> + tools:src="@drawable/tachiyomi_circle" /> + tools:src="@drawable/tachiyomi_circle"/> #1C1C1D @color/md_black_1000 + #455A64 + @color/md_blue_A400 @color/md_black_1000_87 @@ -84,6 +86,4 @@ #009688 - #455A64 - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 10ec72f82..4c7297853 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,16 +1,6 @@ - - - - - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 47846b2ea..52ce7d6da 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -156,8 +156,8 @@ - From 8d3166c5febe2fcf9d53084440a9f18650a467dd Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 18:30:55 -0500 Subject: [PATCH 029/675] Add larger minimalistic splash screen icon --- app/src/main/res/drawable-hdpi/splash_icon.webp | Bin 0 -> 1316 bytes app/src/main/res/drawable-mdpi/splash_icon.webp | Bin 0 -> 902 bytes app/src/main/res/drawable-xhdpi/splash_icon.webp | Bin 0 -> 1654 bytes .../main/res/drawable-xxhdpi/splash_icon.webp | Bin 0 -> 2184 bytes .../main/res/drawable-xxxhdpi/splash_icon.webp | Bin 0 -> 2336 bytes app/src/main/res/drawable/splash_background.xml | 7 +++++-- 6 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/splash_icon.webp create mode 100644 app/src/main/res/drawable-mdpi/splash_icon.webp create mode 100644 app/src/main/res/drawable-xhdpi/splash_icon.webp create mode 100644 app/src/main/res/drawable-xxhdpi/splash_icon.webp create mode 100644 app/src/main/res/drawable-xxxhdpi/splash_icon.webp diff --git a/app/src/main/res/drawable-hdpi/splash_icon.webp b/app/src/main/res/drawable-hdpi/splash_icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..9429d0d06b360f582c8fc6920420da366a1c6d04 GIT binary patch literal 1316 zcmV+<1>5>kNk&E-1pok7MM6+kP&iBw1pojqYJeILcc-9jBggzn-}wCq=70VL1SOJ0 zF}TKn0RS)z3`YzY_85L**kPD4++hF!3;-C;7y|%c!0^{V?bm?W&Aav=JNI^q&_?Xpap2^| z>-V0&`}#Yzfb0_5K~h~)SKrX2Y9=kW32g}hE)`l|$PA&yK*kEquL3YcXik}cenPYS zO{eN5G{Y}+P2Gg1`;o4plhE)LU3Da9QM*>JShi^1tZ9?R4jiR#cJJJy)*& zEI6JhR|lbtmn%dilF@Sc)wUr%Rc(l8X&d5E+=h6Th-8#p-sK`0CzpSnNG8bDorrY0 zT>UEJKEHeUz>CW0gtD^qI34 zE?>QIv$B2X-h;=_UcPnz>8p32zx_(eEU0WEI9Dz`I}2k29ZhwKQw?oheIqk-D?1mz z$c_VDK{%+0*r1toU$R@P<&Hl4Ba7Pk-H^^_iWnBGmC#G8_dU4r+axFA1LxAPZocL7G;ciA(A@|fd!JB~3x=bwNAPXg4=L4`K6NLa( zq1^bC0M;;B3VEf;4L$|D)M0TQvPt518KB9Jfo&E*)s3UTMmk)?pf>^j(&b416W}?6 z9gqVO&pQFduCPy#Zd~;O;AD_rIUvD=E2m7rCD054+$-^Y8<1cPxdiFM*>V7O2Ux`e z;;cFA+6Y(&=t_V`CC)9=08g~&)K20I4S2EeGImD(?osVTt?Bkb_jS z2(S}>R=I%pR1W|@jrr4j2q<==a4Z0Jt5ALZ59X1iVc!X?wJ`1l4>9*e8i`?Z<(}62o8u z3=)L41~NSXI48Aw?jl_21i&4L`(l=ODAqzr}NV zKeKHK;<>q(*^Wf<++epmMLf6nGdqweo?H8v9m^BX)6>i@S5h>qni?DG>uRg9sshUq z%Swu{AU`iBJ1ZkSEd?<-Nl8eI|E2u?`HlXU5|^kXDJiMxnOS*-CC#rc%28IBlb-bJ z+o!kBAKbon@$}JsJGQJ{zI5?|+0!PD8PdO3=XTK{eqL@au8#JK4O*F*qp2ainPOps z_KvO|eqph_M^0ZiaWJ#xH|}3PfBMAXeS5ZTT)Se?ycv_njU3dsXO~VL+C@hM`+2!J a*;$zx>S(A*@~i6@Svvbh^oR^#_Rqa<*otxh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/splash_icon.webp b/app/src/main/res/drawable-mdpi/splash_icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..f78eb6609de852a1d70869b87e83e704e95024a6 GIT binary patch literal 902 zcmV;119|*XNk&F~0{{S5MM6+kP&iC+0{{RoMt~6z*QTIt8!7xr@18cJF#q#UAZd%Y z{Y29?)J}Xz(<+W|e_{>Y4oT z57M@6I~(a8+qP}btZm!g9ox1twliJZwvDl#zq5Vvrf<6Z6w&_)ApaSv1ua;zX7irI z@x`3Gq%@Xp*?uTCtE`^C9v~?l*_RJre@p@ZL`h2JG@v6sk|O2U7bj_@U>|IywI6$9 zC9N&k3j>B!#8`?w)n?}?-`>{5)mD`j=4GZNCM3kg#_npzM%uYXj;pV&tE;QyIIgLw zxwUQZh&VOCezQ1<5-1hthy+T+IjAi5DvN!};+O;~#3_e~k#8SgKYRS}{_X2m&kmoF z$#|Y0;D>o$cJ|!GOV@7Sy7utNv)3QL{urN@(=LwJ0v|U=8w*n-LjxTRLa(Z(siUVw zh>?YjgR7T+Q23&CyBCT>>5{pF=I4OAO?HH3(@p*3@K8mBIDnB$=?lyli7a>y05BUO zLJ!D&*>p5Sagn#?Kz@)>0U>6&t0{iYL$owNV z>CDI~40Z@w4){eCM$F$~9fEQNoFc5YV4ti7s9(dT{g~NWv|4R%9~p}iO!K$Zd?M;H#C!y1tyY|5J@gD2QmQ#o1> zv0=h=J2KWHZ$98#*-HFrWO%PLk)Mx!wP0&Cz?nmy1lN9sRT8${s}`sdp{oI%2~d=^ zy}hm=TD7ZK1wmD`4K3~5qL+EmWOaSHdv2hssivYRKPxRcF*Y_f_T;J9*!ZN>?3@9> z&vy*;_4N%5ow<1P!K)8nXD0J#l3PKV-x6sKl}O~xQ6=&E2+h$-iM%*ObFx|@&koYW z)hmhg4vCB=(NtfNH#0Hv?eqIL&!0Yibnn)+%jac7eZAdX?X4{h99M(Za*ZwRoxKCH zD=!`&r#V)Bo9}L|uPV%lKeBJz#+8faMTZ8sJ6IZO5%EA?P@C(m8JareP9@!PDt;?F<+;Cz<# z$B>x{4D+smc!U_PUkY)RWoqvrPOwbjCd6)*p`#ERnYIaH71QQGEMnSNh`CG~2r-js zJt3wstt-T2rgefCMkC#JZIfc7qas3s{Jh+q9jq-(jC6H0)KrzY^@7+E92FWA=;Pz# z>+9~;Ob=flA0HpzprD|@kcjYz=;-Ly$vWit%P_8&%j#;YC6(oWetsz}EXc}y_xk12 z$M^5uYIyDH)vMR8Ts-q0LS7@Qt*(|-N+go-?nwj7tWeAqF=YPHgVx0K>@xVu8wxL zmX@X_hWfginp&E|h9ZHYsz}&SpsuB>*(U{?H>>n5+0qd>znvWypgv7b8mOe}g!~mxC1K1fu1BhzcW~dSK9@)JTQEU3uCBQe{UC z%>sx${HVSFd5Gw_4&sX;FJ5&Zb1Cv41Wx5;Fb0jJh-yB>9$pTDyiuVD+63`YjR#>N z$XZ0xHV`TPNig! z2>|92h^vUore7c~0QA9-!{~%=LVP#mwilw@mQsr$a3UwIQixNC(#aqTIVEDqp0u(k zgSf?M1H>LxT#Sq!ll z(OWZ@)5Cc|%FPR`2avf_3|(xN^zk%b#CbUeyoYGMv>D)fO#?yifLMvBuA7%W$Oge1{sfTO(@n`ufcTt<0nZ|aYZL+0`A{IZ zosW~bo=D*@n2P~7BaX^KbuE2UYbSUA$b`1t#w^@+c>w|UaBbO_qU_g??p!%_VB5MC zi)Kw3KWgZJ-rYNQNN$^$lo;1ADk>^Eu30voU`rRkZd?^;c%goI$ z{`{j%BEwBSAmAWo>KOvovE>8-E7)?3fMsksK)_!o#4skFA5d#$uiitD*OBH$XX zt*-p@``g!&;{4oVaqvwYe7`O~LP7&CJ4z}~$& zbxdyCIx#6RCORf6A~G^0I5;>Y(ASTvZ$LUQz6cZC37U<{g=IUT? zVPRrqsHd;5t*xV_p`jrXibNuT3U`4>DALf-(ALp2Ffz5YbMg-UKQX0yGy(tG3Oj}$ AYXATM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/splash_icon.webp b/app/src/main/res/drawable-xxhdpi/splash_icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..cbfd186757951b1db7cb31947636d7e3cfd0cb98 GIT binary patch literal 2184 zcmV;32zU2VNk&G12mk*N`Cl$LS4Rr)F|e!LZbCoO;R=JCcvUW@9$qfy?O)|}_?43PImakmBPNRWLJB^*Jl$d^xHt-ZiaR-JLmk*l`yAz1$wH-fYKNArpo}bXrRwn+O8AEv* zJMzVQ&bLY`i3S@{yX`ZS^y&Ya&C5gOVQQTzM%p?2Y^R*%&8I77#M$t$to* zL0I?N`Frte=6&YRScPLXQ>$ zJB1+~7l%;EWdtz*6wO14x2WqDg7A2TF?QmhC7~xUQED-?l!HmI#=>Zrh`D90zA)51 z2Vt;F7;3CKmw40~CQd)lbk%h*q6P@W{Tt?vmD-jN_@$FTSTH87rrIu}jxmw8!;n^G zMc<;lh2k8Gxuvqc1Wqn16t)!etFF3&P?yZb(l!%9Q`B_|<{n4{|1i_!>)&Bw&Ks3scp-Caa2(;zfdElrX^Axy~kF&ot~fxyGWq{BRQSIKTr zdJQqPH$te5I*PvnonT_N|A1Mjj^+fODk!!c^T=8iQ$PvzMb!tB(LfF6mqFo7)KwB7 zq*X)l1kh4pu@jgZY8Xn`^t$4jiHWbGg1w-aKg88O3qoU65CfXa1pWZC3)eCY)J$ai zF(1porV%#EPGp`K>I~C^mNSu;O~$N3mHP_nDm2S*%yS!1SHi|Ju`k4=*8wd7Z4q22 z49&z?$6~VE3a)%S<~*iaD&~c);8-Z;ZBa-7VV5w`FTjun5(a7|yxyR-h$3$=cO8UR z!jh z&Zuq4z*jNDWy1AmemO5Uw=iSyhKZ2J7BXQOnb-I2y=|Eqg9xf+Qn{6gGS3Vz#?zwlY&~Pl5(BxvbRBjH&o6rio;zyHz%1Ka=X<(re8o|2UK?Zflfh%0A9kL=&EdF}E=b7xJO zFxuBsqZ!zo;i7ph5Gzo3&}vv1hM=n&H0wQx~k>cH~ApL9>2k zrll4(BO@atE89qlKj7kAep1WFVJV8_Vuuvn;$n*wMR2iRimsR}=ebxbMHjhPX0j}n zqDx$?l%i8yY>=YkT&$O(6aP@yYO?H*qQhKlGg)>?Q79J&r04_}dXwdZ$#PbT!nn9B zMHjfZB}F&6xFbdPxp>BhF(WJe_pgMnA3uMHi;H{o@ZtT4sPHRSuAB`!b^NeiA985d z?%lh01?zOW;B_lk^QLvnmM@q+XYxdUU%wH92lwvTy=~hz z&6+oFRKH=}8Z~QFu3V*jnX;ve6)WcI;^Js$Z);&;!TvVQ%q?y09NmhQs!+Llordij KaPj~D|F=~n@lfdi literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/splash_icon.webp b/app/src/main/res/drawable-xxxhdpi/splash_icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..34e4ace57e903016f6941d5b7da82f7f2028c8fc GIT binary patch literal 2336 zcmV+*3E%coNk&E(2><|BMM6+kP&iBs2><{uAHhcu*Jq|}8}lcm$n(m8q(_ z((ivp({=~m*YyO$_}MfqEB&rxyUtOsIJRV%RLAx<%pu3N1}4F=jfVNuv9*WU>DVg6 zyyV#OK0tZgv9WU~Yn|IZl*P{NJ(OwA?P-*8&TSdW5a%`#rI&N-htkQpwLod(+^V9~ zb8e+kYC5-kC>5O>yX|^h1S#*}f*{2m-2YvVqaa0H5B3+1ldi`_hjE8z&af=YI&Yfh zNy9L7UH>o;*sf{XCcl4;s;Y|=MNx*!vfRh#^L3IWNviJ6zKSP@C9=8$!qcs*rg_{j z4C6aJ`!52y{Jx&!BqRINX6B%kwiG;tEY zim9EG2#Tqf>oL^z81E#mh-sda2#INp>+zK9@g~r3))muCH4MY}QP=ga1A#!`4NcRu zP5!J`sX3pkD6`auUhmKRjFx#jB3zl`;ez(N*QFiIq%#%if{!!pn zZHs@2Iz<^L%RPJ@C8?Iz>+uv~zmNGmp3+`#RY_{%^Z5qJ@&sk3y28IzdpYo~t{cW_ z^P2S_8eB~6yLXgoc}o^%gf8N#;H@jQ@%109%uv_(pMN>oT8ArOD*aPkT%hFtmO&%-?DM}#u`VKQHQYeZ>E)qCs;fB0%5 z(Q<=x9zM9CT|N>OQ<8bEE)hveE@e*taB5ZRw0$j|=kfu8l;crOeWq_QvXoU*cogHs zF*sVB^9OtUT?^48uJ?x-JgTZVo(0H!FX^$t6)6$#KhLARUmV9kGN+KC;K&~$9I&z| zN=1OS5U=$F_SB zNG1k*2jy;2yO}r{#i`L7g%7n^MUMLwqh_8F5_ zQP$XHInGavtV>Xi+2scul`hp$cvX88zK2tjF6;=(e0%idu%{SdSD_@@<1w5~lxctx zE^CWpI4vo|f*^d79m+*uF2<7PNtDm*Fp|T*$Vl4-%n11 z)29&%uLt%7Oh!ri#GFT243+|;GXlSY@)K4i4m*`XGD^4%)D)O-ISLiO55mVleE{<# zBXJ_i2S|BCFtcd13MD;1P&l8RC00m>dm>6cM>Jj57|I+s)Z^w$`Q^@mv*o!uzw7{$xMyd9PNH&AMjjGaN*8}N;UuHxvbU#XmmsVp@e;1`S)!30}99~wP0T#Ut zlETRTa<2Wnsyf3Q!9f?%ZF=sUf2=8k{S@SWS-Q2#h0rUV;}~w4`ylT#x)%;V#NGD> zAJa?Eg5+Sxib6=Vl!n;4F-Vwwt%RKPSd#^sWYN0Z$uTQE~(UV_zZ*t6=U8Kg| z+-)RDlGNJg>mtkYI7Lwx`!{PZ2j18B9Q(z};5p6^#08$4H!SmKBT@e-uwB#q{$=WH zMHw#3J$$|vl2q60^;YzFJS9+ydOYR4-gv2rue&^c&dMiu{&1Q9ZiWPLGFPC}t?aLv zS^i?>FpdAtiz3->q)ijV=ME+)h#juScGu%&*W)<{bM#L_kGmcl|H5!k5YIT61VKFa zmzU>VkL`cp>8{78uE%$R*zI5v1o7#gKE8835?qh{uE$Xab5Ibc9L#Y+1RYFJ5a%7t zWkG}-Oh^znY-U-Ob-^@E(=ZGpQP*|-oj@S)mZoXiX20LRMpadHs-h_4WLcKG`7(Ew zvX1v=S;~{cybLj)Cv!<}mh~k`YVPxOk!5+JqRdm()&3{7w*sH*dyG```gK8Y&iPHt zvaXn>X&y2RBT3hF{hdHyr>1F-`~7}ZRp%&*GE|oP`+UBZlGNDi^*-wHcw*Uq|Nrm* G{{j^@>W@MI literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml index f8378ff6f..777cb035a 100644 --- a/app/src/main/res/drawable/splash_background.xml +++ b/app/src/main/res/drawable/splash_background.xml @@ -1,7 +1,10 @@ + + + android:drawable="@drawable/splash_icon" + android:gravity="center" /> + From dc93368e03a77b3ac2395d7de9f5b8a72dca4ba2 Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 18:44:20 -0500 Subject: [PATCH 030/675] Fix stretched splash screen icon on older versions of Android --- app/src/main/res/drawable/splash_background.xml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml index 777cb035a..754167183 100644 --- a/app/src/main/res/drawable/splash_background.xml +++ b/app/src/main/res/drawable/splash_background.xml @@ -3,8 +3,10 @@ - + + + From 19adbeebd5d96e4a07630b3038e326aa24d9618a Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 18:49:14 -0500 Subject: [PATCH 031/675] Convert nav drawer icon to webp --- .../res/drawable-hdpi/tachiyomi_circle.png | Bin 5209 -> 0 bytes .../res/drawable-hdpi/tachiyomi_circle.webp | Bin 0 -> 2068 bytes .../res/drawable-mdpi/tachiyomi_circle.png | Bin 3215 -> 0 bytes .../res/drawable-mdpi/tachiyomi_circle.webp | Bin 0 -> 1316 bytes .../res/drawable-xhdpi/tachiyomi_circle.png | Bin 7202 -> 0 bytes .../res/drawable-xhdpi/tachiyomi_circle.webp | Bin 0 -> 2898 bytes .../res/drawable-xxhdpi/tachiyomi_circle.png | Bin 11494 -> 0 bytes .../res/drawable-xxhdpi/tachiyomi_circle.webp | Bin 0 -> 4398 bytes .../res/drawable-xxxhdpi/tachiyomi_circle.png | Bin 16271 -> 0 bytes .../res/drawable-xxxhdpi/tachiyomi_circle.webp | Bin 0 -> 6146 bytes app/src/main/res/layout/navigation_header.xml | 17 ++++++++--------- 11 files changed, 8 insertions(+), 9 deletions(-) delete mode 100644 app/src/main/res/drawable-hdpi/tachiyomi_circle.png create mode 100644 app/src/main/res/drawable-hdpi/tachiyomi_circle.webp delete mode 100644 app/src/main/res/drawable-mdpi/tachiyomi_circle.png create mode 100644 app/src/main/res/drawable-mdpi/tachiyomi_circle.webp delete mode 100644 app/src/main/res/drawable-xhdpi/tachiyomi_circle.png create mode 100644 app/src/main/res/drawable-xhdpi/tachiyomi_circle.webp delete mode 100644 app/src/main/res/drawable-xxhdpi/tachiyomi_circle.png create mode 100644 app/src/main/res/drawable-xxhdpi/tachiyomi_circle.webp delete mode 100644 app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.png create mode 100644 app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.webp diff --git a/app/src/main/res/drawable-hdpi/tachiyomi_circle.png b/app/src/main/res/drawable-hdpi/tachiyomi_circle.png deleted file mode 100644 index e5f3ce794178021b3f90ce173c453b401769ea22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5209 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 T%zHW&00000NkvXXu0mjfr_kSe diff --git a/app/src/main/res/drawable-hdpi/tachiyomi_circle.webp b/app/src/main/res/drawable-hdpi/tachiyomi_circle.webp new file mode 100644 index 0000000000000000000000000000000000000000..0ec2f7bbe8cd592e61a855d4a9345653b2f86064 GIT binary patch literal 2068 zcmV+v2RN;DdbOCqS!Zk@ZXm}a`F#Oo-i-PU$S(jKH! zsi~B!?e4K{+qP}nRwKWRiePv3|5bLMsIG_uhT= zJ!(;2G3Eyf+9jLzmGfMQQj6)}TeOm9Z$)Imwgv62 z`Ar9FSU0oTTM=QfZRRcfN{Qh$G5(2^wD_hG*PhXO25z7`)=kiwVCXu@9E*svm^!cc zfSQ2oI)>g*V?kYJ551}a7!bN(*7Vt_}=|Nb5 z%h%{hxxpoRml~AL(vuPcws?fz5m*IN*hx>=0Gc=arUqfHpu_oZsewbSfNcLGYTTa% z0KayYn!vB~DqAOMu~fV|Q<(dbmds^Shivx+THvf|=v6oUpd?)N8kxe#14=T&rV%n- zJO0x7i!M=3ZE5C1IuB=xZKi|nE#IhM3kSLxY;p7=6=7J_qV_viJg31c#78SI;lTDU z6u$64C}`a)j~$(w z+OU7wm1%c`m-UAh@up$lqLW`-`EpYFLW{W5T=%@)FWkWHc`R1NZ-Uya4cEUVhwH+% zz^Ej}FApu=^=KlQ36{k}W#1rK?d7^=uHOIXM|{HjSI_Lqc~FTPzt}rx)vg<_C*$DP zHw1Hfi@s4VY8#Y#W-i{i@7mLMe_D}uPp|FUxOgT?fz`@Uoo4d2!P$$}Zr*eJ{LOp! zagWa*-?KTD*@Lxw#%krV@q$9NXL#n^1&fz16Qm2~&K&OHLf{$YzF0pSmMYa60Io=y z^{r84wWJ<*fH@Vd5(WTPP&gpO1ONa~7XY0BDo6lG06vjCm`A0fqM<5O4v?@C32Xq) z>Fi$q6U~d_KLc%-8#{&i0Q+v=5$1FC+tyR01N#^G-n0)vuPqN|FWiqto5ml=^2^D# zm3QCTmw6rq>5s=Jp`L^@S3d1??M(ZDfAOh!8)NYi!~pM3fUKtGynU2-B-j@6z=*D6U8W-eKVTJ;q_Zg3MMt?mH&_0(8_6P>p$BEhMYLn z?0Z?l{k+8$#vs2xLD*U2zhjgAMQy`1_pJXWE~m<-*X^j{Kd^*gvUg~8;QQXv6en78 z`NLFn8aOw<3Rhim1^*MC6M;2;+OSizI=2+DftD+ z)eQ)FbGVqsnQ^lSw%|k;EQUvcsq?@;Kirq}mdvXIlxGfyM-as1?sMm$N4$w42K^}d zS|)E|=NR&=>`k_BQwx0OF@p1{1eLxCc#Af({z5%jc{-OaYYfULZb6WbXHvCE&iQZ| zb&s`*l5VMglg5Hbcu%E3KPOF=22#vmg$=Y;HZx zL0jmaeCba2PDtpzafGv$ki`Iwf$r_;_mWSFN4ccJ04Ki~#`;bRBr{_ZPJ(rZ2mlpB z!smg*KMTr>YPdHqmJNW17}UcTi67!-0>+xtB|3F3)aB}AESdfvZ_Hbz#a8R^kH;;& zNa&@zs{&x1&97f0{=%*X)ajoL#UHP|Y zc1ycfClq_7k42#_eKf~^+B@JNi~n^K*-!sR>T$7EMA%b2zWLs1tN0J{Zhgn%?f)|K z&FmfO(7qjeBW%}RjQ_>Tp2v;x82=W-<-DcXBBx}3{hjQO5n;O@!ft7{IcI^TaI4?* zdO{s|yd7xkf-e5p5YU?N`1QWEVRp?P70SK9U_7SQNQ)r%|NO7`&*Q^EHX)vreivKk z|MEq@vEybe`0-IC3n~I%G5U34MZ$ufxBj4i_q^fe5@Sw-OI*TaAx@3{Q>i?2*Fol>8O*KLY{VhqB#%15 zmo2(zaT<8beIas$_R5Zj@q+yE$F$$=oWD04Td?J6C+3{whEKAxLD7oS!dr{SO?f$)+ z@iKEicGmJ)gP=(>4d;r;(Gp>rRc4Cm-=c2Maxy9P1VCwjObdAY1VJEgcMe0|N;#lJ ziccQodirbhS+5S;dd2P#@dPYqFlW!HP&|7@%;Vp;X&}N%0{6ddMf#MHf4kkG5*^YR yztCmB)2IS2hbkn$Dg(Nc3kGn=7cOg8hxi=7n#zS3IOH^X92_WfB^x_b0002f4H*9b literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/tachiyomi_circle.png b/app/src/main/res/drawable-mdpi/tachiyomi_circle.png deleted file mode 100644 index 94cf3a1d6b7f1cbbd1c27381ba9a81a8c599fa85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3215 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 BMzH_@ diff --git a/app/src/main/res/drawable-mdpi/tachiyomi_circle.webp b/app/src/main/res/drawable-mdpi/tachiyomi_circle.webp new file mode 100644 index 0000000000000000000000000000000000000000..ab4db0b7b8d3a072bbecd416f710e5cb143fe58c GIT binary patch literal 1316 zcmV+<1>5>kNk&E-1pok7MM6+kP&il$0000G0000l001ul06|PpNU;F`009TXD3T;8 zR96o-FZ{bD&=3*1@R7d|fe`G@1lzcdjAm;MCm>AOhWgm7NDwA(A$%0DZQD&7$#LCw zn3t+OIX!H8jroPf98$YG#iyxa) zc)H|a^wK&Na#gkUX&n<+Iq5jF9*G_F-dV50xjOHyXW|^S=hpi%DK%+x7Ad7vUty8zq_H0?GM1qBEZRL4Yq-Zl66=4%^Cqz=i+C!wjENwGXd9C- zgu*^1CJ5#LlNbnQ7t^kRK&zOTAm~|4q9H^h?|7IZ2&(f54|ggEq52RH34{=eGrw4b zi!t4EkA-?97AjY;tW-iG_dVbs;UrbP@Qr~NL?s2&evEg8Y2MZM-FVzp9{J0Vxt|!Q^YkHR+-8>4{XmF`YEko;%e|hLp zxTU1Pv94+zTs(dLNR#JN9&D}B;87}?dWL6K_Kr>^?5*%{4^?RJtPIsPw{>-sw1v6~ zqX4DSg(4eK;1mE>P&gn&0{{Rp5dfV5Dlh;r06vjCnMoz1A|Wces;ICM32Xqt6+u=V z(|yy6P8|C3;}NoXz-^ZE0s7tk1Aqhk@Ahu8UqhcPKV$#d-$o433m|i({67qGn2L)a zB|##W&{sjvQ+e4bHosfpd%Y0b4QRBo6+!S#horu^3%G(;Sf69fzYH98X!!xIXvfB^ zed2}S#DyJz0092`v%3{dJK@?H3FXHX;sJQ+67j#@F zf=qJZq*m9O1IabJ$Is`(V5!HK>oyHWm#vct`x)LMp{bRwEjuIaP$enj%+K4=f>ZkK z_%uZ!))i^!^7-|ZxR}2mV1@lSF>>#>?frEtba4U-O(we>4y{8{SE9HEcCY!sAFuxA zTM>JpNWNcJ9e(>yVh9NVcIE`D8lI%DEM(@A^G1K1Wg3@H2bt)6=5 zd~UicX7VjXvonwJ)Iza~Q z()d*AFUUNpTC|m!*tNlT03|Z#N~oz;cios1xI@}k%fBI!pd1`Y3s$G%O(mz|4dc$v~Q#zv7 zd43JN|y)&Zyp$_y{p%c1PA$!`%1jHz1F1OenOufqPhJP7r aViME5OKR-Z^t6N6*^FK>aln_O0000XuXy|b literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/tachiyomi_circle.png b/app/src/main/res/drawable-xhdpi/tachiyomi_circle.png deleted file mode 100644 index 3b62c92b36f99784fb614bf396b3c7655a2acd8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7202 zcmV+-9NpuIP)Py5+(|@1RCodHT?uep#hLE8HKWn!vSitk56K6IIUHkiq_ATQ8;T=TI0IDy0wHV? zk_yX00!6Z$1SnY8B@n`fsw_tevL-QLlI1?l4F-%k9E|Tnm#uqdG&7oWzppht%#2>o zA(w7K)iNKc#>@x&R`|Qp6Uz}dIME>lCIZ9E6 zVZo(}Os`<6${cSwtc$8oJXwfkVNFfd*=~2k470hh&T4J4nN0Oolc~;THao!;JM-b8 za3mfNMiYttNFveKAB}eJj>q>t>i74o9~|@{ODtKQIe8^R$IH}Y5v94P4=zn<#h0Q$ zSAbZ5GJX2=B+_6;40(ot zdM6mNB^-~h8;Hl>TN4a!xU;jf6VF6hOfslDct4r3 z&w9pWGC<+NYwS3Wr|$YGQNY3t=21?X7j<6 zJ^c9nf!GLG$zVLbaUdGK`JeZz5S9oipdOnO5s{F0*;b z{sjDJ^*;Zr7!ioa-`y3B-hB4ftxqCVX;7?s$y-c07@%5x{Ff~)Z6~>0KX;qW7eaM3 zqpN@Zwd1+t^9UTAdCefSVrobDGa51yi~9l5D4^2D{qgvNF9ZTN{<6J&8%p4JkqHbX zqf#<}Wiha#rd6%2-)ytnZ*f^HZ6V0`EV%N20D+FhL?Q|$1@|VaRhTO(gxTd1=E_QW zZ<0zWl94AK3W@meu!sc$B7u88OJ?vnnU9Pqf|$DkV?-bp+qNwnzV_Vh+aE_1KST9| zqCd&#^~Kf7sD#nD6!5caYTWlVG+c*C(^VMBtVPLrf*y%T@GJn?K69pU9dd|pB2>(s zE9}#z32R-QFgu;XWVeq3s~8myi#WiK^!14_RQ=GFEh6yuzl-3;jUwFMF5D?v9q9L0W9Mt(9SxM`E{z4@jXdiiA$*swt|D#gHz z+{QoHrKvGtfPP|?*Zbo;y1UzxIXON|s3%O60V(irX>ML{g4=zs$6`4O+Jy0!WW`c+ zFgVcCYfn2()P3U{vbASrE7?ynI`HwwqW^Dy6N67bEy7)0pZBB!-U97Scw@2mU-tXI zdu>O@hU6m?V!}iikOKdXmX@QAce@{imNSp?J|87GCX9@TincZ}<=fvD_22xa6fOCl zD`k4PyIb_FS|xfPeprM%I)vxk%w8k2qh9b*7$rE zz?|{kM3^9DcXp-n{8r$bem#Bq;U{@KE1f1&TR!k*tB0JgKJi4+`kUWKk;B1sUz5e{ z7M^9xMAdP}iIE*UWT57hYoKOxM}w=GZnrO=VYk2fvd`CpDB_a81!jG4u?*l#*iS7N zH#W}vmdEo@mBq3kFZj@G2zJX)e=1sj{&Qh#XvnI@zW+%LtM>HMrDip}Zk-&POgTgn znE;Vn*KD;O9kp1V`*3)eX78e@SOze^b$(Tq=ieI|{!nkVF3l(Ni3dkDl0esb!g~Qd}YDoL7@zgqUU>cb#qUt zuD%bm!#c`99VzfzfBRcu^>}pI_WO8F4B3eM@4Y8y)4HC(hDcjioyD>kW|L>u1%rc# zDySFaSA0hYUABJvdG+M$Eqbc0mo}i%wJ++U~wvI1f5VB%sCV zh?o!?VuUj5dzbI*(!XB_Ns`~ol1GL3eeiq=*!=bn3n?F%S&6e!tO z_5}Ch#bWTe=cK_;ryX%650>9U7doAvJmK|jOqP>NQjgkjvR|0?TZ*0~PG`f#_4RjS zDn3msdN8}Cs!5seJow~>c*8iU^E#IAOw|_gAJwJ}_+48fW$qCkh0r}XksF*WHT2*~-y(LUGt}c%yHc<3} zrdEfe`_aL{waFY>`;1nc;6bKYmU}$SDC}}=3CKjb&%vqz=7j^Drv2zgqH@uqoZ|tp zB4)%6w`}BEoOKCo{d~~$&vkWY!(C*KcHo8RvDVg#mTRxgIcY&z2Ly3JsO7rrMr}rr z`9?&nh#9d%dJ0RUyd0^GNIrB(MMc9*yZyqPtsgVprYo+HwtJ(3N*%WW_WbSJw;L9_ z2rUzSU506;jC3zqBI?gNOY}VSkR11n$|h#S4j4ZCX(+T8Kh-QbBfAj;QXIRYv2i(6 zs|7mQFKzjlHBP4JSp^gFuYUEbjE_)1o`3%NNnrrr*Yy4Gi@|4~mF5azU|u0+#O}wy z@Q1s0t->ReC}Rd>BN0RfkTFiEo#%2%PDf+_&C1rA8eNu>$G5b!7?x?l`ta1LQw`I~ z5ot#~>d!ex(AK3RVt0_kaSqaH_9docz|=9oe13iXT*$HGxwL3RTwUTyRvi{oCX*_# zFmFUw7}9JS&BQ2wDdMJFaDlMa*Fy_2#)#N?%;u%U67M6{c^SYAa@0Pxvhr*2Xs9+W z8hr_BXDyZxC$sevG&>gKb(AYj_sm4%*#Nn|K^5GHh#fH`mbfJYq+*kvH+u|F3~)9( z94C%pmwFQNjk80p;f|NKZrv&#fBbRLfT_L*_97=HacsqA19__6LBF35Km1Vr%(%wU z+FpC@HL-K&&W!l%AM`1TLUfKmbol*#(bLl-7A#mGP6UwSAugx(<2!Uj42dN!4#Dy+&O~{ef8Djd)VNq2t(lyJgjCW!AbGS(48Vo6M29UR<@+GMor>@h&fsZ-tV1z2)x)OjgW{&V7$ zYuPT}XLHcpUuYtti;MYO(qyAFB4S7^i7C7(ytUO-pg<$OchLm6T(oQBNSI#19t28Tb!}dbDP^&m zDqJ)@fE7w&Ysi39qleC#y4QY1j+bbkDIkF@Q`b5P>gXv0xeKD0)G>f!fCKZsqAYAB z&gngX^)jIg85=c{wdNqh*>E|C+Q=DaoMBk>z`%fb<&{?q)6yd;jpe%-%65F&QCFY6&G7tX@Ad^wY*~BGD-5@;&rsAd8@lrMd@WIbM)6S2GB{3zosDs2ASILBI5fmSaM0~zPVmECU zMnr>C5Vjg_!YCJUu%{K%h&Tw-ikeLnKc9T^Np`gSeHOgf%BB7A1(g4xZQJ1aW30Uq zOJYiFaZ3h_=>dq7k+39*k?u(3Q-WYboCWjj1=;0{GL$Ipt6%-9wxpL{dTC7BqmMo+ zy&bX>IH+F$|DAFYci%_B1l_&?Vo6L<2dGJzUJ>a%fSloA9s6`-WCPDwGb9wo*6)R# zzMMpxjSH67bUG9(ao1dPjkxTx%hIFe!piE^t25%%4>IydoIg=c^1>ZPPQo-VA(q4x z$r4{&Q{ZOz8JXc3pYO9XtE>CqPEl{PGR9sogjP89d*9Q_JxyH-j-G*a=QjAAa@FcI zoFj4OnP*C|ME{{|;=sHK2XYwEC6`>1vC3wYXCiSt48#jFiEh6D<^OOz-bYOF9>svv zE7KW3xx@_7kB5i5p|ftNu~?QFG62hj0m$1RJe6H&sl^gks@N?FrcTt+VD`u(j|lpL za>+6m-GBf6hRLn1t%CADA8~n-p^7GD)T z0Co-9f&!a@!8fogW*P3s5(*{f``{5a9tPZS!wrHmySlnst_3?_%B3ABeLa{jB@?(@ zs8cPOhv_=w8yS7xdFPFJp7RMl$Mxb6<~8(?qhl{k-o^R|^}$L@)m+B_B26rD2_O(# zVr<9&#LGlxiazG`u077}_QR>jof25gktD7O4B*HYx@i}?A&;wHx(SmizZBQKd-sZ` zQO80k7n60*(;LnU1ED-=#UK+0SYIsWCziNn>sJg&q{Q*83->(u;OPDUjo2b~#E@9xGi1ZYHP!n0Y1!y9 zkur+v((voU!_VmaLy;~IJL<)HZ3HdtAC0Jn58&V@UF#V~} zDF%e^?d^RBXQgf7DS}2M=L9(HXwM)1V3d47$8}-zc9b3#8VNznh#fHm5cFSH49LJG zeGpa&N(_a1qtU(Z2Lk_hs>ky$Dlz*?Hx-^b#k~TR1!_bb{1j2^ph%i*vBZIxa$m`Q z4#tHN&z>8Et4yxePtii`h#_uSI7NGg_`z@O`T-f^LYOk!;h6WwS+o8JzKtz9aYQ?K zB~C?|2d@S)BpXfv5e5CTuDN{C&zPr{r}2iz4r) z^Us$CNuB0H)w4eqYrlBs&dYX;jBG|ddJ(7vkqH@O)B~7-iVvG<00p$aGd#TNv>d&F zexf{IryEBUOoy+1KB8CL7nM40yG~KrqiKUS;DIGo0`$xfG z&&eLovDkuN&jG@SXrvo{|9$DW%kv@*MB*WTU3cCo-A~Y64L^~xe@`T`>FSP-J1_%g z%jXgv<^Q-Cz?WE1P#hvCc%;*L0)89688497U(H12C55a_|_h_hPk_BlKy%7|zUv-R_tLKr}_$z02J$h5CjRV{;y zo#E`51Bj6X9~g2v+V~z5aC_B0sL_fIb)9|KfV=hcjYLpYji2%=~iIAZKUjf`w1Jm!l%foHFMQG0@H`2atJ9i919 zP5>19-+_VKRu2um1qcW}7hkl4FVZCCV}Q!17-4;TczDmE%E~I726C8A6!9U+1T5V6 z-grZ}v6FCrJgQQCoYB<}E9W+xjKY?m3w%DyJz#GI0)M%_vvVbW5_|^g{yLonNuyLK z1DKmkV6eP2JiK*zW##-Po2?}u6QbCM%af)Y+*ekDjUB8=xvBThe-=A%_EC&7A9Ivk zM7h5y6nf|K_V(NH-<#<5K%X6@^Q(rdEn++j5O`7IgHY(R<0~r{;h6fmoJ@exj?P@_ zxGy(OG|^39w6&b1gecq}+p&?O3x^F-qsukmvPwKhx+5Ika#d&N4PD{zE<_Q0KLSNh zUht8pph1}V6j7IDF!e>FBOQ^*W*pVHxXNtya42=&yU7zx8sKH?q9z*JiHP42tL(LiXLKA76zbPS+JZ`Y!8Qh zFt=`5;&Ls)FkXMSN$v$g>ACOLTCuzq5Q70x?QS3vI@*Z`~ zMFf8^7Ta^no;|;Q*6Ur5yy@WQ)OxbZEk*?KS#p(u4B(5*n?^K4-{i};^4IIC^H=_V0?uaOy=8&&9hM6uHsi*?@8 z-F@SeUN0wG1Ia)?9ZtApEGeA{blBlmeD;RgA2&3d z_^sO7OL4?~)42YP6$+7OFQ^?k9K<1~2|Uf;@|t7-!DSDkIFlPl@S8-LE&)gDEtdFU zQr_cV| zCqd@w8Fh6Tl5rCSpW=q0D-zlAr@p>>uI=htiEr4AZ&C7|Oi*jP6Z#C4BS|(K^`it= zmPe*?aHdG&@#(d^VlijhHI0q`g#Wg5T7%Wv%-R<_KH!nE5J6`b#+;@D{~hVc+xG5# zekc~>&T!6ZDRy|J$dL^TSJ?q^oiLS}0jv~ zmX%GaH>k0Ja|VtN3@#jM+IVSQ-J+##_t)^Bhz^H2!_5a|6H~Gsze`DUlx5c>g1&g% zzdIcM_%*-(mHYbp*ZE^HdM^-c%6JAcLCJcG){+8`@+6g`2dJ`nW%-H`{B{n~N;HuX z@_w<)HS=_jXW@}9*P;fSZJyg?sx8OAH1{VG13j3ve;5d?!}mRa8t}?sm5x0e6@g4#yk}+)ey< zQFzG7BVQ=jSfT()lh zX5hU7os$P8YRV2g-X&?W8Ib$vGJ>3q^^ick!rQ-s#IbEOOVL` zbydtz#xvF0Ck=R2>wU!l^?qJeuTY>8Xa)kG9nzVgo00CF!xRE5G zp?VI;KW6iD0uhmy|3yLJ_?5=;*$Xov@gUF1oTDAOqnhO#VgxTZ=x^Jmrj;_81Oim3 z)8kU_9(Q+lcX#)*?(XjHyZJYv6p~r_@taSEi2hH2_dl^B8V?bfgM2~M#s1|x4xhbr zoi3fl#&Tfop@=d731Kj0;xn!N+8_zX`FZ<3`8Y@G!$?A5vLG@15F7afN9UiDWz!us%``@=j}MP zZ$ki7CjwaVQJi9O0IDtou=Mjd)wC2q)qxzZVFaof>CDV`;?!HtXsT5s+xcQ_ zz!yn$jeO4Ej*Wz+OSBg^otQhz%(i=^FUimZxH4k`;ekDvb90X%2Kz zNaNAb z_r*rG%~*l5#am_q7Ao`erI*YEn3po$Jou5BXs}tA1uZLnF_V=RC<}SpUuGgMVp+EB zSZwHHP7qN=9p_EuY($8Z>%3{In`F!K-FHnT4$BIqho)jIkcU2(9+}D^OIhzdQ{CfA zrQ4>$QW6+7C@)g$YzO=trf|bAOqG7)z0ORV!$j zzVsz?z*5^1P<1HTuHjwZnc_QglJ#-Cx-e<(g+I-JFxRs7#;w9)-?FF8z}6L!#)lGh zjjVse1PCJ%`Cbi47beW!`EmNh&Yr-UYJn6qPnm!4>$IU0ZU*vd>t~A77asjCUEhhF z&KVzTE!nod#mB!%6T@QHCO+0)XWRSu(8uW!VAr12SVw9^;CRoQX<@gx4~i#J^PI7#A=EfhlEOQs5P?NjC~-}&(Cq##>@DebvHc}XRmY3}J?xN7gC&ys)+ zR@wfZX4d2-n1ZM{acI%1UDsd#tv+ID7vRuDjsoQ+oBS}}K5=m2%8f@Jy!lhD93yR9 zX$L1lJ{098o^+UR>+PGfbk(MVm!E#?%isU1hTNBLu^ik4OXu|UwqdB1mrC-(TuayF z0a&tP&F0-l&)t0R%Et0fou%1bRtkjb}pO`I__XTj3tD_5;v zD^Lb%mT{z*mGuCD8yOA_6poyyOB_P&gna1^@sM9sr#IDqsL$06u{32Xq(z6;*|34Pbiw!8hm@NcdU0Hdkcwt~N^`hfEQ=KaV7+vX|~=68B=i;ymMqKh&VsEc}oT`Ttj$mMX^@zKgmQ`o&onxOeiBHYd=m{6Xhg z=gxK3UE%#lJvv{cB3cnH>0qf|wz0Hk323Ia^UU(&e+7N&z<7Pf3taxke`*lO2AHM= zwK|BU2ObcV%YcW`1u*gH>|t#ht!$!%5KanD<=Q`|{99hy@%U%pqGU=V0>(5GglC*$ zN)BRlrV`$I0!^*TGXLT|a|FTw0RH_pZbu9IgWVp+01GXT)j?wOE?E&&XipidV{Vw~ zfC2B`76sUa3!?NC#K}`ux)a@lqU08;d=}(`Ao7EorGQe`OdSXJ;JzM7LA(g56DuJy ze%`Q-dWuBY8R}c`SmX)Zknaommri!xV|l={A&=A{5h~k(tllvKA)hPvI8QbUC*G=N zNe7sVD9&Zvi<Ne9+y>g|iM;-WoK{&e(X zZn~`}KV8Y~kQ4si2=}-q&a;xm=H_=gl^*d4+J1x+{))x=N7yFO{~3Uo=3ck|BM<>Y;elxn+{0TfhS}o4EM(goDaI=3;KoNm8mITKRXNg zSILuP4dOc}e7OS8^Gm8z;~lmo8xVt_f+zVg_)b@gtNm!SZebeA4gi2F;y*_U4_hcY z+jA0H^4WoZ)wRs>k&q4Svv{gl-Cq^^qPv*>ZYL}NB>IMt9A4W&a<~>@CsacnSH8f5 zTAYP(eBp3OIitnK($i`p3e-F`$LH*t=wM+~>3G$MF_(Yo|K5F3g!leHN;tNvLGo6W zMRHfS2$P(OpaUSci%sT#?wg*2{@uw*(o5B$`B%FATl0b7Q)76ntP*m{{+7!#%dlH@UQ~ zNqe_7-^e-@5{P}|mr7^s0idtU6$%K-U4QGqeEv3arQK)PSOZ{zR62ekKHa(q9{Ql0 zW)mQ+L__n95k3nd2xokHtux39XrGjGgVxasKB#>z~n$e!>8BzGPm}d94XA=a1Iljrv~p zJf4bZ_?^dm^{5^37!N_t=QFJC%ZRDp$rjAchq-U)Jgc&s6Jq^EcS5RHQzH_js&Urs wMwt=o9y~;LIi$FDKzB?B3-@fpSnlky|9JmS(kHho;fdQMQUbT>1viuc05J=xy8r+H literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/tachiyomi_circle.png b/app/src/main/res/drawable-xxhdpi/tachiyomi_circle.png deleted file mode 100644 index dd8cad897a353368aab8b7ec1e6b618728199fe8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11494 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 diff --git a/app/src/main/res/drawable-xxhdpi/tachiyomi_circle.webp b/app/src/main/res/drawable-xxhdpi/tachiyomi_circle.webp new file mode 100644 index 0000000000000000000000000000000000000000..c5e2b696b5d98149dfc77df85d3ee96dd66668ac GIT binary patch literal 4398 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.png b/app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.png deleted file mode 100644 index 1c1b2d8cc6783e0087f29e980e57df93ad1f2636..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16271 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 diff --git a/app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.webp b/app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.webp new file mode 100644 index 0000000000000000000000000000000000000000..74705a6722fae7e58cd984a8959a86070dc40fc8 GIT binary patch literal 6146 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 UiVsvwCEb7*(xV?5000000LCRaJ^%m! literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/navigation_header.xml b/app/src/main/res/layout/navigation_header.xml index fb62144d7..6080e4835 100644 --- a/app/src/main/res/layout/navigation_header.xml +++ b/app/src/main/res/layout/navigation_header.xml @@ -1,15 +1,14 @@ + android:layout_width="match_parent" + android:layout_height="@dimen/navigation_drawer_header_height" + android:gravity="bottom" + android:theme="@style/ThemeOverlay.AppCompat.Dark"> - + android:background="@color/colorPrimary" /> + android:src="@drawable/tachiyomi_circle" /> - \ No newline at end of file + From 489f981e4092d3eb23ed3e1f8ee2eb238ce2860f Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 18:53:33 -0500 Subject: [PATCH 032/675] Convert tracker icons to webp --- .../tachiyomi/data/track/anilist/Anilist.kt | 2 +- app/src/main/res/drawable-xxxhdpi/al.png | Bin 2444 -> 0 bytes app/src/main/res/drawable-xxxhdpi/anilist.webp | Bin 0 -> 2048 bytes app/src/main/res/drawable-xxxhdpi/bangumi.png | Bin 6388 -> 0 bytes app/src/main/res/drawable-xxxhdpi/bangumi.webp | Bin 0 -> 826 bytes app/src/main/res/drawable-xxxhdpi/kitsu.png | Bin 7091 -> 0 bytes app/src/main/res/drawable-xxxhdpi/kitsu.webp | Bin 0 -> 8734 bytes app/src/main/res/drawable-xxxhdpi/mal.png | Bin 2267 -> 0 bytes app/src/main/res/drawable-xxxhdpi/mal.webp | Bin 0 -> 1404 bytes app/src/main/res/drawable-xxxhdpi/shikimori.png | Bin 8847 -> 0 bytes app/src/main/res/drawable-xxxhdpi/shikimori.webp | Bin 0 -> 1494 bytes 11 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 app/src/main/res/drawable-xxxhdpi/al.png create mode 100644 app/src/main/res/drawable-xxxhdpi/anilist.webp delete mode 100644 app/src/main/res/drawable-xxxhdpi/bangumi.png create mode 100644 app/src/main/res/drawable-xxxhdpi/bangumi.webp delete mode 100644 app/src/main/res/drawable-xxxhdpi/kitsu.png create mode 100644 app/src/main/res/drawable-xxxhdpi/kitsu.webp delete mode 100644 app/src/main/res/drawable-xxxhdpi/mal.png create mode 100644 app/src/main/res/drawable-xxxhdpi/mal.webp delete mode 100644 app/src/main/res/drawable-xxxhdpi/shikimori.png create mode 100644 app/src/main/res/drawable-xxxhdpi/shikimori.webp diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index 1f862cfef..fde18a22d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -52,7 +52,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { } } - override fun getLogo() = R.drawable.al + override fun getLogo() = R.drawable.anilist override fun getLogoColor() = Color.rgb(18, 25, 35) diff --git a/app/src/main/res/drawable-xxxhdpi/al.png b/app/src/main/res/drawable-xxxhdpi/al.png deleted file mode 100644 index 6529ad67852f4bb08605172d5bcba2c202149658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2444 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 diff --git a/app/src/main/res/drawable-xxxhdpi/anilist.webp b/app/src/main/res/drawable-xxxhdpi/anilist.webp new file mode 100644 index 0000000000000000000000000000000000000000..b553d6c497d12fdd6ba931a7672e3c17eb88b7f8 GIT binary patch literal 2048 zcmV+b2>H3usB4{5hH8IlxZ z+qP}nwr$(?nq3vYc+eTqo$+nYM)ZFI;Q#;sF&v$uKH)vrxD+cB19jzagX%58Z=@{# zKH|a<${}g$R@2mY_g8Wk}sCiUqP82SND#Cc{iY)FS<`AhsAh$syEWfKOs*M~>gb zaLmGxLOj~IW_m<*C8<=DRKKJg?nBMSp`>Oc<@Hlil_;qdNd^9uR1->S zUsBPgq`FY~qeRZsm7DTNMCEsmNAl=ZHwGQzaJW%d01>7%GEwVlxsG)8g-Vd@!p&iv1d;r#^aIEgD`$uYp5RdBT;iCb6 zWza)V^&wc3!Ie5oz{hGtkc#Se;p6^)BrrhGVFj!R!10bC&YEfgWTW~Eu-5mt{w4@| zt%4=(xqc!@x2{TiMX3IBY_!$M7TwJd*r?2_>~Jsc2JDT_~wD zNhPABdL`v>BdJW3)QDYrxMq2})EUgp`PnY<{a(Q=AN6j@;x(l?+-2qQMR$fjr!v%j zI;REwer#B1O)kro{&H>M6 zjZBb~G6^y4K@JbsPZZxgn83C@NaRt3l+(bq!47is)AkG8;0PrFJodGJW z0IUE$kw%_NC8MIDrBZ3ouo4MnZsBbU3#1lH^$KaIFnJ^h7`evg2HFaZAkcmNi3+Mn*M&tw?}46d3S7eF-|!ZFS;Bfcs; zM%6SFZJB#XY(_T|FOB&qYUA}7O!v^Rl{1Bl*%6Ugv(tZ7k^$r~w`k^sdhR3``IUjugas7gUs~A*&HrNb4oG-r)CV;rQxuVO5;IMKFuk|=g^*oX3Lpz(Ntsl5FDe2ysB6gW${3RkhTm87p zFUPDVA%L;{RLJCPyb>%IgUf>BuJPOd<%+Do$T7AfZ2Q@l)65pF#DB^m=#g53yEC-F zF|@p{_JY7~L*R?nE|J90RvDE+Gf@gwc9={1?JmU_OXKu(i=ba%8_JPlq$MFiwnb9N zZ05nF$xAS6cYw+dK#OU+d#isKb8vLzdUrOZqM_f<2Y!ROR_hIpoSM{C`9h+G-7p{E>2)sk5p>I40~m^|4#h&Lsz$yCL-eqSOAgswX^Z! zNc0VU{rW7UT3FqZa%5GKriZXLg6|8_a}x0cT*5LE);bu>%(5+V-a!)VU-wcz?8a3^ zpRR|}r|4C#l!)OEuFq>A0V_rdu;iq;<>0Gi8aj+K1lz<-HDaj@lX%`}Oy=Wv^Tq*EDG@@Jwq&Lx1->OCjP e&Icj<{F-ZxvHVpiTT2g_{!}xOBzd5A00029tOO7M literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/bangumi.png b/app/src/main/res/drawable-xxxhdpi/bangumi.png deleted file mode 100644 index 412a28ba1c2a3ab9cb48d057d9bc9dfcedafc0ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6388 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 E0H_6rumAu6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/kitsu.png b/app/src/main/res/drawable-xxxhdpi/kitsu.png deleted file mode 100644 index bb9caec08af5c08c705f2b74332fe3ef93dfae34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7091 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 I8;}410F5>AhyVZp literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/mal.png b/app/src/main/res/drawable-xxxhdpi/mal.png deleted file mode 100644 index b148f40be0a26b96aeff199d1033da02c2e26e83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2267 zcmeH|`#Td18^@PJnDd-Pm>#EkSW;4QSj3dflE}p5oHxr^gqTolNsol&Y$)f$G=|M# zju}gfMl(uI%_@gXUS03|KfJ%ZpXfq*ap#nH*HV~nNosb%`~ z%Day%lg!okGpmf5)u~Tw)2weyHfMI8GrO?c)Pp1zxUbO+uOg$|M{OM&~AJ# zZhr|8w5@9>03bp6(+B#4@!tEUQkb<%*p*=4ut-#h4*-n{xE-c{+b2xjNFSnq-0-}> ztrY-3@Dj|*(kbf3YW{Bggq2Ev?eeL?HxG4Oe4H|E)T+5UW5J zN`clBMSX&)`;oM#6}cWW?Zml4HHw=*|3;BYJ$63&>s+k#=+V?tqZZmR=*1}B@bH}c z87kpngt|Q{GC<%tW(lb5ctrGCZtE%9X{?-)U?q@vf@)>gSuA4z1kLV~xL)O77;f=~ z{DLhkSWZi{)HUU@K)HXR1y%XRX~r5}r`xm&*)k-fWN>%r)FaPVHaSA{r&RUVkFkwQ}TRbL?`$sgExS$GUA zL&5|}9WDvR9mBl$fXck|fd8W|ZInX&leF{kO`gMRn{spQ#Jb>;McuuZBJqa*zk zCLgXOOisCOdvIR*SB$qN&m%rA=97{hB8pY-BLJ54b5Jc%%n;P(P%kj6~Sq5MFOO%vn^MBK)?9+FesLZyOb+YMq^zYKg@-6!&%4j4i;n zfPu$0GdBh@>tdawbfOj-$1Z6X@0Uj6iJi^ZUy(bUImc?n6_( zMUFB7FBGGP0?&dAlf}ubu5i)W7@cGp1>RgeRFVJvxnC>-!X>mHD|QxR6T|<$^JMII zN8n!ixPIOF9J;f6%H5#lB3Z0!BfT4h8fTa$X%@=LM-~L$d;~E`eQRciy3@-(c|>fI zDOn8%s#)mHX&w>VmewLCwVDWsnSkWiN))6a`(evBOWCx9o7<4TmB?#+pyX(?lmn#| zGRNj^bA)S!hcycavN)1D4}uz|Lt`T}Rff)o(R!Nd2>chgX7?RgwPV&Y6XdHb%oB4e z9zqm3B|Sm5V#t%UNOIl?f@)B~L?3~pfWMHsS8fj!#c9bv^m$rU(zEJm5Z&RGl<_VC zM7qHB^v+{Uj;zG9D<_W|#L6wCez%2+0f%bt+6{{u!?7+GKwj{u1FB8O4KDROqg_`2 zdK10{I^%yrfTcv&+={VEBHN5v=4lA4S-zN41bbeN;wWuMVFq3bg9(qyOi;HSN)A$_ z%y0?Ct*&5lKBRdWnH77m+1vG>f+^)N4*ui41Lvl9Fr z$PNZ>BAqJro6&%Y1i{Z`BqUVQ4GU=;)?m}J*3V|AA3a~b7csR_Q67ir3(-nqg5s&p zW#3`n2I&R*>}gR8X1Lm&+KBo5Z4;KC4A-74)?D~sJaJI!p26#z2)@Z~`k%J|v%YLq I1NBV&Hw#>^I{*Lx diff --git a/app/src/main/res/drawable-xxxhdpi/mal.webp b/app/src/main/res/drawable-xxxhdpi/mal.webp new file mode 100644 index 0000000000000000000000000000000000000000..a9c6f2e929d02681b82bea00eacb2b96c3858858 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&goH1polBAOM{KD)0bg06vjGok^vmqaiC$>R7N6 z2~FFp&7b%5+REN*mv=qyXZYV%`(-@EdB1vH-k#o=$ZYWU_|DrazTaPJ-P(8&rDkS6 zKYpiJlIkso_nJbLDpu`VpMPz6pN{;IE+;RQhI1mR|Q(`~{C(RZ>%uD=6@ z7tGJrM>PT@MtjzM^Ceo`E}tCNuXRezgZIEl3O$gp?ksx3magU4z%e?|U>?V!ZVY}@ zkb7VJ&Rb-et6`1Z?%pyiwAL*l^HzA;NrMxzK*DC_B=2h_NQP(ZQdKXi&a~{n%f3SE;xy2N#C;zE{p$7$Jq`jV-H*#SCBl_gDrHb0@P|W1z4XY;0g!;@5S}o zhjeqJX>=dAoz0mbo86bY#;=OBNz&!R-xF2p{%_MNJC$(|fj>2?d!&N{U%8Qrbao|A zF~MNv*}?e%uN~D3cTg2Uf_G7-Tm<*8Q|I!I<2bVC3#Ue$7K@Nya^90Ue$Drx*CAP>YamTp|yg$fgFGR8SYof3S+ype3T11V%r| z?-oJm5g+(bK#-&DJN}jGtnj~K@^F)S@~UQ%5%F?M_$C%E#1|oF9$6DIbzAGBus22z zX*Om3_pE*@?eDXZx*Me5|Jlv7%Dv7SsYe+LG=V|F8$WIBv}m42xp~;Y%-SiICwaF) zt;}7ckdq!w9|KFyyu7-tUrFFO5WXTh04}iD@9Zvaew;FlNfAWrD!-xE|H{xDBUE%{ z-?GoFsc5`Q&0uS`xyz)^ikVn`@4s0pm6;;k12ac~27)iYpNr(v{<=zCkYR>$3I$L? zV$ke?Ijgf?8s#(GUO={dCG9h|rVgVNiOMrOrG89BX)&S}Z?}7WQxz3&Y+~XV=Ls%l zj@cE$ne6`m2KnGbv-_zfeG`5#`rC^Zn*UTwyRO{#Sfn;&F=+cEk({<_$*CNiw@9wD z#sOU9Cn4N^*VQifLMQNZjb$Oj)Bm)TzIF~{+(fz0VTwrP_mC}oLR*fNIg$wV^RBzk zFc@ey-8pCUTR`Xi8K?`cMVOUbqc#=f+zMi}-UnERa}z!+kT>r;RnIw`y3? z>pc$GForu|zP}`35ARyC{r%-`niWj9X&b|CYucfF#htw>xp~ z@9DPLa{S+rORMk|RWZBYb&6+hSO2cKg?qgNfuEwxd5t+1FYBnUO*K&Z`^7sQ4P$S4 zZt0DgDMVg)?LQ>&CQ0_VJfNY)G%563C0G_E23IB`>}vWw%^NWMRaeS0T{!W%bmvShF>%wC#ZZ!P$U zXh+1FsYXySbczLnW6M8qYxnC^-&=TYDEgEH<3)%Pa*7gk~s$K>;&tF_H9ZG@0hmy*LQkw~wnLMHA`pa9wc K1y}$80001V0ljko literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/shikimori.png b/app/src/main/res/drawable-xxxhdpi/shikimori.png deleted file mode 100644 index 9859d16e6a7cb27d56117bf3b27bbd123c32c2ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8847 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 literal 0 HcmV?d00001 From eb56567812c3c4f7fde289b8b3d62f15862f0dca Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 18:55:53 -0500 Subject: [PATCH 033/675] Convert filter mock image to webp --- app/src/main/res/drawable/filter_mock.png | Bin 342126 -> 0 bytes app/src/main/res/drawable/filter_mock.webp | Bin 0 -> 61027 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/src/main/res/drawable/filter_mock.png create mode 100644 app/src/main/res/drawable/filter_mock.webp diff --git a/app/src/main/res/drawable/filter_mock.png b/app/src/main/res/drawable/filter_mock.png deleted file mode 100644 index a6d6a0c00aaa09eb696b23be79e38b2937321fad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 342126 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

mb?zl#FWH$fxvCf=BWTX3-V&-Ru5|V}H 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 DA)IjCS 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 diff --git a/app/src/main/res/drawable/filter_mock.webp b/app/src/main/res/drawable/filter_mock.webp new file mode 100644 index 0000000000000000000000000000000000000000..6bfbe6dc87beb2b29729bcb9cdbb0a00164e664c GIT binary patch literal 61027 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 E0Kwn35dZ)H literal 0 HcmV?d00001 From f7669b67972acbf61a6c9e589a2bd5c50e72b0cd Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 19:10:55 -0500 Subject: [PATCH 034/675] Replace left/right layout attributes with start/end --- .../catalogue/SourceDividerItemDecoration.kt | 4 +- .../ExtensionDividerItemDecoration.kt | 4 +- .../res/layout-land/manga_info_controller.xml | 92 +++++++------- .../layout-land/reader_color_filter_sheet.xml | 8 +- .../res/layout/catalogue_drawer_content.xml | 12 +- .../catalogue_global_search_controller.xml | 23 ++-- ...atalogue_global_search_controller_card.xml | 4 +- .../main/res/layout/catalogue_grid_item.xml | 24 ++-- .../main/res/layout/catalogue_list_item.xml | 115 +++++++++--------- .../layout/catalogue_main_controller_card.xml | 4 +- .../catalogue_main_controller_card_item.xml | 12 +- app/src/main/res/layout/categories_item.xml | 6 +- .../res/layout/changelog_header_layout.xml | 11 +- .../main/res/layout/changelog_row_layout.xml | 17 +-- .../main/res/layout/chapters_controller.xml | 4 +- app/src/main/res/layout/chapters_item.xml | 29 ++--- .../layout/common_dialog_with_checkbox.xml | 3 - app/src/main/res/layout/download_item.xml | 12 +- .../main/res/layout/extension_card_header.xml | 4 +- .../main/res/layout/extension_card_item.xml | 9 +- .../layout/extension_detail_controller.xml | 30 ++--- .../main/res/layout/manga_info_controller.xml | 102 ++++++++-------- app/src/main/res/layout/navigation_header.xml | 2 +- .../res/layout/navigation_view_checkbox.xml | 6 +- .../layout/navigation_view_checkedtext.xml | 4 +- .../main/res/layout/navigation_view_group.xml | 6 +- .../main/res/layout/navigation_view_radio.xml | 6 +- .../res/layout/navigation_view_spinner.xml | 4 +- .../main/res/layout/navigation_view_text.xml | 4 +- app/src/main/res/layout/pref_item_source.xml | 15 ++- .../main/res/layout/pref_library_columns.xml | 6 +- .../main/res/layout/reader_color_filter.xml | 78 ++++++------ app/src/main/res/layout/reader_page_sheet.xml | 9 -- .../main/res/layout/reader_settings_sheet.xml | 52 ++++---- .../main/res/layout/recent_chapters_item.xml | 23 ++-- .../layout/recent_chapters_section_item.xml | 3 - .../main/res/layout/recently_read_item.xml | 4 +- app/src/main/res/layout/track_item.xml | 40 +++--- app/src/main/res/layout/track_search_item.xml | 17 +-- 39 files changed, 366 insertions(+), 442 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt index 1b3349616..6d4fb6fbe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt @@ -27,8 +27,8 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio val params = child.layoutParams as RecyclerView.LayoutParams val top = child.bottom + params.bottomMargin val bottom = top + divider.intrinsicHeight - val left = parent.paddingLeft + holder.margin - val right = parent.width - parent.paddingRight - holder.margin + val left = parent.paddingStart + holder.margin + val right = parent.width - parent.paddingEnd - holder.margin divider.setBounds(left, top, right, bottom) divider.draw(c) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDividerItemDecoration.kt index 24c7c0a43..e70bcccca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDividerItemDecoration.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDividerItemDecoration.kt @@ -27,8 +27,8 @@ class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecora val params = child.layoutParams as RecyclerView.LayoutParams val top = child.bottom + params.bottomMargin val bottom = top + divider.intrinsicHeight - val left = parent.paddingLeft + holder.margin - val right = parent.width - parent.paddingRight - holder.margin + val left = parent.paddingStart + holder.margin + val right = parent.width - parent.paddingEnd - holder.margin divider.setBounds(left, top, right, bottom) divider.draw(c) diff --git a/app/src/main/res/layout-land/manga_info_controller.xml b/app/src/main/res/layout-land/manga_info_controller.xml index 2eed51af8..130fd4afe 100644 --- a/app/src/main/res/layout-land/manga_info_controller.xml +++ b/app/src/main/res/layout-land/manga_info_controller.xml @@ -21,24 +21,23 @@ android:layout_height="0dp" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" - android:layout_marginLeft="16dp" + android:layout_marginStart="16dp" android:contentDescription="@string/description_cover" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintDimensionRatio="h,3:2" tools:background="@color/material_grey_700" - app:layout_constraintVertical_bias="0.0" - android:layout_marginStart="16dp"/> + app:layout_constraintVertical_bias="0.0" /> + app:layout_constraintEnd_toEndOf="parent"/> + app:layout_constraintStart_toEndOf="@+id/manga_cover" + app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toEndOf="@+id/manga_author_label" + app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toEndOf="@+id/manga_artist_label" + app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toEndOf="@+id/manga_chapters_label" + app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toEndOf="@+id/manga_last_update_label" + app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toEndOf="@+id/manga_status_label" + app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toEndOf="@+id/manga_source_label" + app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginEnd="64dp"/> - + android:layout_marginEnd="64dp"/> diff --git a/app/src/main/res/layout-land/reader_color_filter_sheet.xml b/app/src/main/res/layout-land/reader_color_filter_sheet.xml index 9cf10846d..75c1f8424 100644 --- a/app/src/main/res/layout-land/reader_color_filter_sheet.xml +++ b/app/src/main/res/layout-land/reader_color_filter_sheet.xml @@ -12,8 +12,8 @@ android:id="@+id/frame" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintRight_toLeftOf="@id/scroll" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/scroll" app:layout_constraintTop_toTopOf="@id/scroll" app:layout_constraintBottom_toBottomOf="@id/scroll"> @@ -40,8 +40,8 @@ android:id="@+id/scroll" android:layout_width="0dp" android:layout_height="wrap_content" - app:layout_constraintLeft_toRightOf="@id/frame" - app:layout_constraintRight_toRightOf="parent"> + app:layout_constraintStart_toEndOf="@id/frame" + app:layout_constraintEnd_toEndOf="parent"> diff --git a/app/src/main/res/layout/catalogue_drawer_content.xml b/app/src/main/res/layout/catalogue_drawer_content.xml index b2b621a9c..b4bf2e16b 100644 --- a/app/src/main/res/layout/catalogue_drawer_content.xml +++ b/app/src/main/res/layout/catalogue_drawer_content.xml @@ -6,6 +6,7 @@ android:layout_height="match_parent" android:clickable="true" android:orientation="vertical"> + + android:paddingStart="?attr/listPreferredItemPaddingStart" + android:paddingEnd="?attr/listPreferredItemPaddingEnd"> + + + - \ No newline at end of file + + diff --git a/app/src/main/res/layout/catalogue_global_search_controller.xml b/app/src/main/res/layout/catalogue_global_search_controller.xml index 4f9ab29f8..7959047a9 100644 --- a/app/src/main/res/layout/catalogue_global_search_controller.xml +++ b/app/src/main/res/layout/catalogue_global_search_controller.xml @@ -1,15 +1,16 @@ - - + android:layout_height="wrap_content"> + + + diff --git a/app/src/main/res/layout/catalogue_global_search_controller_card.xml b/app/src/main/res/layout/catalogue_global_search_controller_card.xml index 60c6ce496..3b38de0f1 100644 --- a/app/src/main/res/layout/catalogue_global_search_controller_card.xml +++ b/app/src/main/res/layout/catalogue_global_search_controller_card.xml @@ -14,7 +14,7 @@ android:padding="@dimen/material_component_text_fields_padding_above_and_below_label" app:layout_constraintBottom_toTopOf="@+id/source_card" app:layout_constraintHeight_default="wrap" - app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Title" /> @@ -45,5 +45,7 @@ android:paddingStart="4dp" android:clipToPadding="false" tools:listitem="@layout/catalogue_global_search_controller_card_item" /> + + diff --git a/app/src/main/res/layout/catalogue_grid_item.xml b/app/src/main/res/layout/catalogue_grid_item.xml index 614a0bbd1..5b833d32f 100644 --- a/app/src/main/res/layout/catalogue_grid_item.xml +++ b/app/src/main/res/layout/catalogue_grid_item.xml @@ -41,14 +41,14 @@ android:layout_height="wrap_content" android:background="@color/colorAccentDark" android:paddingBottom="1dp" - android:paddingLeft="3dp" - android:paddingRight="3dp" + android:paddingStart="3dp" + android:paddingEnd="3dp" android:paddingTop="1dp" android:visibility="gone" tools:visibility="visible" tools:text="120" - app:layout_constraintLeft_toRightOf="@+id/download_text" - android:layout_marginLeft="4dp" + app:layout_constraintStart_toEndOf="@+id/download_text" + android:layout_marginStart="4dp" app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="4dp"/> diff --git a/app/src/main/res/layout/catalogue_list_item.xml b/app/src/main/res/layout/catalogue_list_item.xml index b83795268..09b4e9943 100644 --- a/app/src/main/res/layout/catalogue_list_item.xml +++ b/app/src/main/res/layout/catalogue_list_item.xml @@ -5,22 +5,23 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height" - android:layout_gravity="center_vertical" + android:layout_gravity="center_vertical" android:background="?attr/selectable_list_drawable" tools:layout_editor_absoluteY="25dp" tools:layout_editor_absoluteX="0dp"> + + android:layout_marginStart="8dp"/> @@ -49,8 +49,8 @@ android:layout_height="wrap_content" android:background="@color/md_teal_500" android:paddingBottom="1dp" - android:paddingLeft="3dp" - android:paddingRight="3dp" + android:paddingStart="3dp" + android:paddingEnd="3dp" android:paddingTop="1dp" android:layout_centerVertical="true" android:maxLines="1" @@ -58,58 +58,57 @@ android:visibility="gone" tools:visibility="visible" android:layout_marginEnd="8dp" - app:layout_constraintRight_toLeftOf="@+id/unread_text" + app:layout_constraintEnd_toStartOf="@+id/unread_text" app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="8dp" app:layout_constraintBottom_toBottomOf="parent" android:layout_marginBottom="8dp"/> - + - - - + + diff --git a/app/src/main/res/layout/catalogue_main_controller_card.xml b/app/src/main/res/layout/catalogue_main_controller_card.xml index aec409b0a..e0ba18d4a 100644 --- a/app/src/main/res/layout/catalogue_main_controller_card.xml +++ b/app/src/main/res/layout/catalogue_main_controller_card.xml @@ -12,7 +12,7 @@ android:layout_height="wrap_content" android:paddingTop="8dp" android:paddingBottom="8dp" - android:paddingLeft="@dimen/material_component_text_fields_padding_above_and_below_label" + android:paddingStart="@dimen/material_component_text_fields_padding_above_and_below_label" tools:text="Title" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/catalogue_main_controller_card_item.xml b/app/src/main/res/layout/catalogue_main_controller_card_item.xml index 071c89ad7..faf7915d0 100644 --- a/app/src/main/res/layout/catalogue_main_controller_card_item.xml +++ b/app/src/main/res/layout/catalogue_main_controller_card_item.xml @@ -19,7 +19,7 @@ android:padding="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1:1" - app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:src="@mipmap/ic_launcher_round" /> @@ -28,16 +28,14 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:maxLines="1" - android:paddingLeft="0dp" android:paddingStart="0dp" - android:paddingRight="8dp" android:paddingEnd="8dp" android:ellipsize="end" android:textAppearance="@style/TextAppearance.Regular.SubHeading" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintLeft_toRightOf="@+id/image" + app:layout_constraintStart_toEndOf="@+id/image" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintRight_toLeftOf="@+id/source_latest" + app:layout_constraintEnd_toStartOf="@+id/source_latest" tools:text="Source title"/>