diff --git a/app/build.gradle b/app/build.gradle index c7065c93a..dafd6b900 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,16 +7,16 @@ plugins { } android { - compileSdkVersion 32 - buildToolsVersion '32.0.0' + compileSdkVersion 33 + buildToolsVersion '33.0.0' namespace 'org.koitharu.kotatsu' defaultConfig { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 - targetSdkVersion 32 - versionCode 490 - versionName '4.0-a1' + targetSdkVersion 33 + versionCode 493 + versionName '4.0-a4' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -29,6 +29,8 @@ android { // define this values in your local.properties file buildConfigField 'String', 'SHIKIMORI_CLIENT_ID', "\"${localProperty('shikimori.clientId')}\"" buildConfigField 'String', 'SHIKIMORI_CLIENT_SECRET', "\"${localProperty('shikimori.clientSecret')}\"" + resValue "string", "acra_login", "${localProperty('acra.login')}" + resValue "string", "acra_password", "${localProperty('acra.password')}" } buildTypes { debug { @@ -80,7 +82,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:8709c3dd0c') { + implementation('com.github.KotatsuApp:kotatsu-parsers:f112a06ab6') { exclude group: 'org.json', module: 'json' } @@ -88,7 +90,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.activity:activity-ktx:1.5.1' - implementation 'androidx.fragment:fragment-ktx:1.5.1' + implementation 'androidx.fragment:fragment-ktx:1.5.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-service:2.5.1' @@ -100,7 +102,7 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04' - implementation 'com.google.android.material:material:1.7.0-alpha03' + implementation 'com.google.android.material:material:1.7.0-beta01' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1' @@ -115,18 +117,19 @@ dependencies { implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' - implementation "com.google.dagger:hilt-android:2.42" - kapt "com.google.dagger:hilt-compiler:2.42" + implementation "com.google.dagger:hilt-android:2.43.2" + kapt "com.google.dagger:hilt-compiler:2.43.2" implementation 'androidx.hilt:hilt-work:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.0.0' - implementation 'io.coil-kt:coil-base:2.1.0' - implementation 'io.coil-kt:coil-svg:2.1.0' - implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' + implementation 'io.coil-kt:coil-base:2.2.0' + implementation 'io.coil-kt:coil-svg:2.2.0' +// implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' + implementation 'com.github.KotatsuApp:subsampling-scale-image-view:2942b797a2' 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-http:5.9.6' + implementation 'ch.acra:acra-dialog:5.9.6' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' @@ -144,6 +147,6 @@ dependencies { androidTestImplementation 'androidx.room:room-testing:2.4.3' androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0' - androidTestImplementation 'com.google.dagger:hilt-android-testing:2.42' - kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.42' + androidTestImplementation 'com.google.dagger:hilt-android-testing:2.43.2' + kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.43.2' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fb3509dc2..3be2f59ae 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -10,4 +10,7 @@ } -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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5917b03bc..e5d92b0e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ + (KEY_MANGA)?.manga, + manga = intent?.getParcelableExtraCompat(KEY_MANGA)?.manga, mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE, - uri = intent?.data + uri = intent?.data, ) constructor(args: Bundle?) : this( - manga = args?.getParcelable(KEY_MANGA)?.manga, + manga = args?.getParcelableCompat(KEY_MANGA)?.manga, mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE, - uri = null + uri = null, ) companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt index e1e328c8f..8866cc068 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt @@ -21,14 +21,14 @@ abstract class AlertDialogFragment : DialogFragment() { viewBinding = binding return MaterialAlertDialogBuilder(requireContext(), theme) .setView(binding.root) - .also(::onBuildDialog) + .run(::onBuildDialog) .create() } final override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ) = viewBinding?.root @CallSuper @@ -37,9 +37,9 @@ abstract class AlertDialogFragment : DialogFragment() { super.onDestroyView() } - open fun onBuildDialog(builder: MaterialAlertDialogBuilder) = Unit + open fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder = builder protected fun bindingOrNull(): B? = viewBinding protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt index 591fd6b99..36b5e0e5f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt @@ -8,16 +8,18 @@ import android.view.animation.AccelerateInterpolator import android.view.animation.DecelerateInterpolator import androidx.core.view.isInvisible import androidx.core.view.isVisible +import kotlin.math.hypot import org.koitharu.kotatsu.utils.ext.animatorDurationScale import org.koitharu.kotatsu.utils.ext.measureWidth -import kotlin.math.hypot class BubbleAnimator( private val bubble: View, ) { - private val animationDuration = (bubble.resources.getInteger(android.R.integer.config_shortAnimTime) * - bubble.context.animatorDurationScale).toLong() + private val animationDuration = ( + bubble.resources.getInteger(android.R.integer.config_shortAnimTime) * + bubble.context.animatorDurationScale + ).toLong() private var animator: Animator? = null private var isHiding = false @@ -65,12 +67,12 @@ class BubbleAnimator( private var isCancelled = false - override fun onAnimationCancel(animation: Animator?) { + override fun onAnimationCancel(animation: Animator) { super.onAnimationCancel(animation) isCancelled = true } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) if (!isCancelled && animation === this@BubbleAnimator.animator) { bubble.isInvisible = true @@ -79,4 +81,4 @@ class BubbleAnimator( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt index a00fc90b9..0a87f5262 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt @@ -14,8 +14,10 @@ class ScrollbarAnimator( private val scrollbarPaddingEnd: Float, ) { - private val animationDuration = (scrollbar.resources.getInteger(R.integer.config_defaultAnimTime) * - scrollbar.context.animatorDurationScale).toLong() + private val animationDuration = ( + scrollbar.resources.getInteger(R.integer.config_defaultAnimTime) * + scrollbar.context.animatorDurationScale + ).toLong() private var animator: ViewPropertyAnimator? = null private var isHiding = false @@ -40,30 +42,32 @@ class ScrollbarAnimator( } animator?.cancel() isHiding = true - animator = scrollbar - .animate() - .translationX(scrollbarPaddingEnd) - .alpha(0f) - .setDuration(animationDuration) - .setListener(HideListener()) + animator = scrollbar.animate().apply { + translationX(scrollbarPaddingEnd) + alpha(0f) + duration = animationDuration + setListener(HideListener(this)) + } } - private inner class HideListener : AnimatorListenerAdapter() { + private inner class HideListener( + private val viewPropertyAnimator: ViewPropertyAnimator, + ) : AnimatorListenerAdapter() { private var isCancelled = false - override fun onAnimationCancel(animation: Animator?) { + override fun onAnimationCancel(animation: Animator) { super.onAnimationCancel(animation) isCancelled = true } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) - if (!isCancelled && animation === this@ScrollbarAnimator.animator) { + if (!isCancelled && this@ScrollbarAnimator.animator === viewPropertyAnimator) { scrollbar.isInvisible = true isHiding = false this@ScrollbarAnimator.animator = null } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt index 3aaa7270d..cd60b79f8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt @@ -2,15 +2,11 @@ package org.koitharu.kotatsu.base.ui.widgets import android.animation.LayoutTransition import android.content.Context -import android.transition.AutoTransition -import android.transition.TransitionManager import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowInsets -import android.view.animation.AccelerateDecelerateInterpolator -import android.view.animation.DecelerateInterpolator import androidx.annotation.AttrRes import androidx.annotation.StringRes import androidx.appcompat.widget.Toolbar @@ -30,6 +26,8 @@ import org.koitharu.kotatsu.utils.ext.getAnimationDuration import org.koitharu.kotatsu.utils.ext.getThemeDrawable import org.koitharu.kotatsu.utils.ext.parents +private const val THROTTLE_DELAY = 200L + class BottomSheetHeaderBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -39,11 +37,17 @@ class BottomSheetHeaderBar @JvmOverloads constructor( private val binding = LayoutSheetHeaderBinding.inflate(LayoutInflater.from(context), this) private val closeDrawable = context.getThemeDrawable(materialR.attr.actionModeCloseDrawable) private val bottomSheetCallback = Callback() + private val adjustStateRunnable = Runnable { adjustState() } private var bottomSheetBehavior: BottomSheetBehavior<*>? = null private val locationBuffer = IntArray(2) private val expansionListeners = LinkedList() private var fitStatusBar = false - private var transition: AutoTransition? = null + private val minHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_min) + private val maxHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_max) + private var isLayoutSuppressedCompat = false + private var isLayoutCalledWhileSuppressed = false + private var isBsExpanded = false + private var stateAdjustedAt = 0L @Deprecated("") val toolbar: MaterialToolbar @@ -156,6 +160,14 @@ class BottomSheetHeaderBar @JvmOverloads constructor( binding.toolbar.setSubtitle(resId) } + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + if (isLayoutSuppressedCompat) { + isLayoutCalledWhileSuppressed = true + } else { + super.onLayout(changed, l, t, r, b) + } + } + private fun setBottomSheetBehavior(behavior: BottomSheetBehavior<*>?) { bottomSheetBehavior?.removeBottomSheetCallback(bottomSheetCallback) bottomSheetBehavior = behavior @@ -166,15 +178,20 @@ class BottomSheetHeaderBar @JvmOverloads constructor( } private fun onBottomSheetStateChanged(newState: Int) { - val isExpanded = newState == BottomSheetBehavior.STATE_EXPANDED && isOnTopOfScreen() - if (isExpanded == binding.dragHandle.isGone) { - return + val expanded = newState == BottomSheetBehavior.STATE_EXPANDED && isOnTopOfScreen() + if (isBsExpanded != expanded) { + isBsExpanded = expanded + postAdjustState() } - TransitionManager.beginDelayedTransition(this, getTransition()) - binding.toolbar.navigationIcon = (if (isExpanded) closeDrawable else null) - binding.dragHandle.isGone = isExpanded - expansionListeners.forEach { it.onExpansionStateChanged(this, isExpanded) } - dispatchInsets(ViewCompat.getRootWindowInsets(this)) + } + + private fun suppressLayoutCompat(suppress: Boolean) { + if (suppress == isLayoutSuppressedCompat) return + isLayoutSuppressedCompat = suppress + if (!suppress && isLayoutCalledWhileSuppressed) { + requestLayout() + } + isLayoutCalledWhileSuppressed = false } private fun dispatchInsets(insets: WindowInsetsCompat?) { @@ -182,11 +199,14 @@ class BottomSheetHeaderBar @JvmOverloads constructor( return } val isExpanded = binding.dragHandle.isGone + val topInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0 if (isExpanded) { - val topInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0 updatePadding(top = topInset) } else { updatePadding(top = 0) + binding.dragHandle.updateLayoutParams { + height = topInset.coerceIn(minHandleHeight, maxHandleHeight) + } } } @@ -225,7 +245,7 @@ class BottomSheetHeaderBar @JvmOverloads constructor( return true } val viewId = child.id - return viewId == R.id.dragHandle || viewId == R.id.toolbar || viewId == R.id.frame + return viewId == R.id.dragHandle || viewId == R.id.toolbar } private fun convertLayoutParams(params: ViewGroup.LayoutParams?): Toolbar.LayoutParams? { @@ -242,13 +262,24 @@ class BottomSheetHeaderBar @JvmOverloads constructor( } } - private fun getTransition(): AutoTransition { - transition?.let { return it } - val t = AutoTransition() - t.duration = context.getAnimationDuration(android.R.integer.config_shortAnimTime) - t.addTarget(binding.dragHandle) - transition = t - return t + private fun postAdjustState() { + removeCallbacks(adjustStateRunnable) + val now = System.currentTimeMillis() + if (stateAdjustedAt + THROTTLE_DELAY < now) { + adjustState() + } else { + postDelayed(adjustStateRunnable, THROTTLE_DELAY) + } + } + + private fun adjustState() { + suppressLayoutCompat(true) + binding.toolbar.navigationIcon = (if (isBsExpanded) closeDrawable else null) + binding.dragHandle.isGone = isBsExpanded + expansionListeners.forEach { it.onExpansionStateChanged(this, isBsExpanded) } + dispatchInsets(ViewCompat.getRootWindowInsets(this)) + stateAdjustedAt = System.currentTimeMillis() + suppressLayoutCompat(false) } private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), View.OnClickListener { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt index f012d9a95..3e9e7b55d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt @@ -100,7 +100,7 @@ class SlidingBottomNavigationView @JvmOverloads constructor( .applySystemAnimatorScale(context) .setListener( object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { currentAnimator = null postInvalidate() } diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt index 4c6d48a55..87adf5a1c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt @@ -42,7 +42,7 @@ class BookmarksViewModel @Inject constructor( BookmarksGroup(manga, bookmarks) } } - .catch { e -> e.toErrorState(canRetry = false) } + .catch { e -> emit(listOf(e.toErrorState(canRetry = false))) } .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) fun removeBookmarks(ids: Map>) { 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 683d8abb0..dc18898b3 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 @@ -55,11 +55,12 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud override fun onDestroyView() { binding.webView.stopLoading() + binding.webView.destroy() super.onDestroyView() } - override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { - builder.setNegativeButton(android.R.string.cancel, null) + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder).setNegativeButton(android.R.string.cancel, null) } override fun onResume() { @@ -83,7 +84,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/core/db/dao/TrackLogsDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt index ade35613b..ee0da6165 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.core.db.dao import androidx.room.* +import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogWithManga @@ -8,8 +9,8 @@ import org.koitharu.kotatsu.tracker.data.TrackLogWithManga interface TrackLogsDao { @Transaction - @Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET :offset") - suspend fun findAll(offset: Int, limit: Int): List + @Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0") + fun observeAll(limit: Int): Flow> @Query("DELETE FROM track_logs") suspend fun clear() @@ -25,4 +26,4 @@ interface TrackLogsDao { @Query("SELECT COUNT(*) FROM track_logs") suspend fun count(): Int -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt index af938a813..5960cb5e5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.core.db.entity +import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase @@ -10,7 +11,7 @@ import org.koitharu.kotatsu.utils.ext.longHashCode fun TagEntity.toMangaTag() = MangaTag( key = this.key, title = this.title.toTitleCase(), - source = MangaSource.valueOf(this.source), + source = MangaSource(this.source) ?: MangaSource.DUMMY, ) fun Collection.toMangaTags() = mapToSet(TagEntity::toMangaTag) @@ -19,7 +20,7 @@ fun MangaEntity.toManga(tags: Set) = Manga( id = this.id, title = this.title, altTitle = this.altTitle, - state = this.state?.let { MangaState.valueOf(it) }, + state = this.state?.let { MangaState(it) }, rating = this.rating, isNsfw = this.isNsfw, url = this.url, @@ -27,8 +28,8 @@ fun MangaEntity.toManga(tags: Set) = Manga( coverUrl = this.coverUrl, largeCoverUrl = this.largeCoverUrl, author = this.author, - source = MangaSource.valueOf(this.source), - tags = tags + source = MangaSource(this.source) ?: MangaSource.DUMMY, + tags = tags, ) fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags()) @@ -54,14 +55,18 @@ fun MangaTag.toEntity() = TagEntity( title = title, key = key, source = source.name, - id = "${key}_${source.name}".longHashCode() + id = "${key}_${source.name}".longHashCode(), ) fun Collection.toEntities() = map(MangaTag::toEntity) // Other -@Suppress("FunctionName") fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching { SortOrder.valueOf(name) -}.getOrDefault(fallback) \ No newline at end of file +}.getOrDefault(fallback) + +@Suppress("FunctionName") +fun MangaState(name: String): MangaState? = runCatching { + MangaState.valueOf(name) +}.getOrNull() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt index 62ea43d64..84bc96d21 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt @@ -11,8 +11,10 @@ import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import org.koitharu.kotatsu.BuildConfig @@ -53,11 +55,11 @@ class AppUpdateRepository @Inject constructor( } } - suspend fun fetchUpdate(): AppVersion? { + suspend fun fetchUpdate(): AppVersion? = withContext(Dispatchers.Default) { if (!isUpdateSupported()) { - return null + return@withContext null } - return runCatching { + runCatching { val currentVersion = VersionId(BuildConfig.VERSION_NAME) val available = getAvailableVersions().asArrayList() available.sortBy { it.versionId } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt index c7eb5bbf9..eefe100a7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt @@ -2,10 +2,12 @@ package org.koitharu.kotatsu.core.os import android.app.ActivityManager import android.content.Context +import android.content.SharedPreferences import android.content.pm.ShortcutManager import android.media.ThumbnailUtils import android.os.Build import android.util.Size +import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat @@ -22,6 +24,7 @@ import kotlinx.coroutines.launch import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.core.db.TABLE_HISTORY +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.reader.ui.ReaderActivity @@ -35,13 +38,18 @@ class ShortcutsUpdater @Inject constructor( private val coil: ImageLoader, private val historyRepository: HistoryRepository, private val mangaRepository: MangaDataRepository, -) : InvalidationTracker.Observer(TABLE_HISTORY) { + private val settings: AppSettings, +) : InvalidationTracker.Observer(TABLE_HISTORY), SharedPreferences.OnSharedPreferenceChangeListener { private val iconSize by lazy { getIconSize(context) } private var shortcutsUpdateJob: Job? = null - override fun onInvalidated(tables: MutableSet) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + init { + settings.subscribe(this) + } + + override fun onInvalidated(tables: Set) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || !settings.isDynamicShortcutsEnabled) { return } val prevJob = shortcutsUpdateJob @@ -51,6 +59,16 @@ class ShortcutsUpdater @Inject constructor( } } + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && key == AppSettings.KEY_SHORTCUTS) { + if (settings.isDynamicShortcutsEnabled) { + onInvalidated(emptySet()) + } else { + clearShortcuts() + } + } + } + suspend fun requestPinShortcut(manga: Manga): Boolean { return ShortcutManagerCompat.requestPinShortcut( context, @@ -64,6 +82,15 @@ class ShortcutsUpdater @Inject constructor( return shortcutsUpdateJob?.join() != null } + fun isDynamicShortcutsAvailable(): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + return false + } + val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager + return manager.maxShortcutCountPerActivity > 0 + } + + @RequiresApi(Build.VERSION_CODES.N_MR1) private suspend fun updateShortcutsImpl() = runCatching { val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) @@ -74,6 +101,15 @@ class ShortcutsUpdater @Inject constructor( it.printStackTraceDebug() } + @RequiresApi(Build.VERSION_CODES.N_MR1) + private fun clearShortcuts() { + val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager + try { + manager.removeAllDynamicShortcuts() + } catch (_: IllegalStateException) { + } + } + private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder { val icon = runCatching { val bmp = coil.execute( 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 53360ca0a..7eeb64804 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 @@ -11,6 +11,12 @@ import androidx.core.content.edit import androidx.preference.PreferenceManager import com.google.android.material.color.DynamicColors import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.network.DoHProvider @@ -19,12 +25,6 @@ import org.koitharu.kotatsu.utils.ext.getEnumValue import org.koitharu.kotatsu.utils.ext.observe import org.koitharu.kotatsu.utils.ext.putEnumValue import org.koitharu.kotatsu.utils.ext.toUriOrNull -import java.io.File -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AppSettings @Inject constructor(@ApplicationContext context: Context) { @@ -65,6 +65,9 @@ class AppSettings @Inject constructor(@ApplicationContext 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) } @@ -73,13 +76,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true) set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) } - val isUpdateCheckingEnabled: Boolean - get() = prefs.getBoolean(KEY_APP_UPDATE_AUTO, true) - - var lastUpdateCheckTimestamp: Long - get() = prefs.getLong(KEY_APP_UPDATE, 0L) - set(value) = prefs.edit { putLong(KEY_APP_UPDATE, value) } - val isTrackerEnabled: Boolean get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true) @@ -141,6 +137,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val isExitConfirmationEnabled: Boolean get() = prefs.getBoolean(KEY_EXIT_CONFIRM, false) + val isDynamicShortcutsEnabled: Boolean + get() = prefs.getBoolean(KEY_SHORTCUTS, true) + var sourcesOrder: List get() = prefs.getString(KEY_SOURCES_ORDER, null) ?.split('|') @@ -324,10 +323,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_INCOGNITO_MODE = "incognito" const val KEY_SYNC = "sync" const val KEY_READER_BAR = "reader_bar" + const val KEY_SHORTCUTS = "dynamic_shortcuts" + const val KEY_READER_TAPS_LTR = "reader_taps_ltr" // About const val KEY_APP_UPDATE = "app_update" - const val KEY_APP_UPDATE_AUTO = "app_update_auto" const val KEY_APP_TRANSLATION = "about_app_translation" private const val NETWORK_NEVER = 0 diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/MangaErrorDialog.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/MangaErrorDialog.kt new file mode 100644 index 000000000..b5781451a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/MangaErrorDialog.kt @@ -0,0 +1,72 @@ +package org.koitharu.kotatsu.core.ui + +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.text.HtmlCompat +import androidx.core.text.htmlEncode +import androidx.core.text.parseAsHtml +import androidx.fragment.app.FragmentManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.databinding.DialogMangaErrorBinding +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.utils.ext.report +import org.koitharu.kotatsu.utils.ext.requireParcelable +import org.koitharu.kotatsu.utils.ext.requireSerializable +import org.koitharu.kotatsu.utils.ext.withArgs + +class MangaErrorDialog : AlertDialogFragment() { + + private lateinit var error: Throwable + private lateinit var manga: Manga + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val args = requireArguments() + manga = args.requireParcelable(ARG_MANGA).manga + error = args.requireSerializable(ARG_ERROR) + } + + override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): DialogMangaErrorBinding { + return DialogMangaErrorBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(binding.textViewMessage) { + movementMethod = LinkMovementMethod.getInstance() + text = context.getString( + R.string.manga_error_description_pattern, + this@MangaErrorDialog.error.message?.htmlEncode().orEmpty(), + manga.publicUrl, + ).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY) + } + } + + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setCancelable(true) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.report) { _, _ -> + dismiss() + error.report(TAG) + }.setTitle(R.string.error_occurred) + } + + companion object { + + private const val TAG = "MangaErrorDialog" + private const val ARG_ERROR = "error" + private const val ARG_MANGA = "manga" + + fun show(fm: FragmentManager, manga: Manga, error: Throwable) = MangaErrorDialog().withArgs(2) { + putParcelable(ARG_MANGA, ParcelableManga(manga, false)) + putSerializable(ARG_ERROR, error) + }.show(fm, TAG) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt index d9ca082ee..1733523b7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt @@ -35,13 +35,13 @@ class ChaptersMenuProvider( else -> false } - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { bottomSheetMediator?.lock() return true } - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { - (item?.actionView as? SearchView)?.setQuery("", false) + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + (item.actionView as? SearchView)?.setQuery("", false) viewModel.performChapterSearch(null) bottomSheetMediator?.unlock() return true diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 25b43c7c2..9add56679 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -5,8 +5,13 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Bundle +import android.transition.Slide +import android.transition.TransitionManager +import android.view.Gravity import android.view.Menu import android.view.View +import android.view.ViewGroup +import android.view.animation.AccelerateDecelerateInterpolator import android.widget.Toast import androidx.appcompat.widget.PopupMenu import androidx.core.graphics.Insets @@ -16,6 +21,7 @@ import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope import com.google.android.material.badge.BadgeDrawable import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -27,6 +33,7 @@ import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.os.ShortcutsUpdater +import org.koitharu.kotatsu.core.ui.MangaErrorDialog import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.details.ui.model.HistoryInfo import org.koitharu.kotatsu.download.ui.service.DownloadService @@ -93,6 +100,7 @@ class DetailsActivity : viewModel.onMangaRemoved.observe(this, ::onMangaRemoved) viewModel.onError.observe(this, ::onError) viewModel.onShowToast.observe(this) { + makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show() } viewModel.historyInfo.observe(this, ::onHistoryChanged) viewModel.selectedBranchName.observe(this) { @@ -158,8 +166,11 @@ class DetailsActivity : private fun onMangaUpdated(manga: Manga) { title = manga.title - binding.buttonRead.isEnabled = !manga.chapters.isNullOrEmpty() + val hasChapters = !manga.chapters.isNullOrEmpty() + binding.buttonRead.isEnabled = hasChapters invalidateOptionsMenu() + showBottomSheet(manga.chapters != null) + binding.groupHeader?.isVisible = hasChapters } private fun onMangaRemoved(manga: Manga) { @@ -172,17 +183,17 @@ class DetailsActivity : } private fun onError(e: Throwable) { + val manga = viewModel.manga.value when { ExceptionResolver.canResolve(e) -> { resolveError(e) } - viewModel.manga.value == null -> { + manga == null -> { Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() finishAfterTransition() } else -> { - val snackbar = Snackbar.make( - binding.containerDetails, + val snackbar = makeSnackbar( e.getDisplayMessage(resources), if (viewModel.manga.value?.chapters == null) { Snackbar.LENGTH_INDEFINITE @@ -190,10 +201,9 @@ class DetailsActivity : Snackbar.LENGTH_LONG }, ) - snackbar.anchorView = binding.headerChapters if (e.isReportable()) { - snackbar.setAction(R.string.report) { - e.report("DetailsActivity::onError") + snackbar.setAction(R.string.details) { + MangaErrorDialog.show(supportFragmentManager, manga, e) } } snackbar.show() @@ -238,8 +248,7 @@ class DetailsActivity : fun showChapterMissingDialog(chapterId: Long) { val remoteManga = viewModel.getRemoteManga() if (remoteManga == null) { - val snackbar = Snackbar.make(binding.containerDetails, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT) - snackbar.anchorView = binding.headerChapters + val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT) snackbar.show() return } @@ -291,6 +300,24 @@ class DetailsActivity : private fun isTabletLayout() = binding.layoutBottom == null + private fun showBottomSheet(isVisible: Boolean) { + val view = binding.layoutBottom ?: return + if (view.isVisible == isVisible) return + val transition = Slide(Gravity.BOTTOM) + transition.addTarget(view) + transition.interpolator = AccelerateDecelerateInterpolator() + TransitionManager.beginDelayedTransition(binding.root as ViewGroup, transition) + view.isVisible = isVisible + } + + private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar { + val sb = Snackbar.make(binding.containerDetails, text, duration) + if (binding.layoutBottom?.isVisible == true) { + sb.anchorView = binding.headerChapters + } + return sb + } + companion object { fun newIntent(context: Context, manga: Manga): Intent { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 201a6a761..c3de6fa13 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -70,6 +70,7 @@ class DetailsFragment : super.onViewCreated(view, savedInstanceState) binding.textViewAuthor.setOnClickListener(this) binding.imageViewCover.setOnClickListener(this) + binding.infoLayout.textViewSource.setOnClickListener(this) binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() binding.chipsTags.onChipClickListener = this viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated) @@ -228,6 +229,14 @@ class DetailsFragment : ), ) } + R.id.textView_source -> { + startActivity( + MangaListActivity.newIntent( + context = v.context, + source = manga.source, + ), + ) + } R.id.imageView_cover -> { startActivity( ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }), diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt index e15669f94..9e57bd7f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt @@ -28,4 +28,4 @@ fun MangaChapter.toListItem( uploadDateMs = uploadDate, dateFormat = dateFormat, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt index f6553c37a..0cdbc597e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.utils.ext.assistedViewModels import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.getSerializableCompat @AndroidEntryPoint class FavouritesCategoryEditActivity : @@ -70,8 +71,8 @@ class FavouritesCategoryEditActivity : override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) - val order = savedInstanceState.getSerializable(KEY_SORT_ORDER) - if (order != null && order is SortOrder) { + val order = savedInstanceState.getSerializableCompat(KEY_SORT_ORDER) + if (order != null) { selectedSortOrder = order } } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index cca1dffe6..85e7a45e6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct class FavouritesListViewModel @AssistedInject constructor( @@ -53,7 +54,7 @@ class FavouritesListViewModel @AssistedInject constructor( } else { repository.observeAll(categoryId) }, - createListModeFlow() + createListModeFlow(), ) { list, mode -> when { list.isEmpty() -> listOf( @@ -66,13 +67,13 @@ class FavouritesListViewModel @AssistedInject constructor( R.string.favourites_category_empty }, actionStringRes = 0, - ) + ), ) else -> list.toUi(mode, this) } }.catch { emit(listOf(it.toErrorState(canRetry = false))) - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) init { if (categoryId != NO_ID) { diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index cbf38db4b..c9f851f62 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -23,7 +23,7 @@ import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.onFirst @@ -60,8 +60,8 @@ class HistoryListViewModel @Inject constructor( }.onFirst { loadingCounter.decrement() }.catch { - it.toErrorState(canRetry = false) - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + emit(listOf(it.toErrorState(canRetry = false))) + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) override fun onRefresh() = Unit diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt index fdf161282..243e8cb5d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt @@ -56,17 +56,17 @@ class ReadingProgressView @JvmOverloads constructor( getProgressDrawable().progress = p } - override fun onAnimationStart(animation: Animator?) = Unit + override fun onAnimationStart(animation: Animator) = Unit - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { if (percentAnimator === animation) { percentAnimator = null } } - override fun onAnimationCancel(animation: Animator?) = Unit + override fun onAnimationCancel(animation: Animator) = Unit - override fun onAnimationRepeat(animation: Animator?) = Unit + override fun onAnimationRepeat(animation: Animator) = Unit fun setPercent(value: Float, animate: Boolean) { val currentDrawable = peekProgressDrawable() diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt index 46e318b1f..140be0638 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt @@ -27,7 +27,7 @@ import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.ext.daysDiff private const val HISTORY_MAX_SEGMENTS = 2 @@ -49,8 +49,8 @@ class LibraryViewModel @Inject constructor( ) { history, favourites -> mapList(history, favourites) }.catch { e -> - e.toErrorState(canRetry = false) - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + emit(listOf(e.toErrorState(canRetry = false))) + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) override suspend fun getCounter(mangaId: Long): Int { return trackingRepository.getNewChaptersCount(mangaId) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt index 443f7021d..64747aedd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt @@ -6,6 +6,7 @@ import android.view.View import android.widget.TextView import androidx.annotation.StyleRes import androidx.core.view.updateLayoutParams +import androidx.core.widget.TextViewCompat import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import kotlin.math.roundToInt @@ -46,12 +47,12 @@ class ItemSizeResolver(resources: Resources, private val settings: AppSettings) } } - override fun onViewAttachedToWindow(v: View?) { + override fun onViewAttachedToWindow(v: View) { settings.subscribe(this) update() } - override fun onViewDetachedFromWindow(v: View?) { + override fun onViewDetachedFromWindow(v: View) { settings.unsubscribe(this) } @@ -77,7 +78,7 @@ class ItemSizeResolver(resources: Resources, private val settings: AppSettings) } if (textAppearanceResId != prevTextAppearance) { prevTextAppearance = textAppearanceResId - setTextAppearance(textAppearanceResId) + TextViewCompat.setTextAppearance(this, textAppearanceResId) requestLayout() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt index 1c988732a..3728c57b2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.slider.Slider import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup @@ -17,7 +18,6 @@ import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.databinding.DialogListModeBinding import org.koitharu.kotatsu.utils.ext.setValueRounded import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter -import javax.inject.Inject @AndroidEntryPoint class ListModeSelectDialog : @@ -33,8 +33,9 @@ class ListModeSelectDialog : container: ViewGroup?, ) = DialogListModeBinding.inflate(inflater, container, false) - override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { - builder.setTitle(R.string.list_mode) + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setTitle(R.string.list_mode) .setPositiveButton(R.string.done, null) .setCancelable(true) } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index 19b3f11f7..6b1fe569f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -1,9 +1,11 @@ package org.koitharu.kotatsu.list.ui.filter +import androidx.recyclerview.widget.AsyncListDiffer.ListListener import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter class FilterAdapter( listener: OnFilterChangedListener, + listListener: ListListener, ) : AsyncListDifferDelegationAdapter( FilterDiffCallback(), filterSortDelegate(listener), @@ -11,4 +13,9 @@ class FilterAdapter( filterHeaderDelegate(), filterLoadingDelegate(), filterErrorDelegate(), -) \ No newline at end of file +) { + + init { + differ.addListListener(listListener) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt index c80f7358a..e7be8c8b6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt @@ -6,18 +6,21 @@ import android.os.Bundle import android.view.* import androidx.appcompat.widget.SearchView import androidx.fragment.app.FragmentManager -import dagger.hilt.android.AndroidEntryPoint +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.LinearLayoutManager import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.databinding.SheetFilterBinding import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel +import org.koitharu.kotatsu.utils.ext.isScrolledToTop import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels class FilterBottomSheet : BaseBottomSheet(), MenuItem.OnActionExpandListener, SearchView.OnQueryTextListener, - DialogInterface.OnKeyListener { + DialogInterface.OnKeyListener, + AsyncListDiffer.ListListener { private val viewModel by parentFragmentViewModels() @@ -33,13 +36,13 @@ class FilterBottomSheet : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val adapter = FilterAdapter(viewModel) + val adapter = FilterAdapter(viewModel, this) binding.recyclerView.adapter = adapter viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems) initOptionsMenu() } - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { setExpanded(isExpanded = true, isLocked = true) return true } @@ -71,6 +74,12 @@ class FilterBottomSheet : return false } + override fun onCurrentListChanged(previousList: MutableList, currentList: MutableList) { + if (currentList.size > previousList.size && view != null) { + (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0) + } + } + private fun initOptionsMenu() { binding.headerBar.toolbar.inflateMenu(R.menu.opt_filter) val searchMenuItem = binding.headerBar.toolbar.menu.findItem(R.id.action_search) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 65e0b5658..7d08cb72b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -63,6 +63,10 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local x.tags.containsAll(tags) } } + when (sortOrder) { + SortOrder.ALPHABETICAL -> list.sortBy { it.title } + SortOrder.RATING -> list.sortBy { it.rating } + } return list } @@ -250,7 +254,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local } } - override val sortOrders = setOf(SortOrder.ALPHABETICAL) + override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING) override suspend fun getPageUrl(page: MangaPage) = page.url diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt index 000e57c14..5cd6cf85b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt @@ -27,8 +27,9 @@ class ImportDialogFragment : AlertDialogFragment(), View.On return DialogImportBinding.inflate(inflater, container, false) } - override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { - builder.setTitle(R.string._import) + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setTitle(R.string._import) .setNegativeButton(android.R.string.cancel, null) .setCancelable(true) } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 48349e846..507dafe25 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -1,5 +1,8 @@ package org.koitharu.kotatsu.main.ui +import android.Manifest +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.Build import android.os.Bundle import android.util.SparseIntArray import android.view.MenuItem @@ -7,6 +10,7 @@ import android.view.View import androidx.activity.result.ActivityResultCallback import androidx.activity.viewModels import androidx.appcompat.view.ActionMode +import androidx.core.app.ActivityCompat import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat import androidx.core.graphics.Insets @@ -24,7 +28,6 @@ import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.coroutines.yield import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView @@ -291,12 +294,11 @@ class MainActivity : TrackWorker.setup(applicationContext) SuggestionsWorker.setup(applicationContext) } + requestNotificationsPermission() when { !settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager) settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager) } - yield() - // TODO get().requestFullSyncAndGc(get()) } } @@ -347,6 +349,15 @@ class MainActivity : showNav(!isOpened) } + private fun requestNotificationsPermission() { + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1) + } + } + private inner class VoiceInputCallback : ActivityResultCallback { override fun onActivityResult(result: String?) { diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt index c60ae4aa8..0805d872e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt @@ -4,26 +4,28 @@ import android.util.SparseIntArray import androidx.core.util.set import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.github.AppUpdateRepository -import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( private val historyRepository: HistoryRepository, - private val settings: AppSettings, private val appUpdateRepository: AppUpdateRepository, private val trackingRepository: TrackingRepository, + syncController: SyncController, + database: MangaDatabase, ) : BaseViewModel() { val onOpenReader = SingleLiveEvent() @@ -43,9 +45,12 @@ class MainViewModel @Inject constructor( }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, SparseIntArray(0)) init { - launchJob(Dispatchers.Default) { + launchJob { appUpdateRepository.fetchUpdate() } + launchJob { + syncController.requestFullSyncAndGc(database) + } } fun openLastReader() { 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 a3b4bcbbd..c94cdd153 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 @@ -6,6 +6,7 @@ import android.content.Intent import android.os.Bundle import javax.inject.Inject import javax.inject.Singleton +import org.acra.dialog.CrashReportDialog import org.koitharu.kotatsu.core.prefs.AppSettings @Singleton @@ -14,7 +15,7 @@ class AppProtectHelper @Inject constructor(private val settings: AppSettings) : 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/main/ui/protect/ProtectActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt index b3d0322bf..4a6886068 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt @@ -22,6 +22,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.databinding.ActivityProtectBinding import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat @AndroidEntryPoint class ProtectActivity : @@ -44,7 +45,7 @@ class ProtectActivity : viewModel.onError.observe(this, this::onError) viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.onUnlockSuccess.observe(this) { - val intent = intent.getParcelableExtra(EXTRA_INTENT) + val intent = intent.getParcelableExtraCompat(EXTRA_INTENT) startActivity(intent) finishAfterTransition() } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt index 7d4eb83e1..9efa3cef6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt @@ -6,6 +6,8 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.FragmentManager import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlin.math.roundToInt import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener @@ -17,9 +19,8 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback +import org.koitharu.kotatsu.utils.ext.getParcelableCompat import org.koitharu.kotatsu.utils.ext.withArgs -import javax.inject.Inject -import kotlin.math.roundToInt @AndroidEntryPoint class ChaptersBottomSheet : BaseBottomSheet(), OnListItemClickListener { @@ -33,7 +34,7 @@ class ChaptersBottomSheet : BaseBottomSheet(), OnListItemC override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val chapters = arguments?.getParcelable(ARG_CHAPTERS)?.chapters + val chapters = arguments?.getParcelableCompat(ARG_CHAPTERS)?.chapters if (chapters.isNullOrEmpty()) { dismissAllowingStateLoss() return 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 9986fde88..cb9a0f13c 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 @@ -40,6 +40,7 @@ import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.utils.GridTouchHelper +import org.koitharu.kotatsu.utils.IdlingDetector import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.* @@ -51,15 +52,18 @@ class ReaderActivity : OnPageSelectListener, ReaderConfigBottomSheet.Callback, ReaderControlDelegate.OnInteractionListener, - OnApplyWindowInsetsListener { + OnApplyWindowInsetsListener, + IdlingDetector.Callback { @Inject lateinit var viewModelFactory: ReaderViewModel.Factory + private val idlingDetector = IdlingDetector(TimeUnit.SECONDS.toMillis(10), this) + val viewModel by assistedViewModels { viewModelFactory.create( intent = MangaIntent(intent), - initialState = intent?.getParcelableExtra(EXTRA_STATE), + initialState = intent?.getParcelableExtraCompat(EXTRA_STATE), preselectedBranch = intent?.getStringExtra(EXTRA_BRANCH), ) } @@ -70,6 +74,9 @@ class ReaderActivity : pageSwitchTimer.delaySec = value } + override val readerMode: ReaderMode? + get() = readerManager.currentMode + private lateinit var pageSwitchTimer: PageSwitchTimer private lateinit var touchHelper: GridTouchHelper private lateinit var controlDelegate: ReaderControlDelegate @@ -84,11 +91,12 @@ class ReaderActivity : supportActionBar?.setDisplayHomeAsUpEnabled(true) touchHelper = GridTouchHelper(this, this) pageSwitchTimer = PageSwitchTimer(this, this) - controlDelegate = ReaderControlDelegate(lifecycleScope, settings, this) + controlDelegate = ReaderControlDelegate(settings, this, this) binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected) binding.slider.setLabelFormatter(PageLabelFormatter()) ReaderSliderListener(this, viewModel).attachToSlider(binding.slider) insetsDelegate.interceptingWindowInsetsListener = this + idlingDetector.bindToLifecycle(this) viewModel.onError.observe(this, this::onError) viewModel.readerMode.observe(this, this::onInitReader) @@ -111,6 +119,11 @@ class ReaderActivity : override fun onUserInteraction() { super.onUserInteraction() pageSwitchTimer.onUserInteraction() + idlingDetector.onUserInteraction() + } + + override fun onIdle() { + viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) } private fun onInitReader(mode: ReaderMode) { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt index 916b0ee5f..890c9074b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt @@ -15,14 +15,15 @@ import org.koitharu.kotatsu.databinding.DialogReaderConfigBinding import org.koitharu.kotatsu.utils.ext.withArgs @Deprecated("Not in use") -class ReaderConfigDialog : AlertDialogFragment(), +class ReaderConfigDialog : + AlertDialogFragment(), CheckableButtonGroup.OnCheckedChangeListener { private lateinit var mode: ReaderMode override fun onInflateView( inflater: LayoutInflater, - container: ViewGroup? + container: ViewGroup?, ) = DialogReaderConfigBinding.inflate(inflater, container, false) override fun onCreate(savedInstanceState: Bundle?) { @@ -32,8 +33,9 @@ class ReaderConfigDialog : AlertDialogFragment(), ?: ReaderMode.STANDARD } - override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { - builder.setTitle(R.string.read_mode) + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setTitle(R.string.read_mode) .setPositiveButton(R.string.done, null) .setCancelable(true) } @@ -48,8 +50,10 @@ class ReaderConfigDialog : AlertDialogFragment(), } override fun onDismiss(dialog: DialogInterface) { - ((parentFragment as? Callback) - ?: (activity as? Callback))?.onReaderModeChanged(mode) + ( + (parentFragment as? Callback) + ?: (activity as? Callback) + )?.onReaderModeChanged(mode) super.onDismiss(dialog) } 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 98c14c98c..3da6ed8d1 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 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,19 +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 @@ -99,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/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt index 5f825fee6..cd1e31a8d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt @@ -8,17 +8,19 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Rect -import android.icu.text.SimpleDateFormat import android.util.AttributeSet import android.view.View import androidx.annotation.AttrRes import androidx.core.graphics.ColorUtils import com.google.android.material.R as materialR +import java.text.SimpleDateFormat import java.util.* import org.koitharu.kotatsu.R import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.utils.ext.getThemeColor +import org.koitharu.kotatsu.utils.ext.measureDimension +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.resolveDp class ReaderInfoBarView @JvmOverloads constructor( @@ -29,23 +31,46 @@ class ReaderInfoBarView @JvmOverloads constructor( private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val textBounds = Rect() - private val inset = context.resources.resolveDp(2f) private val timeFormat = SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT) private val timeReceiver = TimeReceiver() + private var insetLeft: Int = 0 + private var insetRight: Int = 0 + private var insetTop: Int = 0 + private val colorText = ColorUtils.setAlphaComponent( + context.getThemeColor(materialR.attr.colorOnSurface, Color.BLACK), + 200, + ) + private val colorOutline = ColorUtils.setAlphaComponent( + context.getThemeColor(materialR.attr.colorSurface, Color.WHITE), + 200, + ) private var timeText = timeFormat.format(Date()) private var text: String = "" private val innerHeight - get() = height - inset - inset - paddingTop - paddingBottom + get() = height - paddingTop - paddingBottom - insetTop private val innerWidth - get() = width - inset - inset - paddingLeft - paddingRight + get() = width - paddingLeft - paddingRight - insetLeft - insetRight init { - paint.color = ColorUtils.setAlphaComponent( - context.getThemeColor(materialR.attr.colorOnSurface, Color.BLACK), - 160, + paint.strokeWidth = context.resources.resolveDp(2f) + val insetCorner = getSystemUiDimensionOffset("rounded_corner_content_padding") + val insetStart = getSystemUiDimensionOffset("status_bar_padding_start") + insetCorner + val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end") + insetCorner + val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL + insetLeft = if (isRtl) insetEnd else insetStart + insetRight = if (isRtl) insetStart else insetEnd + insetTop = minOf(insetLeft, insetRight) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight + insetLeft + insetRight + val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom + insetTop + setMeasuredDimension( + measureDimension(desiredWidth, widthMeasureSpec), + measureDimension(desiredHeight, heightMeasureSpec), ) } @@ -53,9 +78,9 @@ class ReaderInfoBarView @JvmOverloads constructor( super.onDraw(canvas) val ty = innerHeight / 2f + textBounds.height() / 2f - textBounds.bottom paint.textAlign = Paint.Align.LEFT - canvas.drawText(text, paddingLeft + inset, paddingTop + inset + ty, paint) + canvas.drawTextOutline(text, (paddingLeft + insetLeft).toFloat(), paddingTop + insetTop + ty) paint.textAlign = Paint.Align.RIGHT - canvas.drawText(timeText, width - paddingRight - inset, paddingTop + inset + ty, paint) + canvas.drawTextOutline(timeText, (width - paddingRight - insetRight).toFloat(), paddingTop + insetTop + ty) } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { @@ -103,6 +128,15 @@ class ReaderInfoBarView @JvmOverloads constructor( paint.getTextBounds(str, 0, str.length, textBounds) } + private fun Canvas.drawTextOutline(text: String, x: Float, y: Float) { + paint.color = colorOutline + paint.style = Paint.Style.STROKE + drawText(text, x, y, paint) + paint.color = colorText + paint.style = Paint.Style.FILL + drawText(text, x, y, paint) + } + private inner class TimeReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -110,4 +144,13 @@ class ReaderInfoBarView @JvmOverloads constructor( invalidate() } } + + private fun getSystemUiDimensionOffset(name: String): Int = runCatching { + val manager = context.packageManager + val resources = manager.getResourcesForApplication("com.android.systemui") + val resId = resources.getIdentifier(name, "dimen", "com.android.systemui") + resources.getDimensionPixelOffset(resId) + }.onFailure { + it.printStackTraceDebug() + }.getOrDefault(0) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt index 52894b4da..6bd234afd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt @@ -8,6 +8,7 @@ import androidx.viewbinding.ViewBinding import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderViewModel +import org.koitharu.kotatsu.utils.ext.getParcelableCompat private const val KEY_STATE = "state" @@ -18,7 +19,7 @@ abstract class BaseReader : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - var restoredState = savedInstanceState?.getParcelable(KEY_STATE) + var restoredState = savedInstanceState?.getParcelableCompat(KEY_STATE) viewModel.content.observe(viewLifecycleOwner) { onPagesChanged(it.pages, restoredState ?: it.state) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt index d097c1bc2..0529c8ebe 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt @@ -4,12 +4,12 @@ import android.view.ViewGroup import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.utils.ext.resetTransformations -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine @Suppress("LeakingThis") abstract class BaseReaderAdapter>( @@ -45,7 +45,7 @@ abstract class BaseReaderAdapter>( final override fun onCreateViewHolder( parent: ViewGroup, - viewType: Int + viewType: Int, ): H = onCreateViewHolder(parent, loader, settings, exceptionResolver) suspend fun setItems(items: List) = suspendCoroutine { cont -> @@ -58,7 +58,7 @@ abstract class BaseReaderAdapter>( parent: ViewGroup, loader: PageLoader, settings: AppSettings, - exceptionResolver: ExceptionResolver + exceptionResolver: ExceptionResolver, ): H private class DiffCallback : DiffUtil.ItemCallback() { @@ -70,6 +70,5 @@ abstract class BaseReaderAdapter>( override fun areContentsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean { return oldItem == newItem } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt index a4d60c861..397ce0b4e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt @@ -69,7 +69,7 @@ open class PageHolder( override fun onImageShowing(zoom: ZoomMode) { binding.ssiv.maxScale = 2f * maxOf( binding.ssiv.width / binding.ssiv.sWidth.toFloat(), - binding.ssiv.height / binding.ssiv.sHeight.toFloat() + binding.ssiv.height / binding.ssiv.sHeight.toFloat(), ) when (zoom) { ZoomMode.FIT_CENTER -> { @@ -81,7 +81,7 @@ open class PageHolder( binding.ssiv.minScale = binding.ssiv.height / binding.ssiv.sHeight.toFloat() binding.ssiv.setScaleAndCenter( binding.ssiv.minScale, - PointF(0f, binding.ssiv.sHeight / 2f) + PointF(0f, binding.ssiv.sHeight / 2f), ) } ZoomMode.FIT_WIDTH -> { @@ -89,14 +89,14 @@ open class PageHolder( binding.ssiv.minScale = binding.ssiv.width / binding.ssiv.sWidth.toFloat() binding.ssiv.setScaleAndCenter( binding.ssiv.minScale, - PointF(binding.ssiv.sWidth / 2f, 0f) + PointF(binding.ssiv.sWidth / 2f, 0f), ) } ZoomMode.KEEP_START -> { binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE) binding.ssiv.setScaleAndCenter( binding.ssiv.maxScale, - PointF(0f, 0f) + PointF(0f, 0f), ) } } @@ -115,9 +115,9 @@ open class PageHolder( override fun onError(e: Throwable) { bindingInfo.textViewError.text = e.getDisplayMessage(context.resources) bindingInfo.buttonRetry.setText( - ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again } + ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again }, ) bindingInfo.layoutError.isVisible = true bindingInfo.progressBar.hideCompat() } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt index 96740a971..fe9122989 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt @@ -24,6 +24,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter +import org.koitharu.kotatsu.utils.ext.getParcelableCompat import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.withArgs @@ -52,7 +53,7 @@ class PagesThumbnailsSheet : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val pages = arguments?.getParcelable(ARG_PAGES)?.pages + val pages = arguments?.getParcelableCompat(ARG_PAGES)?.pages if (pages.isNullOrEmpty()) { dismissAllowingStateLoss() return diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 9148a373b..2779c5d33 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -99,7 +99,7 @@ class RemoteListFragment : MangaListFragment() { override fun onQueryTextChange(newText: String?): Boolean = false - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) return true } diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt index f203f2e4c..d0be7d23c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt @@ -10,10 +10,13 @@ private const val USER_AGENT_SHIKIMORI = "Kotatsu" class ShikimoriInterceptor(private val storage: ShikimoriStorage) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request().newBuilder() + val sourceRequest = chain.request() + val request = sourceRequest.newBuilder() request.header(CommonHeaders.USER_AGENT, USER_AGENT_SHIKIMORI) - storage.accessToken?.let { - request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") + if (!sourceRequest.url.pathSegments.contains("oauth")) { + storage.accessToken?.let { + request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") + } } val response = chain.proceed(request.build()) if (!response.isSuccessful && !response.isRedirect) { @@ -21,4 +24,4 @@ class ShikimoriInterceptor(private val storage: ShikimoriStorage) : Interceptor } return response } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt index 2fcd12718..2267994ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt @@ -40,13 +40,14 @@ class ShikimoriRepository( suspend fun authorize(code: String?) { val body = FormBody.Builder() - body.add("grant_type", "authorization_code") body.add("client_id", BuildConfig.SHIKIMORI_CLIENT_ID) body.add("client_secret", BuildConfig.SHIKIMORI_CLIENT_SECRET) if (code != null) { + body.add("grant_type", "authorization_code") body.add("redirect_uri", REDIRECT_URI) body.add("code", code) } else { + body.add("grant_type", "refresh_token") body.add("refresh_token", checkNotNull(storage.refreshToken)) } val request = Request.Builder() diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt index 06045eae9..151452acb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikiMangaSelectionDe import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikimoriSelectorAdapter import org.koitharu.kotatsu.utils.ext.assistedViewModels import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.requireParcelable import org.koitharu.kotatsu.utils.ext.withArgs @AndroidEntryPoint @@ -47,7 +48,7 @@ class ScrobblingSelectorBottomSheet : private val viewModel by assistedViewModels { viewModelFactory.create( - requireNotNull(requireArguments().getParcelable(MangaIntent.KEY_MANGA)).manga, + requireArguments().requireParcelable(MangaIntent.KEY_MANGA).manga, ) } @@ -84,7 +85,7 @@ class ScrobblingSelectorBottomSheet : dismiss() } viewModel.searchQuery.observe(viewLifecycleOwner) { - binding.headerBar.toolbar.subtitle = it + binding.headerBar.subtitle = it } } @@ -102,7 +103,7 @@ class ScrobblingSelectorBottomSheet : viewModel.loadList(append = true) } - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { setExpanded(isExpanded = true, isLocked = true) return true } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt index b3eca260d..6247a3707 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment +import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat @AndroidEntryPoint class MangaListActivity : @@ -29,7 +30,7 @@ class MangaListActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityContainerBinding.inflate(layoutInflater)) - val tags = intent.getParcelableExtra(EXTRA_TAGS)?.tags + val tags = intent.getParcelableExtraCompat(EXTRA_TAGS)?.tags supportActionBar?.setDisplayHomeAsUpEnabled(true) val source = intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: tags?.firstOrNull()?.source if (source == null) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt deleted file mode 100644 index 7d8cccdd8..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt +++ /dev/null @@ -1,110 +0,0 @@ -package org.koitharu.kotatsu.settings - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import androidx.activity.ComponentActivity -import androidx.annotation.MainThread -import androidx.core.net.toUri -import com.google.android.material.R as materialR -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.security.MessageDigest -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.BuildConfig -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.github.AppUpdateRepository -import org.koitharu.kotatsu.core.github.AppVersion -import org.koitharu.kotatsu.core.github.VersionId -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.parsers.util.byte2HexFormatted -import org.koitharu.kotatsu.utils.FileSize -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug - -@Deprecated("") -class AppUpdateChecker(private val activity: ComponentActivity) { - - private val settings: AppSettings = TODO() - private val repo: AppUpdateRepository = TODO() - - suspend fun checkIfNeeded(): Boolean? = if ( - settings.isUpdateCheckingEnabled && - settings.lastUpdateCheckTimestamp + PERIOD < System.currentTimeMillis() - ) { - checkNow() - } else { - null - } - - suspend fun checkNow() = runCatching { - val version = repo.fetchUpdate() ?: return@runCatching false - val newVersionId = VersionId(version.name) - val currentVersionId = VersionId(BuildConfig.VERSION_NAME) - val result = newVersionId > currentVersionId - if (result) { - withContext(Dispatchers.Main) { - showUpdateDialog(version) - } - } - settings.lastUpdateCheckTimestamp = System.currentTimeMillis() - result - }.onFailure { - it.printStackTraceDebug() - }.getOrNull() - - @MainThread - private fun showUpdateDialog(version: AppVersion) { - val message = buildString { - append(activity.getString(R.string.new_version_s, version.name)) - appendLine() - append(activity.getString(R.string.size_s, FileSize.BYTES.format(activity, version.apkSize))) - appendLine() - appendLine() - append(version.description) - } - MaterialAlertDialogBuilder(activity, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered) - .setTitle(R.string.app_update_available) - .setMessage(message) - .setIcon(R.drawable.ic_app_update) - .setPositiveButton(R.string.download) { _, _ -> - val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri()) - activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.open_in_browser))) - } - .setNegativeButton(R.string.close, null) - .setCancelable(false) - .create() - .show() - } - - companion object { - - private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE" - private val PERIOD = TimeUnit.HOURS.toMillis(6) - - fun isUpdateSupported(context: Context): Boolean { - return BuildConfig.DEBUG || getCertificateSHA1Fingerprint(context) == CERT_SHA1 - } - - @Suppress("DEPRECATION") - @SuppressLint("PackageManagerGetSignatures") - private fun getCertificateSHA1Fingerprint(context: Context): String? = runCatching { - val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES) - val signatures = requireNotNull(packageInfo?.signatures) - val cert: ByteArray = signatures.first().toByteArray() - val input: InputStream = ByteArrayInputStream(cert) - val cf = CertificateFactory.getInstance("X509") - val c = cf.generateCertificate(input) as X509Certificate - val md: MessageDigest = MessageDigest.getInstance("SHA1") - val publicKey: ByteArray = md.digest(c.encoded) - return publicKey.byte2HexFormatted() - }.onFailure { error -> - error.printStackTraceDebug() - }.getOrNull() - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index 038e07edf..4389c39a6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.launch import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.network.AndroidCookieJar +import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.LocalStorageManager @@ -41,8 +42,13 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach @Inject lateinit var cookieJar: AndroidCookieJar + @Inject + lateinit var shortcutsUpdater: ShortcutsUpdater + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_history) + findPreference(AppSettings.KEY_SHORTCUTS)?.isVisible = + shortcutsUpdater.isDynamicShortcutsAvailable() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 3436ca259..eefc9ba6d 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 dagger.hilt.android.AndroidEntryPoint @@ -53,7 +55,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { super.onViewCreated(view, savedInstanceState) findPreference(KEY_AUTH)?.run { if (isVisible) { - loadUsername(this) + loadUsername(viewLifecycleOwner, this) } } } @@ -68,7 +70,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) { @@ -99,7 +101,8 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { 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/settings/about/AboutSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt index 985ca0d1a..199cc513e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt @@ -2,34 +2,41 @@ package org.koitharu.kotatsu.settings.about import android.content.Intent import android.os.Bundle +import android.view.View import androidx.core.net.toUri +import androidx.fragment.app.viewModels import androidx.preference.Preference -import kotlinx.coroutines.launch +import com.google.android.material.snackbar.Snackbar import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.settings.AppUpdateChecker -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { + private val viewModel by viewModels() + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_about) - val isUpdateSupported = AppUpdateChecker.isUpdateSupported(requireContext()) - findPreference(AppSettings.KEY_APP_UPDATE_AUTO)?.run { - isVisible = isUpdateSupported - } findPreference(AppSettings.KEY_APP_VERSION)?.run { title = getString(R.string.app_version, BuildConfig.VERSION_NAME) - isEnabled = isUpdateSupported + isEnabled = viewModel.isUpdateSupported + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.isLoading.observe(viewLifecycleOwner) { + findPreference(AppSettings.KEY_APP_UPDATE)?.isEnabled = !it } + viewModel.onUpdateAvailable.observe(viewLifecycleOwner, ::onUpdateAvailable) } override fun onPreferenceTreeClick(preference: Preference): Boolean { return when (preference.key) { AppSettings.KEY_APP_VERSION -> { - checkForUpdates() + viewModel.checkForUpdates() true } AppSettings.KEY_APP_TRANSLATION -> { @@ -40,24 +47,12 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { } } - private fun checkForUpdates() { - viewLifecycleScope.launch { - findPreference(AppSettings.KEY_APP_VERSION)?.run { - setSummary(R.string.checking_for_updates) - isSelectable = false - } - val result = AppUpdateChecker(activity ?: return@launch).checkNow() - findPreference(AppSettings.KEY_APP_VERSION)?.run { - setSummary( - when (result) { - true -> R.string.check_for_updates - false -> R.string.no_update_available - null -> R.string.update_check_failed - } - ) - isSelectable = true - } + private fun onUpdateAvailable(version: AppVersion?) { + if (version == null) { + Snackbar.make(listView, R.string.no_update_available, Snackbar.LENGTH_SHORT).show() + return } + AppUpdateDialog(context ?: return).show(version) } private fun openLink(url: String, title: CharSequence?) { @@ -68,7 +63,7 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { Intent.createChooser(intent, title) } else { intent - } + }, ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt new file mode 100644 index 000000000..5289f751c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt @@ -0,0 +1,24 @@ +package org.koitharu.kotatsu.settings.about + +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.github.AppUpdateRepository +import org.koitharu.kotatsu.core.github.AppVersion +import org.koitharu.kotatsu.utils.SingleLiveEvent + +@HiltViewModel +class AboutSettingsViewModel @Inject constructor( + private val appUpdateRepository: AppUpdateRepository, +) : BaseViewModel() { + + val isUpdateSupported = appUpdateRepository.isUpdateSupported() + val onUpdateAvailable = SingleLiveEvent() + + fun checkForUpdates() { + launchLoadingJob { + val update = appUpdateRepository.fetchUpdate() + onUpdateAvailable.call(update) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt new file mode 100644 index 000000000..4752a7461 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt @@ -0,0 +1,39 @@ +package org.koitharu.kotatsu.settings.about + +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri +import com.google.android.material.R as materialR +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.github.AppVersion +import org.koitharu.kotatsu.utils.FileSize + +class AppUpdateDialog(private val context: Context) { + + fun show(version: AppVersion) { + val message = buildString { + append(context.getString(R.string.new_version_s, version.name)) + appendLine() + append(context.getString(R.string.size_s, FileSize.BYTES.format(context, version.apkSize))) + appendLine() + appendLine() + append(version.description) + } + MaterialAlertDialogBuilder( + context, + materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered, + ) + .setTitle(R.string.app_update_available) + .setMessage(message) + .setIcon(R.drawable.ic_app_update) + .setPositiveButton(R.string.download) { _, _ -> + val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri()) + context.startActivity(Intent.createChooser(intent, context.getString(R.string.open_in_browser))) + } + .setNegativeButton(R.string.close, null) + .setCancelable(false) + .create() + .show() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt index 65d0a0d47..825575ad9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt @@ -51,8 +51,9 @@ class BackupDialogFragment : AlertDialogFragment() { viewModel.onError.observe(viewLifecycleOwner, this::onError) } - override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { - builder.setCancelable(false) + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setCancelable(false) .setNegativeButton(android.R.string.cancel, null) } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt index 5af836d4b..68edfe874 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt @@ -44,8 +44,9 @@ class RestoreDialogFragment : AlertDialogFragment() { viewModel.onError.observe(viewLifecycleOwner, this::onError) } - override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { - builder.setCancelable(false) + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setCancelable(false) } private fun onError(e: Throwable) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt index c500a8527..ac855f1e1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt @@ -43,8 +43,8 @@ class NewSourcesDialogFragment : viewModel.sources.observe(viewLifecycleOwner) { adapter.items = it } } - override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { - builder + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) .setPositiveButton(R.string.done, this) .setCancelable(true) .setTitle(R.string.remote_sources) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt index 6cb8eb25e..74620b78f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt @@ -39,8 +39,8 @@ class OnboardDialogFragment : container: ViewGroup?, ) = DialogOnboardBinding.inflate(inflater, container, false) - override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { - builder + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + super.onBuildDialog(builder) .setPositiveButton(R.string.done, this) .setCancelable(true) if (isWelcome) { @@ -50,6 +50,7 @@ class OnboardDialogFragment : .setTitle(R.string.remote_sources) .setNegativeButton(android.R.string.cancel, this) } + return builder } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index 816d9d31b..e1962a000 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -120,7 +120,7 @@ class SourcesSettingsFragment : else -> false } - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) return true } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt index 58b9d6658..8cb75ead8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.databinding.FragmentToolsBinding import org.koitharu.kotatsu.download.ui.DownloadsActivity import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.about.AppUpdateDialog import org.koitharu.kotatsu.settings.tools.model.StorageUsage import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.ext.getThemeColor @@ -68,6 +69,10 @@ class ToolsFragment : intent.data = url.toUri() startActivity(Intent.createChooser(intent, getString(R.string.open_in_browser))) } + R.id.card_update -> { + val version = viewModel.appUpdate.value ?: return + AppUpdateDialog(v.context).show(version) + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt index d8d19d243..6b98d88db 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt @@ -15,7 +15,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.ext.onFirst @HiltViewModel @@ -44,8 +44,8 @@ class SuggestionsViewModel @Inject constructor( }.onFirst { loadingCounter.decrement() }.catch { - it.toErrorState(canRetry = false) - }.asLiveDataDistinct( + emit(listOf(it.toErrorState(canRetry = false))) + }.asFlowLiveData( viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState), ) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt index 452f60f8c..9f8099612 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt @@ -5,9 +5,26 @@ import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem -fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem( - id = trackLog.id, - chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() }, - manga = manga.toManga(tags.toMangaTags()), - createdAt = Date(trackLog.createdAt) -) \ No newline at end of file +fun TrackLogWithManga.toTrackingLogItem(counters: MutableMap): TrackingLogItem { + val chaptersList = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() } + return TrackingLogItem( + id = trackLog.id, + chapters = chaptersList, + manga = manga.toManga(tags.toMangaTags()), + createdAt = Date(trackLog.createdAt), + isNew = counters.decrement(trackLog.mangaId, chaptersList.size), + ) +} + +private fun MutableMap.decrement(key: Long, count: Int): Boolean { + val counter = get(key) + if (counter == null || counter <= 0) { + return false + } + if (counter < count) { + remove(key) + } else { + put(key, counter - count) + } + return true +} diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt index 118480f75..0eb482202 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -18,6 +18,10 @@ abstract class TracksDao { @Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId") abstract suspend fun findNewChapters(mangaId: Long): Int? + @MapInfo(keyColumn = "manga_id", valueColumn = "chapters_new") + @Query("SELECT manga_id, chapters_new FROM tracks") + abstract fun observeNewChaptersMap(): Flow> + @Query("SELECT chapters_new FROM tracks") abstract fun observeNewChapters(): Flow> diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index ce3f9cda9..2fdf8b404 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -5,6 +5,8 @@ import androidx.room.withTransaction import java.util.* import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.MangaEntity @@ -73,9 +75,15 @@ class TrackingRepository @Inject constructor( db.tracksDao.delete(mangaId) } - suspend fun getTrackingLog(offset: Int, limit: Int): List { - return db.trackLogsDao.findAll(offset, limit).map { x -> - x.toTrackingLogItem() + fun observeTrackingLog(limit: Flow): Flow> { + return limit.flatMapLatest { limitValue -> + combine( + db.tracksDao.observeNewChaptersMap(), + db.trackLogsDao.observeAll(limitValue), + ) { counters, entities -> + val countersMap = counters.toMutableMap() + entities.map { x -> x.toTrackingLogItem(countersMap) } + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt index c5021eaf3..286a3a5a4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt @@ -7,5 +7,6 @@ data class TrackingLogItem( val id: Long, val manga: Manga, val chapters: List, - val createdAt: Date -) \ No newline at end of file + val createdAt: Date, + val isNew: Boolean, +) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt index c55eb11ab..ef2022fa0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt @@ -137,7 +137,7 @@ class FeedFragment : } override fun onScrolledToEnd() { - viewModel.loadList(append = true) + viewModel.requestMoreItems() } override fun onItemClick(item: Manga, view: View) { diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt index 6974e053e..1511cc217 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt @@ -4,44 +4,39 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import java.util.* import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem import org.koitharu.kotatsu.tracker.ui.model.toFeedItem import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.ext.daysDiff +private const val PAGE_SIZE = 20 + @HiltViewModel class FeedViewModel @Inject constructor( private val repository: TrackingRepository, ) : BaseViewModel() { - private val logList = MutableStateFlow?>(null) - private val hasNextPage = MutableStateFlow(false) - private var loadingJob: Job? = null + private val limit = MutableStateFlow(PAGE_SIZE) + private val isReady = AtomicBoolean(false) val onFeedCleared = SingleLiveEvent() - val content = combine( - logList.filterNotNull(), - hasNextPage, - ) { list, isHasNextPage -> - buildList(list.size + 2) { + val content = repository.observeTrackingLog(limit) + .map { list -> if (list.isEmpty()) { - add( + listOf( EmptyState( icon = R.drawable.ic_empty_feed, textPrimary = R.string.text_empty_holder_primary, @@ -50,48 +45,26 @@ class FeedViewModel @Inject constructor( ), ) } else { - list.mapListTo(this) - if (isHasNextPage) { - add(LoadingFooter) - } + isReady.set(true) + list.mapList() } - } - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) - - init { - loadList(append = false) - } - - fun loadList(append: Boolean) { - if (loadingJob?.isActive == true) { - return - } - if (append && !hasNextPage.value) { - return - } - loadingJob = launchLoadingJob(Dispatchers.Default) { - val offset = if (append) logList.value?.size ?: 0 else 0 - val list = repository.getTrackingLog(offset, 20) - if (!append) { - logList.value = list - } else if (list.isNotEmpty()) { - logList.value = logList.value?.plus(list) ?: list - } - hasNextPage.value = list.isNotEmpty() - } - } + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) fun clearFeed() { - val lastJob = loadingJob - loadingJob = launchLoadingJob(Dispatchers.Default) { - lastJob?.cancelAndJoin() + launchLoadingJob(Dispatchers.Default) { repository.clearLogs() - logList.value = emptyList() onFeedCleared.postCall(Unit) } } - private fun List.mapListTo(destination: MutableList) { + fun requestMoreItems() { + if (isReady.compareAndSet(true, false)) { + limit.value += PAGE_SIZE + } + } + + private fun List.mapList(): List { + val destination = ArrayList((size * 1.4).toInt()) var prevDate: DateTimeAgo? = null for (item in this) { val date = timeAgo(item.createdAt) @@ -101,6 +74,7 @@ class FeedViewModel @Inject constructor( prevDate = date destination += item.toFeedItem() } + return destination } private fun timeAgo(date: Date): DateTimeAgo { diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt index 522dad2f8..96375fe47 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt @@ -11,22 +11,23 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.ui.model.FeedItem import org.koitharu.kotatsu.utils.ext.disposeImageRequest import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.isLowRamDevice +import org.koitharu.kotatsu.utils.ext.isBold import org.koitharu.kotatsu.utils.ext.newImageRequest fun feedItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, - clickListener: OnListItemClickListener + clickListener: OnListItemClickListener, ) = adapterDelegateViewBinding( - { inflater, parent -> ItemFeedBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemFeedBinding.inflate(inflater, parent, false) }, ) { - itemView.setOnClickListener { clickListener.onItemClick(item.manga, it) } bind { + binding.textViewTitle.isBold = item.isNew + binding.textViewSummary.isBold = item.isNew binding.imageViewCover.newImageRequest(item.imageUrl)?.run { placeholder(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt index 1cdce1869..4d1db25f9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt @@ -9,4 +9,5 @@ data class FeedItem( val title: String, val manga: Manga, val count: Int, -) : ListModel \ No newline at end of file + val isNew: Boolean, +) : ListModel diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt index b12c4ecff..4e2b91233 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt @@ -8,4 +8,5 @@ fun TrackingLogItem.toFeedItem() = FeedItem( title = manga.title, count = chapters.size, manga = manga, -) \ No newline at end of file + isNew = isNew, +) 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 +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/IdlingDetector.kt b/app/src/main/java/org/koitharu/kotatsu/utils/IdlingDetector.kt new file mode 100644 index 000000000..0501a3da6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/IdlingDetector.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.utils + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner + +class IdlingDetector( + private val timeoutMs: Long, + private val callback: Callback, +) : DefaultLifecycleObserver { + + private val handler = Handler(Looper.getMainLooper()) + private val idleRunnable = Runnable { + callback.onIdle() + } + + fun bindToLifecycle(owner: LifecycleOwner) { + owner.lifecycle.addObserver(this) + } + + fun onUserInteraction() { + handler.removeCallbacks(idleRunnable) + handler.postDelayed(idleRunnable, timeoutMs) + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + owner.lifecycle.removeObserver(this) + handler.removeCallbacks(idleRunnable) + } + + fun interface Callback { + + fun onIdle() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt new file mode 100644 index 000000000..3d7b2523c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.utils.ext + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import java.io.Serializable + +@Suppress("DEPRECATION") +inline fun Bundle.getParcelableCompat(key: String): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelable(key, T::class.java) + } else { + getParcelable(key) as? T + } +} + +@Suppress("DEPRECATION") +inline fun Intent.getParcelableExtraCompat(key: String): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableExtra(key, T::class.java) + } else { + getParcelableExtra(key) as? T + } +} + +@Suppress("DEPRECATION") +inline fun Bundle.getSerializableCompat(key: String): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getSerializable(key, T::class.java) + } else { + getSerializable(key) as? T + } +} + +inline fun Bundle.requireSerializable(key: String): T { + return checkNotNull(getSerializableCompat(key)) { + "Serializable of type \"${T::class.java.name}\" not found at \"$key\"" + } +} + +inline fun Bundle.requireParcelable(key: String): T { + return checkNotNull(getParcelableCompat(key)) { + "Parcelable of type \"${T::class.java.name}\" not found at \"$key\"" + } +} 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 6d7e49a77..0950a69a4 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 @@ -10,6 +10,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.R import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener @@ -47,7 +48,18 @@ 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 { @@ -63,3 +75,11 @@ fun ImageRequest.Builder.crossfade(context: Context?): ImageRequest.Builder { val duration = context.resources.getInteger(R.integer.config_defaultAnimTime) * context.animatorDurationScale return crossfade(duration.toInt()) } + +private fun String.baseUrl(): String? { + return (this.toHttpUrlOrNull()?.newBuilder("/") ?: return null) + .username("") + .password("") + .build() + .toString() +} 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 ea4132ac5..5ea1fb4af 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 @@ -1,14 +1,17 @@ package org.koitharu.kotatsu.utils.ext import android.os.Bundle -import android.os.Parcelable import androidx.core.view.MenuProvider 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) @@ -20,18 +23,10 @@ inline fun T.withArgs(size: Int, block: Bundle.() -> Unit): T { val Fragment.viewLifecycleScope inline get() = viewLifecycleOwner.lifecycle.coroutineScope -fun Fragment.parcelableArgument(name: String): Lazy { - return lazy(LazyThreadSafetyMode.NONE) { - requireNotNull(arguments?.getParcelable(name)) { - "No argument $name passed into ${javaClass.simpleName}" - } - } -} - fun Fragment.serializableArgument(name: String): Lazy { return lazy(LazyThreadSafetyMode.NONE) { @Suppress("UNCHECKED_CAST") - requireNotNull(arguments?.getSerializable(name)) { + requireNotNull(arguments?.getSerializableCompat(name)) { "No argument $name passed into ${javaClass.simpleName}" } as T } @@ -50,3 +45,19 @@ fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) { fun Fragment.addMenuProvider(provider: MenuProvider) { requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.STARTED) } + +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) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt index e053cd5a2..2197c4348 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.utils.ext +import android.graphics.Typeface import android.graphics.drawable.Drawable import android.view.View import android.widget.TextView @@ -48,3 +49,15 @@ fun TextView.setTextAndVisible(@StringRes textResId: Int) { fun TextView.setTextColorAttr(@AttrRes attrResId: Int) { setTextColor(context.getThemeColorStateList(attrResId)) } + +var TextView.isBold: Boolean + get() = typeface.isBold + set(value) { + var style = typeface.style + style = if (value) { + style or Typeface.BOLD + } else { + style and Typeface.BOLD.inv() + } + setTypeface(typeface, style) + } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt index 7896da2e5..6f034b7e2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt @@ -2,11 +2,14 @@ package org.koitharu.kotatsu.utils.ext import android.content.Context import android.graphics.Color +import android.widget.TextView import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.FloatRange +import androidx.annotation.StyleRes import androidx.core.content.res.use import androidx.core.graphics.ColorUtils +import androidx.core.widget.TextViewCompat fun Context.getThemeDrawable( @AttrRes resId: Int, @@ -43,3 +46,9 @@ fun Context.getThemeColorStateList( ) = obtainStyledAttributes(intArrayOf(resId)).use { it.getColorStateList(0) } + +fun TextView.setThemeTextAppearance(@AttrRes resId: Int, @StyleRes fallback: Int) { + context.obtainStyledAttributes(intArrayOf(resId)).use { + TextViewCompat.setTextAppearance(this, it.getResourceId(0, fallback)) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index e6c4e11e1..12b2abe7b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext import android.app.Activity import android.graphics.Rect import android.view.View +import android.view.View.MeasureSpec import android.view.ViewGroup import android.view.ViewParent import android.view.inputmethod.InputMethodManager @@ -167,3 +168,19 @@ val View.parents: Sequence p = p.parent } } + +@Suppress("unused") +fun View.measureDimension(desiredSize: Int, measureSpec: Int): Int { + var result: Int + val specMode = MeasureSpec.getMode(measureSpec) + val specSize = MeasureSpec.getSize(measureSpec) + if (specMode == MeasureSpec.EXACTLY) { + result = specSize + } else { + result = desiredSize + if (specMode == MeasureSpec.AT_MOST) { + result = result.coerceAtMost(specSize) + } + } + return result +} diff --git a/app/src/main/res/layout-w600dp/fragment_details.xml b/app/src/main/res/layout-w600dp/fragment_details.xml index a9f6926dc..0244030a2 100644 --- a/app/src/main/res/layout-w600dp/fragment_details.xml +++ b/app/src/main/res/layout-w600dp/fragment_details.xml @@ -181,11 +181,11 @@ android:id="@+id/textView_description" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="16dp" - android:layout_marginBottom="16dp" + android:layout_marginStart="@dimen/margin_normal" + android:layout_marginTop="@dimen/margin_small" + android:layout_marginEnd="@dimen/margin_normal" android:lineSpacingMultiplier="1.2" + android:paddingBottom="@dimen/margin_normal" android:textAppearance="?attr/textAppearanceBodyMedium" android:textIsSelectable="true" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout-w720dp/activity_details.xml b/app/src/main/res/layout-w720dp/activity_details.xml index 2fae1012e..91234ae5d 100644 --- a/app/src/main/res/layout-w720dp/activity_details.xml +++ b/app/src/main/res/layout-w720dp/activity_details.xml @@ -63,56 +63,66 @@ app:layout_constraintWidth_percent="0.5" tools:layout="@layout/fragment_details" /> - - - + app:layout_constraintTop_toBottomOf="@id/appbar"> - + - + + + + + + + diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml index 42d944b1b..13d34ca0d 100644 --- a/app/src/main/res/layout/activity_details.xml +++ b/app/src/main/res/layout/activity_details.xml @@ -42,9 +42,11 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" + android:visibility="gone" app:behavior_hideable="false" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" - app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.BottomSheet"> + app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.BottomSheet" + tools:visibility="visible"> + android:layout_height="match_parent" + app:restoreStrategy="deferred" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_reader.xml b/app/src/main/res/layout/activity_reader.xml index c166f683d..06ad562fe 100644 --- a/app/src/main/res/layout/activity_reader.xml +++ b/app/src/main/res/layout/activity_reader.xml @@ -28,9 +28,9 @@ diff --git a/app/src/main/res/layout/dialog_manga_error.xml b/app/src/main/res/layout/dialog_manga_error.xml new file mode 100644 index 000000000..be84d7bbc --- /dev/null +++ b/app/src/main/res/layout/dialog_manga_error.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 90db85516..342179654 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -193,11 +193,11 @@ android:id="@+id/textView_description" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="16dp" - android:layout_marginBottom="16dp" + android:layout_marginStart="@dimen/margin_normal" + android:layout_marginTop="@dimen/margin_small" + android:layout_marginEnd="@dimen/margin_normal" android:lineSpacingMultiplier="1.2" + android:paddingBottom="@dimen/margin_normal" android:textAppearance="?attr/textAppearanceBodyMedium" android:textIsSelectable="true" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/item_page.xml b/app/src/main/res/layout/item_page.xml index ee57ce9ff..3634cf169 100644 --- a/app/src/main/res/layout/item_page.xml +++ b/app/src/main/res/layout/item_page.xml @@ -1,6 +1,7 @@ + android:layout_height="match_parent" + app:restoreStrategy="deferred" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/layout_app_update.xml b/app/src/main/res/layout/layout_app_update.xml index 046ba497a..a11a63c20 100644 --- a/app/src/main/res/layout/layout_app_update.xml +++ b/app/src/main/res/layout/layout_app_update.xml @@ -2,7 +2,6 @@ @@ -61,4 +60,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/layout_details_info.xml b/app/src/main/res/layout/layout_details_info.xml index 6ad4f61d6..8551c127b 100644 --- a/app/src/main/res/layout/layout_details_info.xml +++ b/app/src/main/res/layout/layout_details_info.xml @@ -62,6 +62,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:background="?selectableItemBackgroundBorderless" android:drawablePadding="4dp" android:gravity="center" android:padding="4dp" diff --git a/app/src/main/res/layout/layout_sheet_header.xml b/app/src/main/res/layout/layout_sheet_header.xml index 47ea53857..d0ec2a0e3 100644 --- a/app/src/main/res/layout/layout_sheet_header.xml +++ b/app/src/main/res/layout/layout_sheet_header.xml @@ -6,27 +6,20 @@ android:layout_height="wrap_content" tools:parentTag="com.google.android.material.appbar.AppBarLayout"> - + android:layout_height="@dimen/bottom_sheet_handle_size_min" + android:minHeight="0dp" + android:paddingTop="12dp" + android:paddingBottom="0dp" /> - - - - - + diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml index 076467e11..e897803dd 100644 --- a/app/src/main/res/layout/preference_toggle_header.xml +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -9,7 +9,7 @@ android:layout_marginHorizontal="16dp" android:layout_marginVertical="8dp" android:paddingBottom="8dp" - app:cardBackgroundColor="?colorOnPrimary" + app:cardBackgroundColor="?colorPrimaryContainer" app:cardCornerRadius="24dp"> - + app:drawableStartCompat="@drawable/ic_timer" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/slider_timer" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_default="wrap" /> - - + app:labelBehavior="floating" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/textView_timer" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_min="120dp" /> + + Balik Diantrikan Berhasil Diotorisasi + Simpan dari sumber daring atau berkas impor. + Manga Anda akan ditampilkan di sini + Cari apa untuk dibaca di bagian «Jelajah» + Masukkan surel Anda untuk melanjutkan + Dibaca ulang + Jelajah + Direncanakan + Selesai + Dibatalkan + Sinkronisasi data Anda + Masukkan surel Anda untuk melanjutkan + Pelacakan + Keluar + Sinkronisasi + Terkirim + Dibaca + Ditunda + Domain tidak valid + Belum ada markah + Anda bisa membuat markah ketika membaca manga + Markah dihapus + Tidak ada sumber manga + Acak + Kosong + Daftar Perubahan + Dihapus dari \"%s\" + Tampilkan bilah informasi di pembaca + Mengimpor manga + Dua jam terakhir + Mengunduh manga + Tampilkan semua + Riwayat dihapus + Hapus semua riwayat + Pilihan + Impor selesai + Dihapus dari favorit + Impor akan segera dimulai + Konten tidak ditemukan atau dihapus \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7cfacd508..c1d0e5e88 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -372,4 +372,6 @@ Ch.%1$d/%2$d Pg.%3$d/%4$d リーダーで情報バーを表示する コミックアーカイブ + 漫画をダウンロードする + フィード \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 7c203b135..67ff86917 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,375 +1,377 @@ - Закрыть меню - Открыть меню - На устройстве - Избранное - История - Произошла ошибка - Не удалось подключиться к интернету - Подробности - Главы - Список - Подробный список - Таблица - Вид списка - Настройки - Онлайн каталоги - Загрузка… - Глава %1$d из %2$d - Закрыть - Повторить - Очистить историю - Ничего не найдено - Истории пока нет - Читать - Избранного пока нет - В избранное - Новая категория - Добавить - Введите название - Сохранить - Поделиться - Создать ярлык… - Поделиться %s - Поиск - Поиск манги - Загрузка… - Обработка… - Загружено - Загрузки - Имя - Популярная - Обновлённая - Новая - Рейтинг - Порядок сортировки - Фильтр - Тема - Светлая - Тёмная - Как в системе - Страницы - Очистить - Очистить всю историю чтения полностью\? - Удалить - «%s» удалено из истории - «%s» удалено с устройства - Дождитесь завершения загрузки… - Сохранить страницу - Сохранено - Поделиться изображением - Импорт - Удалить - Операция не поддерживается - Выберите файл в формате CBZ или ZIP. - Описание отсутствует - История и кэш - Очистить кэш страниц - Кэш - Б|кБ|МБ|ГБ|ТБ - Стандартный - Манхва - Режим чтения - Размер сетки - Поиск по %s - Удалить мангу - Удалить \"%s\" с устройства навсегда\? - Настройки режима чтения - Листание страниц - Нажатия по краям - Кнопки громкости - Продолжить - Предупреждение - Это может привести к большому расходу трафика - Больше не спрашивать - Отмена… - Ошибка - Очистить кэш миниатюр - Очистить историю поиска - Очищено - Только жесты - Внутренний накопитель - Внешний накопитель - Домен - Проверять наличие новых версий приложения - Доступна новая версия приложения - Показ уведомления, если доступна новая версия - Открыть в веб-браузере - В этой манге %s. Сохранить их все\? - Сохранить - Уведомления - Включено %1$d из %2$d - Новые главы - Загрузить - Читать с начала - Перезапустить - Настройки уведомлений - Звук уведомления - Светодиодная индикация - Вибросигнал - Категории избранного - Категории… - Переименовать - Удалить категорию \"%s\" из избранного\? + Закрыть меню + Открыть меню + На устройстве + Избранное + История + Произошла ошибка + Не удалось подключиться к интернету + Подробности + Главы + Список + Подробный список + Таблица + Вид списка + Настройки + Онлайн каталоги + Загрузка… + Глава %1$d из %2$d + Закрыть + Повторить + Очистить историю + Ничего не найдено + Истории пока нет + Читать + Избранного пока нет + В избранное + Новая категория + Добавить + Введите название + Сохранить + Поделиться + Создать ярлык… + Поделиться %s + Поиск + Поиск манги + Загрузка… + Обработка… + Загружено + Загрузки + Имя + Популярная + Обновлённая + Новая + Рейтинг + Порядок сортировки + Фильтр + Тема + Светлая + Тёмная + Как в системе + Страницы + Очистить + Очистить всю историю чтения полностью\? + Удалить + «%s» удалено из истории + «%s» удалено с устройства + Дождитесь завершения загрузки… + Сохранить страницу + Сохранено + Поделиться изображением + Импорт + Удалить + Операция не поддерживается + Выберите файл в формате CBZ или ZIP. + Описание отсутствует + История и кэш + Очистить кэш страниц + Кэш + Б|кБ|МБ|ГБ|ТБ + Стандартный + Манхва + Режим чтения + Размер сетки + Поиск по %s + Удалить мангу + Удалить \"%s\" с устройства навсегда\? + Настройки режима чтения + Листание страниц + Нажатия по краям + Кнопки громкости + Продолжить + Предупреждение + Это может привести к большому расходу трафика + Больше не спрашивать + Отмена… + Ошибка + Очистить кэш миниатюр + Очистить историю поиска + Очищено + Только жесты + Внутренний накопитель + Внешний накопитель + Домен + Проверять наличие новых версий приложения + Доступна новая версия приложения + Показ уведомления, если доступна новая версия + Открыть в веб-браузере + В этой манге %s. Сохранить их все\? + Сохранить + Уведомления + Включено %1$d из %2$d + Новые главы + Загрузить + Читать с начала + Перезапустить + Настройки уведомлений + Звук уведомления + Светодиодная индикация + Вибросигнал + Категории избранного + Категории… + Переименовать + Удалить категорию \"%s\" из избранного\? \nВся манга в ней будет потеряна. - Удалить - Как-то здесь пусто… - Попробуйте переформулировать запрос. - Вы можете использовать категории для организации своих избранных. Нажмите «+», чтобы создать категорию - То, что вы прочитаете, будет отображено здесь - Найдите, что почитать, в боковом меню. - Сохраните что-нибудь - Сохраните что-нибудь из онлайн-каталога или импортируйте из файла. - Полка - Недавнее - Анимация листания - Папка для загрузок - Недоступно - Нет доступного хранилища - Другое хранилище - Готово - Всё избранное - Категория пуста - Прочитать позже - Обновления - Новые главы из того, что вы читаете, будут показаны здесь - Результаты поиска - Похожие - Новая версия: %s - Размер: %s - Ожидание подключения… - Очистить ленту обновлений - Очищено - Повернуть экран - Обновить - Обновление скоро начнётся - Следить за обновлениями - Не проверять - Введите пароль - Неверный пароль - Защитить приложение - Запрашивать пароль при запуске Kotatsu - Повторите пароль - Пароли не совпадают - О программе - Версия %s - Проверить обновления - Проверка обновления… - Не удалось проверить обновления - Нет доступных обновлений - Справа налево - Создать категорию - Масштабирование - Вписать в экран - Подогнать по высоте - Подогнать по ширине - Исходный размер - Чёрная - Потребляет меньше энергии на экранах AMOLED - Резервное копирование и восстановление - Создать резервную копию - Восстановить данные - Восстановлено - Подготовка… - Файл не найден - Все данные были восстановлены - Данные были восстановлены, но возникли некоторые ошибки - Вы можете создать резервную копию избранного и истории и потом восстановить их - Только что - Вчера - Давно - Группировать - Сегодня - Попробовать ещё раз - Выбранный режим будет сохранён для текущей манги - Без звука - Необходимо пройти CAPTCHA - Пройти - Очистить куки - Все файлы cookie были удалены - Проверка новых глав: %1$d из %2$d - Очистить ленту - Удалить всю историю обновлений навсегда\? - Проверка новых глав - В обратном порядке - Войти - Авторизуйтесь, чтобы просмотреть этот контент - По умолчанию: %s - …и ещё %1$d - Далее - Введите пароль для запуска приложения - Подтвердить - Пароль должен состоять из 4 символов или более - Поиск только по %s - Другие - Добро пожаловать - Удалить все последние поисковые запросы навсегда\? - Резервная копия сохранена - Некоторые устройства имеют различное поведение системы, что может привести к нарушению фоновых задач. - Подробнее - В очереди - Нет активных загрузок - Глава отсутствует - Скачайте или прочитайте эту недостающую главу онлайн. - Помочь с переводом приложения - Перевод - Тема на 4PDA - Обратная связь - Авторизация выполнена - Вход в %s не поддерживается - Вы выйдете из всех источников - Жанры - Завершено - Онгоинг - Формат даты - По умолчанию - Исключить NSFW мангу из истории - Вы должны ввести имя - Показывать номера страницы - Включенные источники - Доступные источники - Динамическая тема - Применяет тему приложения, основанную на цветовой палитре обоев на устройстве - Политика скриншотов - Разрешить - Запретить для NSFW - Всегда блокировать - Рекомендации - Включить рекомендации - Предлагать мангу на основе Ваших предпочтений - Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы - Начните читать мангу, чтобы получать персональные предложения - Не предлагать NSFW мангу - Включено - Выключено - Не удалось загрузить список жанров - Вычисление… - Создать проблему на GitHub - Импорт манги: %1$d из %2$d - Сбросить фильтр - Поиск по жанрам - Выберите языки, на которых Вы хоите читать мангу. Это можно будет изменить позже в настройках. - Никогда - Только по Wi-Fi - Всегда - Предварительная загрузка страниц - Вы авторизованы как %s - 18+ - Разные языки - Найти главу - В этой манге нет глав - Оформление - Контент - Обновление рекомендаций - Исключить жанры - Укажите жанры, которые Вы не хотите видеть в рекомендациях - Удалить выбранную мангу с накопителя? - Удаление завершено - Загрузить выбранную мангу со всеми главами? Это может привести к большому расходу трафика и места на накопителе - Загружать параллельно - Замедление загрузки - Помогает избежать блокировки IP-адреса - Обработка сохранённой манги - Главы будут удалены в фоновом режиме. Это может занять какое-то время - Скрыть - Доступны новые источники манги - Проверять новые главы и уведомлять о них - Вы будете получать уведомления об обновлении манги, которую Вы читаете - Вы не будете получать уведомления, но новые главы будут отображаться в списке - Включить уведомления - Название - Изменить - Изменить категорию - Отслеживание - Нет категорий избранного - Добавить закладку - Удалить закладку - Закладки - Закладка удалена - Закладка добавлена - Отменить - Удалено из истории - DNS поверх HTTPS - Режим по умолчанию - Автоопределение режима чтения - Автоматически определяет, является ли манга веб-комиксом - Отключить оптимизацию батареи - Помогает с фоновой проверкой обновлений - Что-то пошло не так. Пожалуйста, отправьте отчёт разработчикам, чтобы помочь всё исправить. - Отправить - Отключить все - Использовать отпечаток пальца, если доступно - Манга из Вашего избранного - Манга, которую Вы недавно читали - Читаю - Запланировано - Отложено - Заброшено - Завершено - Показать процент прочитанного в истории и избранном - Манга, помеченная как NSFW, никогда не будет добавлена в историю и ваш прогресс чтения не будет сохранен - %1$s%% - Отчёт - Выйти - Перечитываю - Показать индикаторы прогресса чтения - Удаление данных - Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы - Показать все - Неверное доменное имя - Найдите, что почитать во разделе «Обзор» - Назад - Выбрать диапазон - История очищена - Настроить - Закладок пока нет - Закладки удалены - Нет источников манги - Включите источники манги для чтения онлайн - Рандом - Упорядочить - Пусто - Обзор - Инструменты - Нажмите Назад ещё раз, чтобы выйти - Нажмите Назад 2 раза для выхода из приложения - Другой кэш - Введите электронную почту, чтобы продолжить - Опции - Контент не найден или был удален - Отключено - %s с - Гл. %1$d/%2$d Стр. %3$d/%4$d - Показывать информационную панель в режиме чтения - Архив комиксов - Вы можете удалить исходный файл из хранилища, чтобы сэкономить место на нём - Режим инкогнито - Синхронизация - Синхронизируйте ваши данные - Введите электронную почту, чтобы продолжить - Ваша манга будет показана здесь - Аккаунт уже существует - Вы можете создавать закладки во время чтения манги - Подтверждение выхода - Отменено - Очистить всю историю - Последние 2 часа - Вы уверены, что хотите удалить выбранные категории избранного\? + Удалить + Как-то здесь пусто… + Попробуйте переформулировать запрос. + Вы можете использовать категории для организации своих избранных. Нажмите «+», чтобы создать категорию + То, что вы прочитаете, будет отображено здесь + Найдите, что почитать, в боковом меню. + Сохраните что-нибудь + Сохраните что-нибудь из онлайн-каталога или импортируйте из файла. + Полка + Недавнее + Анимация листания + Папка для загрузок + Недоступно + Нет доступного хранилища + Другое хранилище + Готово + Всё избранное + Категория пуста + Прочитать позже + Обновления + Новые главы из того, что вы читаете, будут показаны здесь + Результаты поиска + Похожие + Новая версия: %s + Размер: %s + Ожидание подключения… + Очистить ленту обновлений + Очищено + Повернуть экран + Обновить + Обновление скоро начнётся + Следить за обновлениями + Не проверять + Введите пароль + Неверный пароль + Защитить приложение + Запрашивать пароль при запуске Kotatsu + Повторите пароль + Пароли не совпадают + О программе + Версия %s + Проверить обновления + Проверка обновления… + Не удалось проверить обновления + Нет доступных обновлений + Справа налево + Создать категорию + Масштабирование + Вписать в экран + Подогнать по высоте + Подогнать по ширине + Исходный размер + Чёрная + Потребляет меньше энергии на экранах AMOLED + Резервное копирование и восстановление + Создать резервную копию + Восстановить данные + Восстановлено + Подготовка… + Файл не найден + Все данные были восстановлены + Данные были восстановлены, но возникли некоторые ошибки + Вы можете создать резервную копию избранного и истории и потом восстановить их + Только что + Вчера + Давно + Группировать + Сегодня + Попробовать ещё раз + Выбранный режим будет сохранён для текущей манги + Без звука + Необходимо пройти CAPTCHA + Пройти + Очистить куки + Все файлы cookie были удалены + Проверка новых глав: %1$d из %2$d + Очистить ленту + Удалить всю историю обновлений навсегда\? + Проверка новых глав + В обратном порядке + Войти + Авторизуйтесь, чтобы просмотреть этот контент + По умолчанию: %s + …и ещё %1$d + Далее + Введите пароль для запуска приложения + Подтвердить + Пароль должен состоять из 4 символов или более + Поиск только по %s + Другие + Добро пожаловать + Удалить все последние поисковые запросы навсегда\? + Резервная копия сохранена + Некоторые устройства имеют различное поведение системы, что может привести к нарушению фоновых задач. + Подробнее + В очереди + Нет активных загрузок + Глава отсутствует + Скачайте или прочитайте эту недостающую главу онлайн. + Помочь с переводом приложения + Перевод + Тема на 4PDA + Обратная связь + Авторизация выполнена + Вход в %s не поддерживается + Вы выйдете из всех источников + Жанры + Завершено + Онгоинг + Формат даты + По умолчанию + Исключить NSFW мангу из истории + Вы должны ввести имя + Показывать номера страницы + Включенные источники + Доступные источники + Динамическая тема + Применяет тему приложения, основанную на цветовой палитре обоев на устройстве + Политика скриншотов + Разрешить + Запретить для NSFW + Всегда блокировать + Рекомендации + Включить рекомендации + Предлагать мангу на основе Ваших предпочтений + Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы + Начните читать мангу, чтобы получать персональные предложения + Не предлагать NSFW мангу + Включено + Выключено + Не удалось загрузить список жанров + Вычисление… + Создать проблему на GitHub + Импорт манги: %1$d из %2$d + Сбросить фильтр + Поиск по жанрам + Выберите языки, на которых Вы хоите читать мангу. Это можно будет изменить позже в настройках. + Никогда + Только по Wi-Fi + Всегда + Предварительная загрузка страниц + Вы авторизованы как %s + 18+ + Разные языки + Найти главу + В этой манге нет глав + Оформление + Контент + Обновление рекомендаций + Исключить жанры + Укажите жанры, которые Вы не хотите видеть в рекомендациях + Удалить выбранную мангу с накопителя? + Удаление завершено + Загрузить выбранную мангу со всеми главами? Это может привести к большому расходу трафика и места на накопителе + Загружать параллельно + Замедление загрузки + Помогает избежать блокировки IP-адреса + Обработка сохранённой манги + Главы будут удалены в фоновом режиме. Это может занять какое-то время + Скрыть + Доступны новые источники манги + Проверять новые главы и уведомлять о них + Вы будете получать уведомления об обновлении манги, которую Вы читаете + Вы не будете получать уведомления, но новые главы будут отображаться в списке + Включить уведомления + Название + Изменить + Изменить категорию + Отслеживание + Нет категорий избранного + Добавить закладку + Удалить закладку + Закладки + Закладка удалена + Закладка добавлена + Отменить + Удалено из истории + DNS поверх HTTPS + Режим по умолчанию + Автоопределение режима чтения + Автоматически определяет, является ли манга веб-комиксом + Отключить оптимизацию батареи + Помогает с фоновой проверкой обновлений + Что-то пошло не так. Пожалуйста, отправьте отчёт разработчикам, чтобы помочь всё исправить. + Отправить + Отключить все + Использовать отпечаток пальца, если доступно + Манга из Вашего избранного + Манга, которую Вы недавно читали + Читаю + Запланировано + Отложено + Заброшено + Завершено + Показать процент прочитанного в истории и избранном + Манга, помеченная как NSFW, никогда не будет добавлена в историю и ваш прогресс чтения не будет сохранен + %1$s%% + Отчёт + Выйти + Перечитываю + Показать индикаторы прогресса чтения + Удаление данных + Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы + Показать все + Неверное доменное имя + Найдите, что почитать во разделе «Обзор» + Назад + Выбрать диапазон + История очищена + Настроить + Закладок пока нет + Закладки удалены + Нет источников манги + Включите источники манги для чтения онлайн + Случайная + Упорядочить + Пусто + Обзор + Инструменты + Нажмите Назад ещё раз, чтобы выйти + Нажмите Назад 2 раза для выхода из приложения + Другой кэш + Введите электронную почту, чтобы продолжить + Опции + Контент не найден или был удален + Выкл. + %s с + Гл. %1$d/%2$d Стр. %3$d/%4$d + Показывать информационную панель в режиме чтения + Архив комиксов + Вы можете удалить исходный файл из хранилища, чтобы сэкономить место на нём + Режим инкогнито + Синхронизация + Синхронизируйте ваши данные + Введите электронную почту, чтобы продолжить + Ваша манга будет показана здесь + Аккаунт уже существует + Вы можете создавать закладки во время чтения манги + Подтверждение выхода + Отменено + Очистить всю историю + Последние 2 часа + Вы уверены, что хотите удалить выбранные категории избранного\? \nВся манга в них будет потеряна и это не может быть отменено. - Кэш страниц - Список изменений - %s - %s - Использование хранилища - Доступно - Сохранённая манга - Удалено из избранного - Удалено из \"%s\" - Доступно обновление приложения: %s - Нет глав - Автоматическое листание - Папка с изображениями - Импорт манги - Импорт завершён - Импорт скоро начнётся - + Кэш страниц + Список изменений + %s - %s + Использование хранилища + Доступно + Сохранённая манга + Удалено из избранного + Удалено из \"%s\" + Доступно обновление приложения: %s + Нет глав + Автоматическое листание + Папка с изображениями + Импорт манги + Импорт завершён + Импорт скоро начнётся + Лента + Загрузка манги + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1f2932beb..f4d237306 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -320,4 +320,57 @@ Показати всі Виберіть діапазон Вміст не знайдено або видалено + Ваша манга буде відображатися тут + Знайдіть, що почитати у розділі «Огляд» + Назад + Введіть свою електронну пошту, щоб продовжити + Історія очищена + Керувати + Закладок ще немає + Ви можете створювати закладки під час читання манґи + Закладки видалено + Немає джерел манґи + Увімкніть джерела манґи, щоб читати онлайн + Випадкова + Ви впевнені, що бажаєте видалити вибрані улюблені категорії\? +\nУсю манґу в них буде втрачено, і це неможливо скасувати. + Впорядкувати + Порожньо + Журнал змін + Огляд + Збережена манґа + Кеш сторінок + Використання сховища + Доступні + Видалено з \"%s\" + Параметри + Завантаження манґи + Режим інкогніто + Доступне оновлення програми: %s + Немає розділів + Автоматична прокрутка + Викл. + Показувати інформаційну панель у режимі читання + Архів коміксів + Папка із зображеннями + Імпорт манґи + Імпорт завершено + Ви можете видалити оригінальний файл зі сховища, щоб заощадити місце + Імпорт почнеться незабаром + Стрічка + Розд. %1$d/%2$d Стор. %3$d/%4$d + %s с + Обліковий запис уже існує + Синхронізуйте ваші дані + Скасовано + Натисніть Назад ще раз, щоб вийти + Синхронізація + Очистити всю історію + Останні 2 години + Двічі натисніть Назад, щоб вийти з програми + Підтвердження виходу + Введіть електронну пошту, щоб продовжити + Видалено з уподобань + Інший кеш + %s - %s \ No newline at end of file diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 4e9592508..88f15044d 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -6,7 +6,7 @@ https://twitter.com/kotatsuapp https://reddit.com/user/kotatsuapp https://hosted.weblate.org/engage/kotatsu - kotatsu@waifu.club + http://86.57.183.214:8082/report org.kotatsu.sync http://86.57.183.214:8081 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a9459a31a..f68a30c84 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -35,6 +35,8 @@ 4dp 0dp + 16dp + 24dp 8dp 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd57ab681..940613e5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -376,5 +376,10 @@ You can delete the original file from storage to save space Import will start soon Feed + Error details:<br><tt>%1$s</tt><br><br>1. Try to <a href="%2$s">open manga in a web browser</a> to ensure it is available on its source<br>2. If it is available, send an error report to the developers. + Show recent manga shortcuts + Make recent manga available by long pressing on application icon + Tap on the right edge or pressing the right key always switches to the next page + Ergonomic reader control Color filter diff --git a/app/src/main/res/xml/pref_about.xml b/app/src/main/res/xml/pref_about.xml index ab078ac09..a4e72c089 100644 --- a/app/src/main/res/xml/pref_about.xml +++ b/app/src/main/res/xml/pref_about.xml @@ -9,12 +9,6 @@ android:persistent="false" android:summary="@string/check_for_updates" /> - - + + - \ No newline at end of file + diff --git a/app/src/main/res/xml/pref_reader.xml b/app/src/main/res/xml/pref_reader.xml index 9d82c1cf3..dbbe245e0 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" /> + +