Improve branch selection

pull/373/head
Koitharu 3 years ago
parent e69964d1f5
commit 6d0cd49db3
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 543 versionCode 544
versionName '5.1-b2' versionName '5.1'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -78,7 +78,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:96b9ac36f3') { implementation('com.github.KotatsuApp:kotatsu-parsers:cae7073f87') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
@ -86,7 +86,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.0' implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.activity:activity-ktx:1.7.1' implementation 'androidx.activity:activity-ktx:1.7.1'
implementation 'androidx.fragment:fragment-ktx:1.5.7' implementation 'androidx.fragment:fragment-ktx:1.5.7'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
@ -122,8 +122,8 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.45' implementation 'com.google.dagger:hilt-android:2.46.1'
kapt 'com.google.dagger:hilt-compiler:2.45' kapt 'com.google.dagger:hilt-compiler:2.46.1'
implementation 'androidx.hilt:hilt-work:1.0.0' implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.0.0'
@ -152,6 +152,6 @@ dependencies {
androidTestImplementation 'androidx.room:room-testing:2.5.1' androidTestImplementation 'androidx.room:room-testing:2.5.1'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0' androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.45' androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.45' kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1'
} }

@ -0,0 +1,93 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.content.DialogInterface
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
import com.hannesdorfmann.adapterdelegates4.AdapterDelegatesManager
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import org.koitharu.kotatsu.R
class RecyclerViewAlertDialog private constructor(
private val delegate: AlertDialog
) : DialogInterface by delegate {
fun show() = delegate.show()
class Builder<T>(context: Context) {
private val recyclerView = RecyclerView(context)
private val delegatesManager = AdapterDelegatesManager<List<T>>()
private var items: List<T>? = null
private val delegate = MaterialAlertDialogBuilder(context)
.setView(recyclerView)
init {
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.updatePadding(
top = context.resources.getDimensionPixelOffset(R.dimen.list_spacing),
)
recyclerView.clipToPadding = false
}
fun setTitle(@StringRes titleResId: Int): Builder<T> {
delegate.setTitle(titleResId)
return this
}
fun setTitle(title: CharSequence): Builder<T> {
delegate.setTitle(title)
return this
}
fun setIcon(@DrawableRes iconId: Int): Builder<T> {
delegate.setIcon(iconId)
return this
}
fun setPositiveButton(
@StringRes textId: Int,
listener: DialogInterface.OnClickListener,
): Builder<T> {
delegate.setPositiveButton(textId, listener)
return this
}
fun setNegativeButton(
@StringRes textId: Int,
listener: DialogInterface.OnClickListener? = null
): Builder<T> {
delegate.setNegativeButton(textId, listener)
return this
}
fun setCancelable(isCancelable: Boolean): Builder<T> {
delegate.setCancelable(isCancelable)
return this
}
fun addAdapterDelegate(subject: AdapterDelegate<List<T>>): Builder<T> {
delegatesManager.addDelegate(subject)
return this
}
fun setItems(list: List<T>): Builder<T> {
items = list
return this
}
fun create(): RecyclerViewAlertDialog {
recyclerView.adapter = ListDelegationAdapter(delegatesManager).also {
it.items = items
}
return RecyclerViewAlertDialog(delegate.create())
}
}
}

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.base.ui.list
import android.view.View import android.view.View
interface OnListItemClickListener<I> { fun interface OnListItemClickListener<I> {
fun onItemClick(item: I, view: View) fun onItemClick(item: I, view: View)

@ -3,8 +3,8 @@ package org.koitharu.kotatsu.core.model
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.utils.ext.iterator import org.koitharu.kotatsu.utils.ext.iterator
fun Collection<Manga>.ids() = mapToSet { it.id } fun Collection<Manga>.ids() = mapToSet { it.id }
@ -35,15 +35,22 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
} }
} }
val groups = ch.groupBy { it.branch } val groups = ch.groupBy { it.branch }
if (groups.size == 1) {
return groups.keys.first()
}
val candidates = HashMap<String?, List<MangaChapter>>(groups.size)
for (locale in LocaleListCompat.getAdjustedDefault()) { for (locale in LocaleListCompat.getAdjustedDefault()) {
var language = locale.getDisplayLanguage(locale).toTitleCase(locale) val displayLanguage = locale.getDisplayLanguage(locale)
if (groups.containsKey(language)) { val displayName = locale.getDisplayName(locale)
return language for (branch in groups.keys) {
} if (branch != null && (
language = locale.getDisplayName(locale).toTitleCase(locale) branch.contains(displayLanguage, ignoreCase = true) ||
if (groups.containsKey(language)) { branch.contains(displayName, ignoreCase = true)
return language )
) {
candidates[branch] = groups[branch] ?: continue
}
} }
} }
return groups.maxByOrNull { it.value.size }?.key return candidates.ifEmpty { groups }.maxByOrNull { it.value.size }?.key
} }

