From b4f93fc0a504b23558a088d2fb25ff36f6674b1d Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 26 Aug 2022 10:06:50 +0300 Subject: [PATCH 1/3] Fix showing reader control by long press --- .../java/org/koitharu/kotatsu/utils/GridTouchHelper.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/GridTouchHelper.kt b/app/src/main/java/org/koitharu/kotatsu/utils/GridTouchHelper.kt index 13ccd3fa7..9605fb93b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/GridTouchHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/GridTouchHelper.kt @@ -7,7 +7,7 @@ import kotlin.math.roundToInt class GridTouchHelper( context: Context, - private val listener: OnGridTouchListener + private val listener: OnGridTouchListener, ) : GestureDetector.SimpleOnGestureListener() { private val detector = GestureDetector(context, this) @@ -16,7 +16,7 @@ class GridTouchHelper( private var isDispatching = false init { - detector.setIsLongpressEnabled(false) + detector.setIsLongpressEnabled(true) detector.setOnDoubleTapListener(this) } @@ -46,7 +46,7 @@ class GridTouchHelper( } 2 -> AREA_RIGHT else -> return false - } + }, ) return true } @@ -66,4 +66,4 @@ class GridTouchHelper( fun onProcessTouch(rawX: Int, rawY: Int): Boolean } -} \ No newline at end of file +} From 040d3e4433c2ab3d425846ecc5d66b606a3fffd3 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 26 Aug 2022 12:08:19 +0300 Subject: [PATCH 2/3] Reader control direction depends on mode #214 --- .../kotatsu/core/prefs/AppSettings.kt | 7 +- .../main/ui/protect/AppProtectHelper.kt | 3 +- .../kotatsu/reader/ui/ReaderActivity.kt | 13 ++-- .../reader/ui/ReaderControlDelegate.kt | 67 +++++++++++++------ app/src/main/res/values/strings.xml | 4 +- app/src/main/res/xml/pref_reader.xml | 6 ++ 6 files changed, 70 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index cd9c8a07a..2500fe656 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -14,9 +14,6 @@ import java.io.File import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.callbackFlow import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.network.DoHProvider @@ -64,6 +61,9 @@ class AppSettings(context: Context) { val readerPageSwitch: Set get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS) + val isReaderTapsAdaptive: Boolean + get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false) + var isTrafficWarningEnabled: Boolean get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true) set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) } @@ -314,6 +314,7 @@ class AppSettings(context: Context) { const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown" const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible" const val KEY_DOH = "doh" + const val KEY_READER_TAPS_LTR = "reader_taps_ltr" // About const val KEY_APP_UPDATE = "app_update" diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt index a92d866d2..45f7e9ea4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.app.Application import android.content.Intent import android.os.Bundle +import org.acra.dialog.CrashReportDialog import org.koitharu.kotatsu.core.prefs.AppSettings class AppProtectHelper(private val settings: AppSettings) : Application.ActivityLifecycleCallbacks { @@ -11,7 +12,7 @@ class AppProtectHelper(private val settings: AppSettings) : Application.Activity private var isUnlocked = settings.appPassword.isNullOrEmpty() override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - if (activity !is ProtectActivity && !isUnlocked) { + if (!isUnlocked && activity !is ProtectActivity && activity !is CrashReportDialog) { val sourceIntent = Intent(activity, activity.javaClass) activity.intent?.let { sourceIntent.putExtras(it) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 35d203b26..2bf5c641a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -19,6 +19,7 @@ import androidx.transition.TransitionManager import androidx.transition.TransitionSet import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -47,7 +48,6 @@ import org.koitharu.kotatsu.utils.GridTouchHelper import org.koitharu.kotatsu.utils.ScreenOrientationHelper import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.* -import java.util.concurrent.TimeUnit class ReaderActivity : BaseFullscreenActivity(), @@ -67,6 +67,9 @@ class ReaderActivity : ) } + override val readerMode: ReaderMode? + get() = readerManager.currentMode + private lateinit var touchHelper: GridTouchHelper private lateinit var orientationHelper: ScreenOrientationHelper private lateinit var controlDelegate: ReaderControlDelegate @@ -82,7 +85,7 @@ class ReaderActivity : supportActionBar?.setDisplayHomeAsUpEnabled(true) touchHelper = GridTouchHelper(this, this) orientationHelper = ScreenOrientationHelper(this) - controlDelegate = ReaderControlDelegate(lifecycleScope, get(), this) + controlDelegate = ReaderControlDelegate(get(), this, this) binding.toolbarBottom.inflateMenu(R.menu.opt_reader_bottom) binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected) insetsDelegate.interceptingWindowInsetsListener = this @@ -146,7 +149,7 @@ class ReaderActivity : ChaptersBottomSheet.show( supportFragmentManager, viewModel.manga?.chapters.orEmpty(), - viewModel.getCurrentState()?.chapterId ?: 0L + viewModel.getCurrentState()?.chapterId ?: 0L, ) } R.id.action_screen_rotate -> { @@ -317,12 +320,12 @@ class ReaderActivity : binding.appbarTop.updatePadding( top = systemBars.top, right = systemBars.right, - left = systemBars.left + left = systemBars.left, ) binding.appbarBottom?.updatePadding( bottom = systemBars.bottom, right = systemBars.right, - left = systemBars.left + left = systemBars.left, ) return WindowInsetsCompat.Builder(insets) .setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt index dbe853894..6fbf45e28 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt @@ -1,33 +1,39 @@ package org.koitharu.kotatsu.reader.ui +import android.content.SharedPreferences import android.view.KeyEvent import android.view.SoundEffectConstants import android.view.View -import androidx.lifecycle.LifecycleCoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.observeAsFlow +import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.utils.GridTouchHelper class ReaderControlDelegate( - scope: LifecycleCoroutineScope, - settings: AppSettings, - private val listener: OnInteractionListener -) { + private val settings: AppSettings, + private val listener: OnInteractionListener, + owner: LifecycleOwner, +) : DefaultLifecycleObserver, SharedPreferences.OnSharedPreferenceChangeListener { private var isTapSwitchEnabled: Boolean = true private var isVolumeKeysSwitchEnabled: Boolean = false + private var isReaderTapsAdaptive: Boolean = true init { - settings.observeAsFlow(AppSettings.KEY_READER_SWITCHERS) { readerPageSwitch } - .flowOn(Dispatchers.Default) - .onEach { - isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it - isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in it - }.launchIn(scope) + owner.lifecycle.addObserver(this) + settings.subscribe(this) + updateSettings() + } + + override fun onDestroy(owner: LifecycleOwner) { + settings.unsubscribe(this) + owner.lifecycle.removeObserver(this) + super.onDestroy(owner) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + updateSettings() } fun onGridTouch(area: Int, view: View) { @@ -41,7 +47,7 @@ class ReaderControlDelegate( view.playSoundEffect(SoundEffectConstants.NAVIGATION_UP) } GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) { - listener.switchPageBy(-1) + listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1) view.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT) } GridTouchHelper.AREA_BOTTOM -> if (isTapSwitchEnabled) { @@ -49,7 +55,7 @@ class ReaderControlDelegate( view.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN) } GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) { - listener.switchPageBy(1) + listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1) view.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT) } } @@ -72,17 +78,25 @@ class ReaderControlDelegate( KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, - KeyEvent.KEYCODE_DPAD_RIGHT -> { + -> { listener.switchPageBy(1) true } + KeyEvent.KEYCODE_DPAD_RIGHT -> { + listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1) + true + } KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP, KeyEvent.KEYCODE_DPAD_UP, - KeyEvent.KEYCODE_DPAD_LEFT -> { + -> { listener.switchPageBy(-1) true } + KeyEvent.KEYCODE_DPAD_LEFT -> { + listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1) + true + } KeyEvent.KEYCODE_DPAD_CENTER -> { listener.toggleUiVisibility() true @@ -97,8 +111,21 @@ class ReaderControlDelegate( ) } + private fun updateSettings() { + val switch = settings.readerPageSwitch + isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in switch + isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in switch + isReaderTapsAdaptive = settings.isReaderTapsAdaptive + } + + private fun isReaderTapsReversed(): Boolean { + return isReaderTapsAdaptive && listener.readerMode == ReaderMode.REVERSED + } + interface OnInteractionListener { + val readerMode: ReaderMode? + fun switchPageBy(delta: Int) fun toggleUiVisibility() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f33ed79f9..fb55d5185 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -325,4 +325,6 @@ Content not found or removed Downloading manga <b>%1$s</b> %2$s - \ No newline at end of file + Tap on the right edge or pressing the right key always switches to the next page + Ergonomic reader control + diff --git a/app/src/main/res/xml/pref_reader.xml b/app/src/main/res/xml/pref_reader.xml index 42e46b8ad..d8c4fe0c3 100644 --- a/app/src/main/res/xml/pref_reader.xml +++ b/app/src/main/res/xml/pref_reader.xml @@ -29,6 +29,12 @@ android:title="@string/switch_pages" app:allowDividerAbove="true" /> + + Date: Sat, 27 Aug 2022 09:37:38 +0300 Subject: [PATCH 3/3] Fix crashes --- app/build.gradle | 8 +++---- app/proguard-rules.pro | 4 +++- .../browser/cloudflare/CloudFlareDialog.kt | 5 +++-- .../settings/SourceSettingsFragment.kt | 11 ++++++---- .../org/koitharu/kotatsu/utils/ext/CoilExt.kt | 22 ++++++++++++++++++- .../koitharu/kotatsu/utils/ext/FragmentExt.kt | 20 +++++++++++++++++ 6 files changed, 58 insertions(+), 12 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ddd415d0f..fe37b7419 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 423 - versionName '3.4.11' + versionCode 424 + versionName '3.4.12' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -119,8 +119,8 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' - implementation 'ch.acra:acra-mail:5.9.5' - implementation 'ch.acra:acra-dialog:5.9.5' + implementation 'ch.acra:acra-mail:5.9.6' + implementation 'ch.acra:acra-dialog:5.9.6' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fb3509dc2..3aac6ee47 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -10,4 +10,6 @@ } -keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment -keep class org.koitharu.kotatsu.core.db.entity.* { *; } --dontwarn okhttp3.internal.platform.ConscryptPlatform \ No newline at end of file +-dontwarn okhttp3.internal.platform.ConscryptPlatform +-keep class org.koitharu.kotatsu.core.exceptions.* { *; } +-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt index 8c1de2625..6488a7a81 100644 --- a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt @@ -25,7 +25,7 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud override fun onInflateView( inflater: LayoutInflater, - container: ViewGroup? + container: ViewGroup?, ) = FragmentCloudflareBinding.inflate(inflater, container, false) @SuppressLint("SetJavaScriptEnabled") @@ -49,6 +49,7 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud override fun onDestroyView() { binding.webView.stopLoading() + binding.webView.destroy() super.onDestroyView() } @@ -77,7 +78,7 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud override fun onCheckPassed() { pendingResult.putBoolean(EXTRA_RESULT, true) - dismiss() + dismissAllowingStateLoss() } companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt index 3db0bb810..b5209896b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -2,6 +2,8 @@ package org.koitharu.kotatsu.settings import android.os.Bundle import android.view.View +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers @@ -47,7 +49,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { super.onViewCreated(view, savedInstanceState) findPreference(KEY_AUTH)?.run { if (isVisible) { - loadUsername(this) + loadUsername(viewLifecycleOwner, this) } } } @@ -62,7 +64,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { } } - private fun loadUsername(preference: Preference) = viewLifecycleScope.launch { + private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch { runCatching { preference.summary = null withContext(Dispatchers.Default) { @@ -89,11 +91,12 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { } } - private fun resolveError(error: Throwable): Unit { + private fun resolveError(error: Throwable) { viewLifecycleScope.launch { if (exceptionResolver.resolve(error)) { val pref = findPreference(KEY_AUTH) ?: return@launch - loadUsername(pref) + val lifecycleOwner = awaitViewLifecycle() + loadUsername(lifecycleOwner, pref) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt index 2fc05e365..28a7afa49 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt @@ -9,6 +9,7 @@ import coil.request.ImageResult import coil.request.SuccessResult import coil.util.CoilUtils import com.google.android.material.progressindicator.BaseProgressIndicator +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener @@ -45,9 +46,28 @@ fun ImageResult.toBitmapOrNull() = when (this) { } fun ImageRequest.Builder.referer(referer: String): ImageRequest.Builder { - return setHeader(CommonHeaders.REFERER, referer) + if (referer.isEmpty()) { + return this + } + try { + setHeader(CommonHeaders.REFERER, referer) + } catch (e: IllegalArgumentException) { + val baseUrl = referer.baseUrl() + if (baseUrl != null) { + setHeader(CommonHeaders.REFERER, baseUrl) + } + } + return this } fun ImageRequest.Builder.indicator(indicator: BaseProgressIndicator<*>): ImageRequest.Builder { return listener(ImageRequestIndicatorListener(indicator)) +} + +private fun String.baseUrl(): String? { + return (this.toHttpUrlOrNull()?.newBuilder("/") ?: return null) + .username("") + .password("") + .build() + .toString() } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt index d37b579e7..f330af2ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt @@ -7,8 +7,12 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer import androidx.lifecycle.coroutineScope import java.io.Serializable +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine inline fun T.withArgs(size: Int, block: Bundle.() -> Unit): T { val b = Bundle(size) @@ -49,4 +53,20 @@ fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) { fun Fragment.addMenuProvider(provider: MenuProvider) { requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED) +} + +suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner = suspendCancellableCoroutine { cont -> + val liveData = viewLifecycleOwnerLiveData + val observer = object : Observer { + override fun onChanged(result: LifecycleOwner?) { + if (result != null) { + liveData.removeObserver(this) + cont.resume(result) + } + } + } + liveData.observeForever(observer) + cont.invokeOnCancellation { + liveData.removeObserver(observer) + } } \ No newline at end of file