Compare commits

...

14 Commits

Author SHA1 Message Date
Koitharu
4d838d290d Update dependencies 2022-10-03 08:41:32 +03:00
Koitharu
048efdf59f Fix crash on slider 2022-10-03 08:07:25 +03:00
Koitharu
af2adeba13 Fix opening fingerprint dialog 2022-10-01 12:19:36 +03:00
Koitharu
93c6bec452 Fix widgets colors 2022-10-01 10:34:25 +03:00
Koitharu
c944044465 Update version 2022-09-22 17:38:08 +03:00
Koitharu
8a63ca2310 Fix coroutines cancellation 2022-09-22 17:33:40 +03:00
Koitharu
12e5e3b35e Update gitignore 2022-09-22 16:53:27 +03:00
Zakhar Timoshenko
553a85ef86 Widget theme fix #225 2022-09-22 16:49:38 +03:00
Koitharu
de7012cabf Change acra sender to http 2022-09-15 08:36:20 +03:00
Koitharu
46f0d3ef74 Fix onboarding dialog 2022-09-14 12:34:33 +03:00
Koitharu
c27c785ac2 Use Coil for empty states images 2022-09-14 12:30:07 +03:00
Koitharu
4186c36f30 Prevent GoneOnInvisibleListener leak 2022-09-12 15:45:34 +03:00
Koitharu
757e33dfb4 Fix unwanted errors reporting 2022-09-12 14:32:42 +03:00
Koitharu
ab9bdf9f07 Fixes 2022-09-11 09:11:35 +03:00
56 changed files with 384 additions and 215 deletions

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/kotlinScripting.xml
/.idea/kotlinc.xml
/.idea/deploymentTargetDropDown.xml
/.idea/androidTestResultsUserPreferences.xml
/.idea/render.experimental.xml

3
.idea/kotlinc.xml generated
View File

@@ -3,4 +3,7 @@
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.6.21" />
</component>
</project>

View File