@ -19,10 +19,13 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import okio.Closeable
import okio.buffer
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import java.net.HttpURLConnection import java.net.HttpURLConnection
@ -54,7 +57,9 @@ class FaviconFetcher(
val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" } val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" }
val response = loadIcon(icon.url, mangaSource) val response = loadIcon(icon.url, mangaSource)
val responseBody = response.requireBody() val responseBody = response.requireBody()
val source = writeToDiskCache(responseBody)?.toImageSource() ?: responseBody.toImageSource() val source = writeToDiskCache(responseBody)?.toImageSource()?.also {
response.closeQuietly()
} ?: responseBody.toImageSource(response)
return SourceResult( return SourceResult(
source = source, source = source,
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type), mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type),
@ -71,7 +76,7 @@ class FaviconFetcher(
options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) } options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) }
val response = okHttpClient.newCall(request.build()).await() val response = okHttpClient.newCall(request.build()).await()
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) { if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
response.body?.closeQuietly() response.closeQuietly()
throw HttpException(response) throw HttpException(response)
} }
return response return response
@ -116,8 +121,12 @@ class FaviconFetcher(
return ImageSource(data, fileSystem, diskCacheKey, this) return ImageSource(data, fileSystem, diskCacheKey, this)
} }
private fun ResponseBody.toImageSource(): ImageSource { private fun ResponseBody.toImageSource(response: Closeable): ImageSource {
return ImageSource(source(), options.context, FaviconMetadata(mangaSource)) return ImageSource(
source().withExtraCloseable(response).buffer(),
options.context,
FaviconMetadata(mangaSource),
)
} }
private fun Response.toDataSource(): DataSource { private fun Response.toDataSource(): DataSource {

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.details.domain package org.koitharu.kotatsu.details.domain
class BranchComparator : Comparator<String?> { import org.koitharu.kotatsu.details.ui.model.MangaBranch
override fun compare(o1: String?, o2: String?): Int = compareValues(o1, o2) class BranchComparator : Comparator<MangaBranch> {
override fun compare(o1: MangaBranch, o2: MangaBranch): Int = compareValues(o1.name, o2.name)
} }

@ -1,12 +1,12 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.transition.Slide import android.transition.Slide
import android.transition.TransitionManager import android.transition.TransitionManager
import android.view.Gravity import android.view.Gravity
import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -19,31 +19,31 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.dialog.RecyclerViewAlertDialog
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.adapter.branchAD
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.utils.ViewBadge import org.koitharu.kotatsu.utils.ViewBadge
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.utils.ext.textAndVisible import org.koitharu.kotatsu.utils.ext.textAndVisible
import javax.inject.Inject import javax.inject.Inject
@ -53,7 +53,9 @@ class DetailsActivity :
BaseActivity<ActivityDetailsBinding>(), BaseActivity<ActivityDetailsBinding>(),
View.OnClickListener, View.OnClickListener,
BottomSheetHeaderBar.OnExpansionChangeListener, BottomSheetHeaderBar.OnExpansionChangeListener,
NoModalBottomSheetOwner, View.OnLongClickListener, PopupMenu.OnMenuItemClickListener { NoModalBottomSheetOwner,
View.OnLongClickListener,
PopupMenu.OnMenuItemClickListener {
override val bsHeader: BottomSheetHeaderBar? override val bsHeader: BottomSheetHeaderBar?
get() = binding.headerChapters get() = binding.headerChapters
@ -254,18 +256,19 @@ class DetailsActivity :
} }
private fun showBranchPopupMenu() { private fun showBranchPopupMenu() {
val menu = PopupMenu(this, binding.headerChapters ?: binding.buttonDropdown) var dialog: DialogInterface? = null
val currentBranch = viewModel.selectedBranchValue val listener = OnListItemClickListener<MangaBranch> { item, _ ->
for (branch in viewModel.branches.value ?: return) { viewModel.setSelectedBranch(item.name)
val item = menu.menu.add(R.id.group_branches, Menu.NONE, Menu.NONE, branch) dialog?.dismiss()
item.isChecked = branch == currentBranch
} }
menu.menu.setGroupCheckable(R.id.group_branches, true, true) dialog = RecyclerViewAlertDialog.Builder<MangaBranch>(this)
menu.setOnMenuItemClickListener { item -> .addAdapterDelegate(branchAD(listener))
viewModel.setSelectedBranch(item.title?.toString()) .setCancelable(true)
true .setNegativeButton(android.R.string.cancel, null)
} .setTitle(R.string.translations)
menu.show() .setItems(viewModel.branches.value.orEmpty())
.create()
.also { it.show() }
} }
private fun openReader(isIncognitoMode: Boolean) { private fun openReader(isIncognitoMode: Boolean) {

@ -16,6 +16,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@ -124,18 +125,20 @@ class DetailsMenuProvider(
return true return true
} }
private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List<String?>) { private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List<MangaBranch>) {
val dialogBuilder = MaterialAlertDialogBuilder(activity) val dialogBuilder = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.save_manga) .setTitle(R.string.save_manga)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
if (branches.size > 1) { if (branches.size > 1) {
val items = Array(branches.size) { i -> branches[i].orEmpty() } val items = Array(branches.size) { i -> branches[i].name.orEmpty() }
val currentBranch = viewModel.selectedBranchIndex.value ?: -1 val currentBranch = branches.indexOfFirst { it.isSelected }
val checkedIndices = BooleanArray(branches.size) { i -> i == currentBranch } val checkedIndices = BooleanArray(branches.size) { i -> i == currentBranch }
dialogBuilder.setMultiChoiceItems(items, checkedIndices) { _, i, checked -> dialogBuilder.setMultiChoiceItems(items, checkedIndices) { _, i, checked ->
checkedIndices[i] = checked checkedIndices[i] = checked
}.setPositiveButton(R.string.save) { _, _ -> }.setPositiveButton(R.string.save) { _, _ ->
val selectedBranches = branches.filterIndexedTo(HashSet()) { i, _ -> checkedIndices[i] } val selectedBranches = branches.mapIndexedNotNullTo(HashSet()) { i, b ->
if (checkedIndices[i]) b.name else null
}
val chaptersIds = manga.chapters?.mapNotNullToSet { c -> val chaptersIds = manga.chapters?.mapNotNullToSet { c ->
if (c.branch in selectedBranches) c.id else null if (c.branch in selectedBranches) c.id else null
} }

@ -36,6 +36,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.domain.BranchComparator import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
@ -44,7 +45,6 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
@ -158,17 +158,15 @@ class DetailsViewModel @Inject constructor(
scrobblingInfo.filterNotNull() scrobblingInfo.filterNotNull()
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val branches: LiveData<List<String?>> = delegate.manga.map { val branches: LiveData<List<MangaBranch>> = combine(
val chapters = it?.chapters ?: return@map emptyList() delegate.manga,
chapters.mapToSet { x -> x.branch }.sortedWith(BranchComparator())
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val selectedBranchIndex = combine(
branches.asFlow(),
delegate.selectedBranch, delegate.selectedBranch,
) { branches, selected -> ) { m, b ->
branches.indexOf(selected) val chapters = m?.chapters ?: return@combine emptyList()
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, -1) chapters.groupBy { x -> x.branch }
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
.sortedWith(BranchComparator())
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val selectedBranchName = delegate.selectedBranch val selectedBranchName = delegate.selectedBranch
.asFlowLiveData(viewModelScope.coroutineContext, null) .asFlowLiveData(viewModelScope.coroutineContext, null)

@ -0,0 +1,39 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.graphics.Color
import android.text.Spannable
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import androidx.core.text.buildSpannedString
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.utils.ext.getThemeColor
fun branchAD(
clickListener: OnListItemClickListener<MangaBranch>,
) = adapterDelegateViewBinding<MangaBranch, MangaBranch, ItemCheckableNewBinding>(
{ inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) },
) {
val clickAdapter = AdapterDelegateClickListenerAdapter(this, clickListener)
itemView.setOnClickListener(clickAdapter)
val counterColorSpan = ForegroundColorSpan(context.getThemeColor(android.R.attr.textColorSecondary, Color.LTGRAY))
val counterSizeSpan = RelativeSizeSpan(0.86f)
bind {
binding.root.text = buildSpannedString {
append(item.name ?: getString(R.string.system_default))
append(' ')
append(' ')
val start = length
append(item.count.toString())
setSpan(counterColorSpan, start, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(counterSizeSpan, start, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
binding.root.isChecked = item.isSelected
}
}

@ -1,45 +1,16 @@
package org.koitharu.kotatsu.details.ui.adapter package org.koitharu.kotatsu.details.ui.adapter
import android.view.LayoutInflater import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import android.view.View import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import android.view.ViewGroup import org.koitharu.kotatsu.details.ui.model.MangaBranch
import android.widget.BaseAdapter
import android.widget.TextView class BranchesAdapter(
import org.koitharu.kotatsu.R list: List<MangaBranch>,
import org.koitharu.kotatsu.parsers.util.replaceWith listener: OnListItemClickListener<MangaBranch>,
) : ListDelegationAdapter<List<MangaBranch>>() {
class BranchesAdapter : BaseAdapter() {
init {
private val dataSet = ArrayList<String?>() delegatesManager.addDelegate(branchAD(listener))
items = list
override fun getCount(): Int {
return dataSet.size
}
override fun getItem(position: Int): Any? {
return dataSet[position]
}
override fun getItemId(position: Int): Long {
return dataSet[position].hashCode().toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_branch, parent, false)
(view as TextView).text = dataSet[position]
return view
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_branch_dropdown, parent, false)
(view as TextView).text = dataSet[position]
return view
}
fun setItems(items: Collection<String?>) {
dataSet.replaceWith(items)
notifyDataSetChanged()
} }
} }

@ -0,0 +1,32 @@
package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.list.ui.model.ListModel
class MangaBranch(
val name: String?,
val count: Int,
val isSelected: Boolean,
) : ListModel {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaBranch
if (name != other.name) return false
if (count != other.count) return false
return isSelected == other.isSelected
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + count
result = 31 * result + isSelected.hashCode()
return result
}
override fun toString(): String {
return "$name: $count"
}
}

@ -9,7 +9,6 @@ import android.net.Uri
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
@ -71,6 +70,7 @@ class ImportWorker @AssistedInject constructor(
.setPriority(NotificationCompat.PRIORITY_MIN) .setPriority(NotificationCompat.PRIORITY_MIN)
.setDefaults(0) .setDefaults(0)
.setSilent(true) .setSilent(true)
.setOngoing(true)
.setProgress(0, 0, true) .setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)

@ -50,6 +50,7 @@ import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.IdlingDetector import org.koitharu.kotatsu.utils.IdlingDetector
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.hasGlobalPoint import org.koitharu.kotatsu.utils.ext.hasGlobalPoint
import org.koitharu.kotatsu.utils.ext.isRtl
import org.koitharu.kotatsu.utils.ext.observeWithPrevious import org.koitharu.kotatsu.utils.ext.observeWithPrevious
import org.koitharu.kotatsu.utils.ext.postDelayed import org.koitharu.kotatsu.utils.ext.postDelayed
import org.koitharu.kotatsu.utils.ext.setValueRounded import org.koitharu.kotatsu.utils.ext.setValueRounded
@ -156,6 +157,7 @@ class ReaderActivity :
if (binding.appbarTop.isVisible) { if (binding.appbarTop.isVisible) {
lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1)) lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1))
} }
binding.slider.isRtl = mode == ReaderMode.REVERSED
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

@ -4,7 +4,7 @@ import androidx.annotation.IdRes
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit import androidx.fragment.app.commit
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.reader.ui.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
@ -15,7 +15,7 @@ class ReaderManager(
@IdRes private val containerResId: Int, @IdRes private val containerResId: Int,
) { ) {
private val modeMap = EnumMap<ReaderMode, Class<out BaseReader<*>>>(ReaderMode::class.java) private val modeMap = EnumMap<ReaderMode, Class<out BaseReaderFragment<*>>>(ReaderMode::class.java)
init { init {
modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java
@ -23,8 +23,8 @@ class ReaderManager(
modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
} }
val currentReader: BaseReader<*>? val currentReader: BaseReaderFragment<*>?
get() = fragmentManager.findFragmentById(containerResId) as? BaseReader<*> get() = fragmentManager.findFragmentById(containerResId) as? BaseReaderFragment<*>
val currentMode: ReaderMode? val currentMode: ReaderMode?
get() { get() {
@ -40,7 +40,7 @@ class ReaderManager(
} }
} }
fun replace(reader: BaseReader<*>) { fun replace(reader: BaseReaderFragment<*>) {
fragmentManager.commit { fragmentManager.commit {
setReorderingAllowed(true) setReorderingAllowed(true)
replace(containerResId, reader) replace(containerResId, reader)

@ -11,7 +11,6 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
@ -148,8 +147,8 @@ class ReaderConfigBottomSheet :
} }
} }
override fun onActivityResult(uri: Uri?) { override fun onActivityResult(result: Uri?) {
viewModel.onActivityResult(uri) viewModel.onActivityResult(result)
dismissAllowingStateLoss() dismissAllowingStateLoss()
} }
@ -157,7 +156,6 @@ class ReaderConfigBottomSheet :
val helper = ScreenOrientationHelper(requireActivity()) val helper = ScreenOrientationHelper(requireActivity())
orientationHelper = helper orientationHelper = helper
helper.observeAutoOrientation() helper.observeAutoOrientation()
.flowWithLifecycle(lifecycle)
.onEach { .onEach {
binding.buttonScreenRotate.isGone = it binding.buttonScreenRotate.isGone = it
}.launchIn(viewLifecycleScope) }.launchIn(viewLifecycleScope)

@ -12,7 +12,7 @@ import org.koitharu.kotatsu.utils.ext.getParcelableCompat
private const val KEY_STATE = "state" private const val KEY_STATE = "state"
abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() { abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
protected val viewModel by activityViewModels<ReaderViewModel>() protected val viewModel by activityViewModels<ReaderViewModel>()
private var stateToSave: ReaderState? = null private var stateToSave: ReaderState? = null

@ -13,8 +13,8 @@ import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import org.koitharu.kotatsu.utils.ext.doOnPageChanged import org.koitharu.kotatsu.utils.ext.doOnPageChanged
@ -26,7 +26,7 @@ import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@AndroidEntryPoint @AndroidEntryPoint
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() { class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() {
@Inject @Inject
lateinit var networkState: NetworkState lateinit var networkState: NetworkState
@ -70,6 +70,7 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
override fun onDestroyView() { override fun onDestroyView() {
pagerAdapter = null pagerAdapter = null
binding.pager.adapter = null
super.onDestroyView() super.onDestroyView()
} }

@ -13,8 +13,8 @@ import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.doOnPageChanged import org.koitharu.kotatsu.utils.ext.doOnPageChanged
import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled
@ -25,7 +25,7 @@ import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@AndroidEntryPoint @AndroidEntryPoint
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() { class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() {
@Inject @Inject
lateinit var networkState: NetworkState lateinit var networkState: NetworkState
@ -69,6 +69,7 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
override fun onDestroyView() { override fun onDestroyView() {
pagesAdapter = null pagesAdapter = null
binding.pager.adapter = null
super.onDestroyView() super.onDestroyView()
} }

@ -12,8 +12,8 @@ import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
@ -22,7 +22,7 @@ import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() { class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>() {
@Inject @Inject
lateinit var networkState: NetworkState lateinit var networkState: NetworkState
@ -60,6 +60,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
override fun onDestroyView() { override fun onDestroyView() {
webtoonAdapter = null webtoonAdapter = null
binding.recyclerView.adapter = null
super.onDestroyView() super.onDestroyView()
} }

@ -28,6 +28,7 @@ import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
@ -100,6 +101,7 @@ class SuggestionsWorker @AssistedInject constructor(
.setPriority(NotificationCompat.PRIORITY_MIN) .setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(NotificationCompat.CATEGORY_SERVICE) .setCategory(NotificationCompat.CATEGORY_SERVICE)
.setDefaults(0) .setDefaults(0)
.setOngoing(true)
.setSilent(true) .setSilent(true)
.setProgress(0, 0, true) .setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync) .setSmallIcon(android.R.drawable.stat_notify_sync)
@ -145,15 +147,27 @@ class SuggestionsWorker @AssistedInject constructor(
.take(MAX_RESULTS) .take(MAX_RESULTS)
suggestionRepository.replace(suggestions) suggestionRepository.replace(suggestions)
if (appSettings.isSuggestionsNotificationAvailable) { if (appSettings.isSuggestionsNotificationAvailable) {
runCatchingCancellable { for (i in 0..3) {
val manga = suggestions[Random.nextInt(0, suggestions.size / 3)] try {
val details = mangaRepositoryFactory.create(manga.manga.source) val manga = suggestions[Random.nextInt(0, suggestions.size / 3)]
.getDetails(manga.manga) val details = mangaRepositoryFactory.create(manga.manga.source)
if (details !in tagsBlacklist) { .getDetails(manga.manga)
if (details.rating > 0 && details.rating < RATING_MIN) {
continue
}
if (details.isNsfw && appSettings.isSuggestionsExcludeNsfw) {
continue
}
if (details in tagsBlacklist) {
continue
}
showNotification(details) showNotification(details)
break
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
e.printStackTraceDebug()
} }
}.onFailure {
it.printStackTraceDebug()
} }
} }
return suggestions.size return suggestions.size
@ -178,7 +192,8 @@ class SuggestionsWorker @AssistedInject constructor(
if (blacklist.isNotEmpty()) { if (blacklist.isNotEmpty()) {
list.removeAll { manga -> manga in blacklist } list.removeAll { manga -> manga in blacklist }
} }
list list.shuffle()
list.take(MAX_SOURCE_RESULTS)
}.onFailure { }.onFailure {
it.printStackTraceDebug() it.printStackTraceDebug()
}.getOrDefault(emptyList()) }.getOrDefault(emptyList())
@ -296,8 +311,10 @@ class SuggestionsWorker @AssistedInject constructor(
private const val MANGA_CHANNEL_ID = "suggestions" private const val MANGA_CHANNEL_ID = "suggestions"
private const val WORKER_NOTIFICATION_ID = 36 private const val WORKER_NOTIFICATION_ID = 36
private const val MAX_RESULTS = 80 private const val MAX_RESULTS = 80
private const val MAX_SOURCE_RESULTS = 14
private const val MAX_RAW_RESULTS = 200 private const val MAX_RAW_RESULTS = 200
private const val TAG_EQ_THRESHOLD = 0.4f private const val TAG_EQ_THRESHOLD = 0.4f
private const val RATING_MIN = 0.5f
private val preferredSortOrders = listOf( private val preferredSortOrders = listOf(
SortOrder.UPDATED, SortOrder.UPDATED,

@ -219,6 +219,7 @@ class TrackWorker @AssistedInject constructor(
.setPriority(NotificationCompat.PRIORITY_MIN) .setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(NotificationCompat.CATEGORY_SERVICE) .setCategory(NotificationCompat.CATEGORY_SERVICE)
.setDefaults(0) .setDefaults(0)
.setOngoing(true)
.setSilent(true) .setSilent(true)
.setProgress(0, 0, true) .setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync) .setSmallIcon(android.R.drawable.stat_notify_sync)

@ -1,22 +0,0 @@
package org.koitharu.kotatsu.utils
import android.graphics.drawable.Drawable
import androidx.preference.Preference
import coil.target.Target
class PreferenceIconTarget(
private val preference: Preference,
) : Target {
override fun onError(error: Drawable?) {
preference.icon = error
}
override fun onStart(placeholder: Drawable?) {
preference.icon = placeholder
}
override fun onSuccess(result: Drawable) {
preference.icon = result
}
}

@ -34,18 +34,21 @@ fun Fragment.addMenuProvider(provider: MenuProvider) {
} }
@MainThread @MainThread
suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner = suspendCancellableCoroutine { cont -> suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner {
val liveData = viewLifecycleOwnerLiveData val liveData = viewLifecycleOwnerLiveData
val observer = object : Observer<LifecycleOwner?> { liveData.value?.let { return it }
override fun onChanged(value: LifecycleOwner?) { return suspendCancellableCoroutine { cont ->
if (value != null) { val observer = object : Observer<LifecycleOwner?> {
liveData.removeObserver(this) override fun onChanged(value: LifecycleOwner?) {
cont.resume(value) if (value != null) {
liveData.removeObserver(this)
cont.resume(value)
}
} }
} }
} liveData.observeForever(observer)
liveData.observeForever(observer) cont.invokeOnCancellation {
cont.invokeOnCancellation { liveData.removeObserver(observer)
liveData.removeObserver(observer) }
} }
} }

@ -4,9 +4,9 @@ import android.view.View
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
fun Insets.end(view: View): Int { fun Insets.end(view: View): Int {
return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) left else right return if (view.isRtl) left else right
} }
fun Insets.start(view: View): Int { fun Insets.start(view: View): Int {
return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) right else left return if (view.isRtl) right else left
} }

@ -54,7 +54,9 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is IOException -> getDisplayMessage(message, resources) ?: localizedMessage is IOException -> getDisplayMessage(message, resources) ?: localizedMessage
else -> localizedMessage else -> localizedMessage
} ?: resources.getString(R.string.error_occurred) }.ifNullOrEmpty {
resources.getString(R.string.error_occurred)
}
private fun getDisplayMessage(msg: String?, resources: Resources): String? = when { private fun getDisplayMessage(msg: String?, resources: Resources): String? = when {
msg.isNullOrEmpty() -> null msg.isNullOrEmpty() -> null

@ -200,3 +200,9 @@ fun <V> V.setChecked(checked: Boolean, animate: Boolean) where V : View, V : Che
jumpDrawablesToCurrentState() jumpDrawablesToCurrentState()
} }
} }
var View.isRtl: Boolean
get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
set(value) {
layoutDirection = if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
}

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/spinnerItemStyle"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge" />

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="?android:attr/spinnerDropDownItemStyle"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:drawableEnd="?android:listChoiceIndicatorSingle"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge"
tools:text="Scanlator" />

@ -414,4 +414,5 @@
<string name="downloads_removed">Downloads have been removed</string> <string name="downloads_removed">Downloads have been removed</string>
<string name="downloads_cancelled">Downloads have been cancelled</string> <string name="downloads_cancelled">Downloads have been cancelled</string>
<string name="suggestions_enable_prompt">Do you want to receive personalized manga suggestions?</string> <string name="suggestions_enable_prompt">Do you want to receive personalized manga suggestions?</string>
<string name="translations">Translations</string>
</resources> </resources>

@ -6,7 +6,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.0.1' classpath 'com.android.tools.build:gradle:8.0.1'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.45' classpath 'com.google.dagger:hilt-android-gradle-plugin:2.46.1'
} }
} }

Loading…
Cancel
Save