@@ -14,8 +14,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 32
versionCode 426
versionName '3.4.14'
versionCode 429
versionName '3.4.17'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -28,6 +28,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 {
@@ -79,7 +81,7 @@ afterEvaluate {
}
}
dependencies {
implementation('com.github.KotatsuApp:kotatsu-parsers:b3a9c5fcda') {
implementation('com.github.KotatsuApp:kotatsu-parsers:b1990c7918') {
exclude group: 'org.json', module: 'json'
}
@@ -87,7 +89,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.2'
implementation 'androidx.fragment:fragment-ktx:1.5.3'
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'
@@ -115,11 +117,11 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'io.insert-koin:koin-android:3.2.0'
implementation 'io.coil-kt:coil-base:2.2.0'
implementation 'io.coil-kt:coil-base:2.2.1'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'ch.acra:acra-mail:5.9.6'
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'
@@ -138,5 +140,5 @@ dependencies {
androidTestImplementation 'io.insert-koin:koin-test-junit4:3.2.0'
androidTestImplementation 'androidx.room:room-testing:2.4.3'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
}

View File

@@ -8,9 +8,10 @@ import androidx.fragment.app.strictmode.FragmentStrictMode
import androidx.room.InvalidationTracker
import org.acra.ReportField
import org.acra.config.dialog
import org.acra.config.mailSender
import org.acra.config.httpSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.koin.android.ext.android.get
import org.koin.android.ext.android.getKoin
import org.koin.android.ext.koin.androidContext
@@ -73,7 +74,7 @@ class KotatsuApp : Application() {
appWidgetModule,
suggestionsModule,
shikimoriModule,
bookmarksModule
bookmarksModule,
)
}
}
@@ -82,16 +83,25 @@ class KotatsuApp : Application() {
super.attachBaseContext(base)
initAcra {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.KEY_VALUE_LIST
reportFormat = StringFormat.JSON
excludeMatchingSharedPreferencesKeys = listOf(
"sources_\\w+",
)
httpSender {
uri = getString(R.string.url_error_report)
basicAuthLogin = getString(R.string.acra_login)
basicAuthPassword = getString(R.string.acra_password)
httpMethod = HttpSender.Method.POST
}
reportContent = listOf(
ReportField.PACKAGE_NAME,
ReportField.APP_VERSION_CODE,
ReportField.APP_VERSION_NAME,
ReportField.ANDROID_VERSION,
ReportField.PHONE_MODEL,
ReportField.CRASH_CONFIGURATION,
ReportField.STACK_TRACE,
ReportField.SHARED_PREFERENCES
ReportField.CRASH_CONFIGURATION,
ReportField.SHARED_PREFERENCES,
)
dialog {
text = getString(R.string.crash_text)
@@ -100,11 +110,6 @@ class KotatsuApp : Application() {
resIcon = R.drawable.ic_alert_outline
resTheme = android.R.style.Theme_Material_Light_Dialog_Alert
}
mailSender {
mailTo = getString(R.string.email_error_report)
reportAsFile = true
reportFileName = "stacktrace.txt"
}
}
}
@@ -129,7 +134,7 @@ class KotatsuApp : Application() {
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
@@ -138,7 +143,7 @@ class KotatsuApp : Application() {
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.penaltyLog()
.build()
.build(),
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()

View File

@@ -1,8 +1,12 @@
package org.koitharu.kotatsu.base.domain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
fun interface ReversibleHandle {
@@ -10,7 +14,13 @@ fun interface ReversibleHandle {
}
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
reverse()
runCatchingCancellable {
withContext(NonCancellable) {
reverse()
}
}.onFailure {
it.printStackTraceDebug()
}
}
operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle {

View File

@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
private const val PAGE_SIZE = 10
@@ -84,7 +85,7 @@ class BackupRepository(private val db: MangaDatabase) {
JsonDeserializer(it).toTagEntity()
}
val history = JsonDeserializer(item).toHistoryEntity()
result += runCatching {
result += runCatchingCancellable {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
@@ -99,7 +100,7 @@ class BackupRepository(private val db: MangaDatabase) {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val category = JsonDeserializer(item).toFavouriteCategoryEntity()
result += runCatching {
result += runCatchingCancellable {
db.favouriteCategoriesDao.upsert(category)
}
}
@@ -115,7 +116,7 @@ class BackupRepository(private val db: MangaDatabase) {
JsonDeserializer(it).toTagEntity()
}
val favourite = JsonDeserializer(item).toFavouriteEntity()
result += runCatching {
result += runCatchingCancellable {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)

View File

@@ -6,6 +6,7 @@ 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
@@ -25,6 +26,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireBitmap
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class ShortcutsUpdater(
private val context: Context,
@@ -37,10 +39,12 @@ class ShortcutsUpdater(
private var shortcutsUpdateJob: Job? = null
override fun onInvalidated(tables: MutableSet<String>) {
val prevJob = shortcutsUpdateJob
shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) {
prevJob?.join()
updateShortcutsImpl()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val prevJob = shortcutsUpdateJob
shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) {
prevJob?.join()
updateShortcutsImpl()
}
}
}
@@ -48,7 +52,7 @@ class ShortcutsUpdater(
return ShortcutManagerCompat.requestPinShortcut(
context,
buildShortcutInfo(manga).build(),
null
null,
)
}
@@ -57,7 +61,8 @@ class ShortcutsUpdater(
return shortcutsUpdateJob?.join() != null
}
private suspend fun updateShortcutsImpl() = runCatching {
@RequiresApi(Build.VERSION_CODES.N_MR1)
private suspend fun updateShortcutsImpl() = runCatchingCancellable {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
.filter { x -> x.title.isNotEmpty() }
@@ -68,17 +73,17 @@ class ShortcutsUpdater(
}
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
val icon = runCatching {
val icon = runCatchingCancellable {
val bmp = coil.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.size(iconSize.width, iconSize.height)
.build()
.build(),
).requireBitmap()
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
}.fold(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) }
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
)
mangaRepository.storeManga(manga)
return ShortcutInfoCompat.Builder(context, manga.id.toString())
@@ -87,7 +92,7 @@ class ShortcutsUpdater(
.setIcon(icon)
.setIntent(
ReaderActivity.newIntent(context, manga.id)
.setAction(ReaderActivity.ACTION_MANGA_READ)
.setAction(ReaderActivity.ACTION_MANGA_READ),
)
}

View File

@@ -12,6 +12,7 @@ import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import com.google.android.material.snackbar.Snackbar
import kotlin.math.roundToInt
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -29,7 +30,6 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
import org.koitharu.kotatsu.utils.ext.addMenuProvider
import kotlin.math.roundToInt
class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(),
@@ -46,7 +46,7 @@ class ChaptersFragment :
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
container: ViewGroup?,
) = FragmentChaptersBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -98,7 +98,7 @@ class ChaptersFragment :
manga = viewModel.manga.value ?: return,
state = ReaderState(item.chapter.id, 0, 0),
),
options.toBundle()
options.toBundle(),
)
}
@@ -128,7 +128,7 @@ class ChaptersFragment :
Snackbar.make(
binding.recyclerViewChapters,
R.string.chapters_will_removed_background,
Snackbar.LENGTH_LONG
Snackbar.LENGTH_LONG,
).show()
}
}
@@ -271,8 +271,8 @@ class ChaptersFragment :
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false
menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_search)?.isVisible = viewModel.isChaptersEmpty.value == false
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {

View File

@@ -38,6 +38,7 @@ 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.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class DetailsViewModel(
intent: MangaIntent,
@@ -165,7 +166,7 @@ class DetailsViewModel(
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
val original = localMangaRepository.getRemoteManga(manga)
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
runCatching {
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
onMangaRemoved.postCall(manga)
@@ -204,7 +205,7 @@ class DetailsViewModel(
reload()
} else {
viewModelScope.launch(Dispatchers.Default) {
runCatching {
runCatchingCancellable {
localMangaRepository.getDetails(downloadedManga)
}.onSuccess {
delegate.relatedManga.value = it

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class MangaDetailsDelegate(
private val intent: MangaIntent,
@@ -44,9 +45,9 @@ class MangaDetailsDelegate(
val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist)
mangaData.value = manga
relatedManga.value = runCatching {
relatedManga.value = runCatchingCancellable {
if (manga.source == MangaSource.LOCAL) {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
MangaRepository(m.source).getDetails(m)
} else {
localMangaRepository.findSavedManga(manga)

View File

@@ -6,10 +6,18 @@ import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import java.io.File
import kotlinx.coroutines.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.internal.closeQuietly
@@ -28,6 +36,7 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
private const val MAX_FAILSAFE_ATTEMPTS = 2
@@ -150,7 +159,7 @@ class DownloadManager(
}
outState.value = DownloadState.PostProcessing(startId, data, cover)
output.mergeWithExisting()
output.finalize()
output.finish()
val localManga = localMangaRepository.getFromFile(output.file)
outState.value = DownloadState.Done(startId, data, cover, localManga)
} catch (e: CancellationException) {
@@ -226,7 +235,7 @@ class DownloadManager(
)
}
private suspend fun loadCover(manga: Manga) = runCatching {
private suspend fun loadCover(manga: Manga) = runCatchingCancellable {
imageLoader.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)

View File

@@ -22,6 +22,7 @@ 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.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class FavouritesListViewModel(
private val categoryId: Long,
@@ -45,7 +46,7 @@ class FavouritesListViewModel(
} else {
repository.observeAll(categoryId)
},
createListModeFlow()
createListModeFlow(),
) { list, mode ->
when {
list.isEmpty() -> listOf(
@@ -58,8 +59,9 @@ class FavouritesListViewModel(
R.string.favourites_category_empty
},
actionStringRes = 0,
)
),
)
else -> list.toUi(mode, this)
}
}.catch {

View File

@@ -5,9 +5,9 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.google.android.material.R as materialR
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import com.google.android.material.R as materialR
class HistoryListMenuProvider(
private val context: Context,
@@ -38,6 +38,6 @@ class HistoryListMenuProvider(
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_history_grouping).isChecked = viewModel.isGroupingEnabled.value == true
menu.findItem(R.id.action_history_grouping)?.isChecked = viewModel.isGroupingEnabled.value == true
}
}

View File

@@ -1,13 +1,16 @@
@file:SuppressLint("UnsafeOptInUsageError")
package org.koitharu.kotatsu.list.ui.adapter
import android.annotation.SuppressLint
import android.view.View
import androidx.annotation.CheckResult
import androidx.core.view.doOnNextLayout
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import org.koitharu.kotatsu.R
@CheckResult
fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? {
return if (counter > 0) {
val badgeDrawable = badge ?: initBadge(this)

View File

@@ -1,23 +1,31 @@
package org.koitharu.kotatsu.list.ui.adapter
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun emptyStateListAD(
coil: ImageLoader,
listener: MangaListListener,
) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>(
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) }
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) },
) {
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
bind {
binding.icon.setImageResource(item.icon)
binding.icon.newImageRequest(item.icon)?.enqueueWith(coil)
binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
}
onViewRecycled {
binding.icon.disposeImageRequest()
}
}

View File

@@ -4,9 +4,9 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.*
import kotlin.jvm.internal.Intrinsics
class MangaListAdapter(
coil: ImageLoader,
@@ -24,7 +24,7 @@ class MangaListAdapter(
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
.addDelegate(ITEM_TYPE_FILTER, currentFilterAD(listener))
.addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(listener))

View File

@@ -46,7 +46,7 @@ fun mangaListDetailedItemAD(
}
binding.textViewRating.textAndVisible = item.rating
binding.textViewTags.text = item.tags
itemView.bindBadge(badge, item.counter)
badge = itemView.bindBadge(badge, item.counter)
}
onViewRecycled {

View File

@@ -40,7 +40,7 @@ fun mangaListItemAD(
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
itemView.bindBadge(badge, item.counter)
badge = itemView.bindBadge(badge, item.counter)
}
onViewRecycled {

View File

@@ -6,15 +6,22 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.text.Collator
import java.util.*
import java.util.Locale
import java.util.TreeSet
class FilterCoordinator(
private val repository: RemoteMangaRepository,
@@ -152,7 +159,7 @@ class FilterCoordinator(
}
private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
runCatching {
runCatchingCancellable {
repository.getTags()
}.onFailure { error ->
error.printStackTraceDebug()
@@ -203,4 +210,4 @@ class FilterCoordinator(
return collator?.compare(t1, t2) ?: compareValues(t1, t2)
}
}
}
}

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.local.domain
import androidx.annotation.WorkerThread
import java.io.File
import java.util.zip.ZipFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.Closeable
@@ -11,8 +13,6 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.readText
import java.io.File
import java.util.zip.ZipFile
class CbzMangaOutput(
val file: File,
@@ -62,7 +62,7 @@ class CbzMangaOutput(
index.addChapter(chapter)
}
suspend fun finalize() {
suspend fun finish() {
runInterruptible(Dispatchers.IO) {
output.put(ENTRY_NAME_INDEX, index.toString())
output.finish()
@@ -89,7 +89,7 @@ class CbzMangaOutput(
otherIndex = MangaIndex(
zip.getInputStream(entry).use {
it.reader().readText()
}
},
)
} else {
output.copyEntryFrom(zip, entry)

View File

@@ -8,19 +8,31 @@ import androidx.collection.ArraySet
import androidx.core.net.toFile
import androidx.core.net.toUri
import java.io.File
import java.io.IOException
import java.util.*
import java.util.Enumeration
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import okio.IOException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.TempFileFilter
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.toCamelCase
import org.koitharu.kotatsu.utils.AlphanumComparator
import org.koitharu.kotatsu.utils.CompositeMutex
@@ -28,6 +40,7 @@ import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.readText
import org.koitharu.kotatsu.utils.ext.resolveName
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
private const val MAX_PARALLELISM = 4
@@ -70,6 +83,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)) {
"Manga is not local or saved"
}
else -> getFromFile(Uri.parse(manga.url).toFile())
}
@@ -226,7 +240,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
context: CoroutineContext,
): Deferred<Manga?> = async(context) {
runInterruptible {
runCatching { getFromFile(file) }.getOrNull()
runCatchingCancellable { getFromFile(file) }.getOrNull()
}
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.ui
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
@@ -10,6 +11,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okio.IOException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.ui.service.DownloadService
@@ -21,8 +23,8 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.progress.Progress
import java.io.IOException
class LocalListViewModel(
private val repository: LocalMangaRepository,
@@ -40,7 +42,7 @@ class LocalListViewModel(
override val content = combine(
mangaList,
createListModeFlow(),
listError
listError,
) { list, mode, error ->
when {
error != null -> listOf(error.toErrorState(canRetry = true))
@@ -51,8 +53,9 @@ class LocalListViewModel(
textPrimary = R.string.text_local_holder_primary,
textSecondary = R.string.text_local_holder_secondary,
actionStringRes = R.string._import,
)
),
)
else -> ArrayList<ListModel>(list.size + 1).apply {
add(headerModel)
list.toUi(this, mode)
@@ -60,7 +63,7 @@ class LocalListViewModel(
}
}.asLiveDataDistinct(
viewModelScope.coroutineContext + Dispatchers.Default,
listOf(LoadingState)
listOf(LoadingState),
)
init {
@@ -97,7 +100,7 @@ class LocalListViewModel(
for (manga in itemsToRemove) {
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file")
runCatching {
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
mangaList.update { list ->
@@ -113,6 +116,8 @@ class LocalListViewModel(
try {
listError.value = null
mangaList.value = repository.getList(0, null, null)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
listError.value = e
}
@@ -121,7 +126,7 @@ class LocalListViewModel(
private fun cleanup() {
if (!DownloadService.isRunning) {
viewModelScope.launch {
runCatching {
runCatchingCancellable {
repository.cleanup()
}.onFailure { error ->
error.printStackTraceDebug()

View File

@@ -12,19 +12,24 @@ import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.*
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.transition.TransitionManager
import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -59,7 +64,6 @@ import org.koitharu.kotatsu.tracker.ui.FeedFragment
import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.VoiceInputContract
import org.koitharu.kotatsu.utils.ext.*
import com.google.android.material.R as materialR
private const val TAG_PRIMARY = "primary"
private const val TAG_SEARCH = "search"
@@ -94,10 +98,10 @@ class MainActivity :
it,
binding.toolbar,
R.string.open_menu,
R.string.close_menu
R.string.close_menu,
).apply {
setHomeAsUpIndicator(
ContextCompat.getDrawable(this@MainActivity, materialR.drawable.abc_ic_ab_back_material)
ContextCompat.getDrawable(this@MainActivity, materialR.drawable.abc_ic_ab_back_material),
)
setToolbarNavigationClickListener {
binding.searchView.hideKeyboard()
@@ -429,7 +433,12 @@ class MainActivity :
}
private fun onFirstStart() {
lifecycleScope.launchWhenResumed {
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
val settings = get<AppSettings>()
when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
}
val isUpdateSupported = withContext(Dispatchers.Default) {
TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext)
@@ -438,11 +447,6 @@ class MainActivity :
if (isUpdateSupported) {
AppUpdateChecker(this@MainActivity).checkIfNeeded()
}
val settings = get<AppSettings>()
when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
}
}
}
@@ -469,7 +473,7 @@ class MainActivity :
val drawer = drawer ?: return
val isLocked = actionModeDelegate.isActionModeStarted || isSearchOpened
drawer.setDrawerLockMode(
if (isLocked) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED
if (isLocked) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED,
)
drawerToggle?.isDrawerIndicatorEnabled = !isLocked
}

View File

@@ -46,7 +46,10 @@ class ProtectActivity :
startActivity(intent)
finishAfterTransition()
}
}
override fun onStart() {
super.onStart()
if (!useFingerprint()) {
binding.editPassword.requestFocus()
}

View File

@@ -213,7 +213,7 @@ class ReaderActivity :
val resolveTextId = ExceptionResolver.getResolveStringId(e)
if (resolveTextId != 0) {
dialog.setPositiveButton(resolveTextId, listener)
} else {
} else if (e.isReportable()) {
dialog.setPositiveButton(R.string.report, listener)
}
dialog.show()

View File

@@ -6,6 +6,7 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import java.util.Date
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.R
@@ -31,7 +32,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import java.util.*
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
private const val BOUNDS_PAGE_OFFSET = 2
private const val PAGES_TRIM_THRESHOLD = 120
@@ -69,7 +70,7 @@ class ReaderViewModel(
mangaName = manga?.title,
chapterName = chapter?.name,
chapterNumber = chapter?.number ?: 0,
chaptersTotal = chapters.size()
chaptersTotal = chapters.size(),
)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null)
@@ -80,7 +81,7 @@ class ReaderViewModel(
val readerAnimation = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
key = AppSettings.KEY_READER_ANIMATION,
valueProducer = { readerAnimation }
valueProducer = { readerAnimation },
)
val isScreenshotsBlockEnabled = combine(
@@ -123,12 +124,12 @@ class ReaderViewModel(
val manga = checkNotNull(mangaData.value)
dataRepository.savePreferences(
manga = manga,
mode = newMode
mode = newMode,
)
readerMode.value = newMode
content.value?.run {
content.value = copy(
state = getCurrentState()
state = getCurrentState(),
)
}
}
@@ -358,7 +359,7 @@ class ReaderViewModel(
?: manga.chapters?.randomOrNull()
?: error("There are no chapters in this manga")
val pages = repo.getPages(chapter)
return runCatching {
return runCatchingCancellable {
val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages)
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
}.onSuccess {
@@ -389,7 +390,7 @@ class ReaderViewModel(
*/
private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job {
return processLifecycleScope.launch(Dispatchers.Default) {
runCatching {
runCatchingCancellable {
addOrUpdate(
manga = manga,
chapterId = state.chapterId,

View File

@@ -13,7 +13,7 @@ abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B,
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
exceptionResolver: ExceptionResolver,
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
@Suppress("LeakingThis")
@@ -37,6 +37,14 @@ abstract class BasePageHolder<B : ViewBinding>(
protected abstract fun onBind(data: ReaderPage)
@CallSuper
open fun onAttachedToWindow() {
}
@CallSuper
open fun onDetachedFromWindow() {
}
@CallSuper
open fun onRecycled() {
delegate.onRecycle()

View File

@@ -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<H : BasePageHolder<*>>(
@@ -35,6 +35,16 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
super.onViewRecycled(holder)
}
override fun onViewAttachedToWindow(holder: H) {
super.onViewAttachedToWindow(holder)
holder.onAttachedToWindow()
}
override fun onViewDetachedFromWindow(holder: H) {
super.onViewDetachedFromWindow(holder)
holder.onDetachedFromWindow()
}
open fun getItem(position: Int): ReaderPage = differ.currentList[position]
open fun getItemOrNull(position: Int) = differ.currentList.getOrNull(position)
@@ -45,7 +55,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
final override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
viewType: Int,
): H = onCreateViewHolder(parent, loader, settings, exceptionResolver)
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont ->
@@ -58,7 +68,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
parent: ViewGroup,
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
exceptionResolver: ExceptionResolver,
): H
private class DiffCallback : DiffUtil.ItemCallback<ReaderPage>() {
@@ -70,6 +80,5 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
override fun areContentsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean {
return oldItem == newItem
}
}
}

View File

@@ -3,24 +3,30 @@ package org.koitharu.kotatsu.reader.ui.pager
import android.net.Uri
import androidx.core.net.toUri
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import java.io.File
import java.io.IOException
class PageHolderDelegate(
private val loader: PageLoader,
private val settings: AppSettings,
private val callback: Callback,
private val exceptionResolver: ExceptionResolver
private val exceptionResolver: ExceptionResolver,
) : SubsamplingScaleImageView.DefaultOnImageEventListener() {
private val scope = loader.loaderScope + Dispatchers.Main.immediate
@@ -88,6 +94,8 @@ class PageHolderDelegate(
loader.convertInPlace(file)
state = State.CONVERTED
callback.onImageReady(file.toUri())
} catch (ce: CancellationException) {
throw ce
} catch (e2: Throwable) {
e.addSuppressed(e2)
state = State.ERROR

View File

@@ -25,11 +25,11 @@ class WebtoonHolder(
View.OnClickListener {
private var scrollToRestore = 0
private val goneOnInvisibleListener = GoneOnInvisibleListener(bindingInfo.progressBar)
init {
binding.ssiv.setOnImageEventListener(delegate)
bindingInfo.buttonRetry.setOnClickListener(this)
GoneOnInvisibleListener(bindingInfo.progressBar).attach()
}
override fun onBind(data: ReaderPage) {
@@ -41,6 +41,16 @@ class WebtoonHolder(
binding.ssiv.recycle()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
goneOnInvisibleListener.attach()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
goneOnInvisibleListener.detach()
}
override fun onLoadingStarted() {
bindingInfo.layoutError.isVisible = false
bindingInfo.progressBar.showCompat()

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
fun pageThumbnailAD(
@@ -69,7 +70,7 @@ fun pageThumbnailAD(
text = (item.number).toString()
}
job = scope.launch {
val drawable = runCatching {
val drawable = runCatchingCancellable {
loadPageThumbnail(item)
}.getOrNull()
binding.imageViewThumb.setImageDrawable(drawable)

View File

@@ -2,10 +2,16 @@ package org.koitharu.kotatsu.remotelist.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
@@ -16,7 +22,14 @@ import org.koitharu.kotatsu.list.ui.filter.FilterCoordinator
import org.koitharu.kotatsu.list.ui.filter.FilterItem
import org.koitharu.kotatsu.list.ui.filter.FilterState
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.list.ui.model.CurrentFilterModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@@ -132,6 +145,8 @@ class RemoteListViewModel(
mangaList.value = mangaList.value?.plus(list) ?: list
}
hasNextPage.value = list.isNotEmpty()
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
e.printStackTraceDebug()
listError.value = e

View File

@@ -3,15 +3,20 @@ package org.koitharu.kotatsu.scrobbling.domain
import androidx.collection.LongSparseArray
import androidx.collection.getOrElse
import androidx.core.text.parseAsHtml
import java.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
import org.koitharu.kotatsu.scrobbling.domain.model.*
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.utils.ext.findKeyByValue
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.util.EnumMap
abstract class Scrobbler(
protected val db: MangaDatabase,
@@ -47,7 +52,7 @@ abstract class Scrobbler(
private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? {
val mangaInfo = infoCache.getOrElse(targetId) {
runCatching {
runCatchingCancellable {
getMangaInfo(targetId)
}.onFailure {
it.printStackTraceDebug()
@@ -72,9 +77,9 @@ abstract class Scrobbler(
}
suspend fun Scrobbler.tryScrobble(mangaId: Long, chapter: MangaChapter): Boolean {
return runCatching {
return runCatchingCancellable {
scrobble(mangaId, chapter)
}.onFailure {
it.printStackTraceDebug()
}.isSuccess
}
}

View File

@@ -19,6 +19,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class MangaSearchRepository(
private val settings: AppSettings,
@@ -30,7 +31,7 @@ class MangaSearchRepository(
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
settings.getMangaSources(includeHidden = false).asFlow()
.flatMapMerge(concurrency) { source ->
runCatching {
runCatchingCancellable {
MangaRepository(source).getList(
offset = 0,
query = query,
@@ -63,7 +64,7 @@ class MangaSearchRepository(
SUGGESTION_PROJECTION,
"${SearchManager.SUGGEST_COLUMN_QUERY} LIKE ?",
arrayOf("%$query%"),
"date DESC"
"date DESC",
)?.use { cursor ->
val count = minOf(cursor.count, limit)
if (count == 0) {
@@ -113,7 +114,7 @@ class MangaSearchRepository(
SUGGESTION_PROJECTION,
null,
arrayOfNulls(1),
null
null,
)?.use { cursor -> cursor.count } ?: 0
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.search.ui
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
@@ -9,14 +10,20 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
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.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class SearchViewModel(
private val repository: MangaRepository,
private val query: String,
settings: AppSettings
settings: AppSettings,
) : MangaListViewModel(settings) {
private val mangaList = MutableStateFlow<List<Manga>?>(null)
@@ -28,7 +35,7 @@ class SearchViewModel(
mangaList,
createListModeFlow(),
listError,
hasNextPage
hasNextPage,
) { list, mode, error, hasNext ->
when {
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
@@ -39,8 +46,9 @@ class SearchViewModel(
textPrimary = R.string.nothing_found,
textSecondary = R.string.text_search_holder_secondary,
actionStringRes = 0,
)
),
)
else -> {
val result = ArrayList<ListModel>(list.size + 1)
list.toUi(result, mode)
@@ -88,6 +96,8 @@ class SearchViewModel(
mangaList.value = mangaList.value?.plus(list) ?: list
}
hasNextPage.value = list.isNotEmpty()
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
listError.value = e
}

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
private const val MAX_PARALLELISM = 4
private const val MIN_HAS_MORE_ITEMS = 8
@@ -48,8 +49,9 @@ class MultiSearchViewModel(
textSecondary = R.string.text_search_holder_secondary,
actionStringRes = 0,
)
}
},
)
loading -> list + LoadingFooter
else -> list
}
@@ -81,6 +83,8 @@ class MultiSearchViewModel(
loadingData.value = true
query.postValue(q)
searchImpl(q)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
listError.value = e
} finally {
@@ -94,7 +98,7 @@ class MultiSearchViewModel(
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
val deferredList = sources.map { source ->
async(dispatcher) {
runCatching {
runCatchingCancellable {
val list = MangaRepository(source).getList(offset = 0, query = q)
.toUi(ListMode.GRID)
if (list.isNotEmpty()) {

View File

@@ -5,12 +5,12 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.list.ui.adapter.*
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel
import kotlin.jvm.internal.Intrinsics
class MultiSearchAdapter(
lifecycleOwner: LifecycleOwner,
@@ -33,11 +33,11 @@ class MultiSearchAdapter(
selectionDecoration = selectionDecoration,
listener = listener,
itemClickListener = itemClickListener,
)
),
)
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
.addDelegate(emptyStateListAD(listener))
.addDelegate(emptyStateListAD(coil, listener))
.addDelegate(errorStateListAD(listener))
}

View File

@@ -8,6 +8,12 @@ import androidx.activity.ComponentActivity
import androidx.annotation.MainThread
import androidx.core.net.toUri
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.koin.android.ext.android.get
@@ -20,12 +26,7 @@ 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
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 org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class AppUpdateChecker(private val activity: ComponentActivity) {
@@ -41,7 +42,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
null
}
suspend fun checkNow() = runCatching {
suspend fun checkNow() = runCatchingCancellable {
val version = repo.getLatestVersion()
val newVersionId = VersionId(version.name)
val currentVersionId = VersionId(BuildConfig.VERSION_NAME)

View File

@@ -7,6 +7,7 @@ import android.view.View
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
@@ -65,18 +66,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
clearCache(preference, CacheDir.PAGES)
true
}
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
clearCache(preference, CacheDir.THUMBS)
true
}
AppSettings.KEY_COOKIES_CLEAR -> {
clearCookies()
true
}
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
clearSearchHistory(preference)
true
}
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
viewLifecycleScope.launch {
trackerRepo.clearLogs()
@@ -85,11 +90,12 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
Snackbar.make(
view ?: return@launch,
R.string.updates_feed_cleared,
Snackbar.LENGTH_SHORT
Snackbar.LENGTH_SHORT,
).show()
}
true
}
AppSettings.KEY_SHIKIMORI -> {
if (!shikimoriRepository.isAuthorized) {
launchShikimoriAuth()
@@ -98,6 +104,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
super.onPreferenceTreeClick(preference)
}
}
else -> super.onPreferenceTreeClick(preference)
}
}
@@ -110,6 +117,8 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
storageManager.clearCache(cache)
val size = storageManager.computeCacheSize(cache)
preference.summary = FileSize.BYTES.format(ctx, size)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
preference.summary = e.getDisplayMessage(ctx.resources)
} finally {
@@ -136,7 +145,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
Snackbar.make(
view ?: return@launch,
R.string.search_history_cleared,
Snackbar.LENGTH_SHORT
Snackbar.LENGTH_SHORT,
).show()
}
}.show()
@@ -154,7 +163,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
Snackbar.make(
listView ?: return@launch,
R.string.cookies_cleared,
Snackbar.LENGTH_SHORT
Snackbar.LENGTH_SHORT,
).show()
}
}.show()

View File

@@ -18,7 +18,13 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.awaitViewLifecycle
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.serializableArgument
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
class SourceSettingsFragment : BasePreferenceFragment(0) {
@@ -60,12 +66,13 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
startActivity(SourceAuthActivity.newIntent(preference.context, source))
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch {
runCatching {
runCatchingCancellable {
preference.summary = null
withContext(Dispatchers.Default) {
requireNotNull(repository?.getAuthProvider()?.getUsername())
@@ -85,6 +92,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
.show()
}
else -> preference.summary = error.getDisplayMessage(preference.context.resources)
}
error.printStackTraceDebug()

View File

@@ -9,14 +9,14 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
import java.io.FileOutputStream
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.databinding.DialogProgressBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.progress.Progress
import java.io.File
import java.io.FileOutputStream
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
@@ -24,7 +24,7 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
private var backup: File? = null
private val saveFileContract = registerForActivityResult(
ActivityResultContracts.CreateDocument("*/*")
ActivityResultContracts.CreateDocument("*/*"),
) { uri ->
val file = backup
if (uri != null && file != null) {
@@ -88,6 +88,8 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
}
Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_LONG).show()
dismiss()
} catch (e: InterruptedException) {
throw e
} catch (e: Exception) {
onError(e)
}

View File

@@ -41,7 +41,7 @@ class OnboardDialogFragment :
override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
builder
.setPositiveButton(R.string.done, this)
.setCancelable(true)
.setCancelable(false)
if (isWelcome) {
builder.setTitle(R.string.welcome)
} else {

View File

@@ -8,6 +8,8 @@ import androidx.annotation.FloatRange
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import java.util.concurrent.TimeUnit
import kotlin.math.pow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@@ -24,8 +26,6 @@ import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import org.koitharu.kotatsu.utils.ext.asArrayList
import org.koitharu.kotatsu.utils.ext.trySetForeground
import java.util.concurrent.TimeUnit
import kotlin.math.pow
class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params), KoinComponent {
@@ -47,7 +47,7 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
val channel = NotificationChannel(
WORKER_CHANNEL_ID,
title,
NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_LOW,
)
channel.setShowBadge(false)
channel.enableVibration(false)
@@ -118,7 +118,7 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
}.map { manga ->
MangaSuggestion(
manga = manga,
relevance = computeRelevance(manga.tags, allTags)
relevance = computeRelevance(manga.tags, allTags),
)
}.sortedBy { it.relevance }.take(LIMIT)
suggestionRepository.replace(suggestions)

View File

@@ -4,11 +4,11 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.adapter.*
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
import kotlin.jvm.internal.Intrinsics
class FeedAdapter(
coil: ImageLoader,
@@ -23,7 +23,7 @@ class FeedAdapter(
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
.addDelegate(ITEM_TYPE_DATE_HEADER, relatedDateItemAD())
}

View File

@@ -27,6 +27,7 @@ import org.koitharu.kotatsu.tracker.domain.Tracker
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import org.koitharu.kotatsu.utils.ext.trySetForeground
@@ -80,7 +81,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
val deferredList = coroutineScope {
tracks.map { (track, channelId) ->
async(dispatcher) {
runCatching {
runCatchingCancellable {
tracker.fetchUpdates(track, commit = true)
}.onSuccess { updates ->
if (updates.isValid && updates.isNotEmpty()) {

View File

@@ -19,5 +19,10 @@ class GoneOnInvisibleListener(
fun attach() {
view.viewTreeObserver.addOnGlobalLayoutListener(this)
onGlobalLayout()
}
}
fun detach() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}

View File

@@ -1,50 +0,0 @@
package org.koitharu.kotatsu.utils
import androidx.annotation.MainThread
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Runnable
class PausingDispatcher(
private val dispatcher: CoroutineDispatcher,
) : CoroutineDispatcher() {
@Volatile
private var isPaused = false
private val queue = ConcurrentLinkedQueue<Task>()
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
return isPaused || super.isDispatchNeeded(context)
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
if (isPaused) {
queue.add(Task(context, block))
} else {
dispatcher.dispatch(context, block)
}
}
@MainThread
fun pause() {
isPaused = true
}
@MainThread
fun resume() {
if (!isPaused) {
return
}
isPaused = false
while (true) {
val task = queue.poll() ?: break
dispatcher.dispatch(task.context, task.block)
}
}
private class Task(
val context: CoroutineContext,
val block: Runnable,
)
}

View File

@@ -29,7 +29,7 @@ val Context.activityManager: ActivityManager?
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatching {
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {
val info = getForegroundInfo()
setForeground(info)
}.isSuccess

View File

@@ -2,11 +2,18 @@ package org.koitharu.kotatsu.utils.ext
import android.content.ActivityNotFoundException
import android.content.res.Resources
import androidx.collection.arraySetOf
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import kotlinx.coroutines.CancellationException
import okio.FileNotFoundException
import org.acra.ktx.sendWithAcra
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.*
import org.koitharu.kotatsu.core.exceptions.CaughtException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException
import org.koitharu.kotatsu.parsers.exception.NotFoundException
@@ -18,26 +25,47 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is ActivityNotFoundException,
is UnsupportedOperationException,
-> resources.getString(R.string.operation_not_supported)
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is FileNotFoundException -> resources.getString(R.string.file_not_found)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
is ContentUnavailableException -> message
is ParseException -> shortMessage
is SocketTimeoutException -> resources.getString(R.string.network_error)
is UnknownHostException,
is SocketTimeoutException,
-> resources.getString(R.string.network_error)
is WrongPasswordException -> resources.getString(R.string.wrong_password)
is NotFoundException -> resources.getString(R.string.not_found_404)
else -> localizedMessage
} ?: resources.getString(R.string.error_occurred)
fun Throwable.isReportable(): Boolean {
if (this !is Exception) {
return true
}
return this is ParseException || this is IllegalArgumentException ||
this is IllegalStateException || this.javaClass == RuntimeException::class.java
return this is Error || this.javaClass in reportableExceptions
}
fun Throwable.report(message: String?) {
val exception = CaughtException(this, message)
exception.sendWithAcra()
}
private val reportableExceptions = arraySetOf<Class<*>>(
ParseException::class.java,
RuntimeException::class.java,
IllegalStateException::class.java,
IllegalArgumentException::class.java,
ConcurrentModificationException::class.java,
UnsupportedOperationException::class.java,
)
inline fun <R> runCatchingCancellable(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (e: InterruptedException) {
throw e
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}
}

View File

@@ -114,7 +114,8 @@ fun <T> RecyclerView.ViewHolder.getItem(clazz: Class<T>): T? {
fun Slider.setValueRounded(newValue: Float) {
val step = stepSize
value = (newValue / step).roundToInt() * step
val roundedValue = (newValue / step).roundToInt() * step
value = roundedValue.coerceIn(valueFrom, valueTo)
}
val RecyclerView.isScrolledToTop: Boolean

View File

@@ -23,6 +23,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollIndicators="top|bottom"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_source_locale" />

View File

@@ -6,4 +6,4 @@
android:layout_width="@dimen/widget_cover_width"
android:layout_height="@dimen/widget_cover_height"
android:scaleType="centerCrop"
tools:ignore="ContentDescription" />
tools:ignore="ContentDescription" />

View File

@@ -31,6 +31,7 @@
android:elegantTextHeight="false"
android:ellipsize="end"
android:lines="2"
android:padding="2dp"
android:paddingHorizontal="4dp"
android:paddingBottom="4dp"
android:textColor="?android:attr/textColorPrimary" />

View File

@@ -41,4 +41,10 @@
<item name="android:textColorHighlightInverse">@color/m3_dynamic_highlighted_text</item>
<item name="android:textColorAlertDialogListItem">@color/m3_dynamic_dark_default_color_primary_text</item>
</style>
</resources>
<style name="Theme.Kotatsu.AppWidgetContainer" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="android:colorBackground">@color/m3_sys_color_dynamic_dark_secondary_container</item>
<item name="android:panelColorBackground">@color/m3_sys_color_dynamic_dark_inverse_primary</item>
</style>
</resources>

View File

@@ -43,8 +43,8 @@
</style>
<style name="Theme.Kotatsu.AppWidgetContainer" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="android:colorBackground">@android:color/system_accent1_50</item>
<item name="android:panelColorBackground">@android:color/system_accent1_100</item>
<item name="android:colorBackground">@color/m3_sys_color_dynamic_light_secondary_container</item>
<item name="android:panelColorBackground">@color/m3_sys_color_dynamic_light_inverse_primary</item>
</style>
</resources>
</resources>

View File

@@ -6,7 +6,7 @@
<string name="url_twitter">https://twitter.com/kotatsuapp</string>
<string name="url_reddit">https://reddit.com/user/kotatsuapp</string>
<string name="url_weblate">https://hosted.weblate.org/engage/kotatsu</string>
<string name="email_error_report">kotatsu@waifu.club</string>
<string name="url_error_report" translatable="false">https://acra.rumblur.space/report</string>
<string-array name="values_theme" translatable="false">
<item>-1</item>
<item>1</item>