Update ktlint to 1.3.0

This commit is contained in:
FooIbar committed 2024-06-16 17:48:50 +08:00
1 parent 3946428df6
commit 5a3c165ff1
79 files changed
+707 -979

No files matched your search

+4 -2
View File
@@ -294,13 +294,15 @@ aboutLibraries {
duplicationRule = GROUP
}
val ktlintVersion = libs.ktlint.get().version
spotless {
kotlin {
// https://github.com/diffplug/spotless/issues/111
target("src/**/*.kt")
ktlint()
ktlint(ktlintVersion)
}
kotlinGradle {
ktlint()
ktlint(ktlintVersion)
}
}
@@ -48,11 +48,8 @@ fun <T> Modifier.anchoredHorizontalDraggable(
): Modifier = this then AnchoredHorizontalDraggableElement(
state = state,
canDrag = { (x, y) ->
if (x > 0) {
enableDragFromStartToEnd
} else {
enableDragFromEndToStart
} && x.absoluteValue > y.absoluteValue * 2f
val enableDrag = if (x > 0) enableDragFromStartToEnd else enableDragFromEndToStart
enableDrag && x.absoluteValue > y.absoluteValue * 2f
},
enabled = enabled,
startDragImmediately = startDragImmediately,
@@ -124,9 +121,7 @@ private class AnchoredHorizontalDraggableNode<T>(
private val isReverseDirection: Boolean
get() = requireLayoutDirection() == LayoutDirection.Rtl
override fun canDrag(change: PointerInputChange): Boolean {
return canDrag(change.positionChange().reverseIfNeeded())
}
override fun canDrag(change: PointerInputChange): Boolean = canDrag(change.positionChange().reverseIfNeeded())
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
state.anchoredDrag {
@@ -240,7 +235,8 @@ private class DraggableAnchorsNode<T>(
var state: AnchoredDraggableState<T>,
var anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
var orientation: Orientation,
) : Modifier.Node(), LayoutModifierNode {
) : Modifier.Node(),
LayoutModifierNode {
private var didLookahead: Boolean = false
override fun onDetach() {
@@ -46,7 +46,9 @@ import kotlinx.coroutines.launch
*/
internal abstract class HorizontalDragGestureNode(
enabled: Boolean,
) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
) : DelegatingNode(),
PointerInputModifierNode,
CompositionLocalConsumerModifierNode {
protected var enabled = enabled
private set
@@ -132,65 +134,63 @@ internal abstract class HorizontalDragGestureNode(
pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds)
}
private fun initializePointerInputNode(): SuspendingPointerInputModifierNode {
return SuspendingPointerInputModifierNode {
// re-create tracker when pointer input block restarts. This lazily creates the tracker
// only when it is need.
val velocityTracker = VelocityTracker()
val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
{ startEvent, initialDelta ->
if (!isListeningForEvents) {
if (channel == null) {
channel = Channel(capacity = Channel.UNLIMITED)
}
startListeningForEvents()
private fun initializePointerInputNode(): SuspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
// re-create tracker when pointer input block restarts. This lazily creates the tracker
// only when it is need.
val velocityTracker = VelocityTracker()
val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
{ startEvent, initialDelta ->
if (!isListeningForEvents) {
if (channel == null) {
channel = Channel(capacity = Channel.UNLIMITED)
}
val xSign = sign(startEvent.position.x)
val ySign = sign(startEvent.position.y)
val adjustedStart = startEvent.position -
Offset(initialDelta.x * xSign, initialDelta.y * ySign)
channel?.trySend(DragStarted(adjustedStart))
startListeningForEvents()
}
val xSign = sign(startEvent.position.x)
val ySign = sign(startEvent.position.y)
val adjustedStart = startEvent.position -
Offset(initialDelta.x * xSign, initialDelta.y * ySign)
val onDragEnd: () -> Unit = {
val maximumVelocity = currentValueOf(LocalViewConfiguration)
.maximumFlingVelocity
val velocity = velocityTracker.calculateVelocity(
Velocity(maximumVelocity, maximumVelocity),
channel?.trySend(DragStarted(adjustedStart))
}
val onDragEnd: () -> Unit = {
val maximumVelocity = currentValueOf(LocalViewConfiguration)
.maximumFlingVelocity
val velocity = velocityTracker.calculateVelocity(
Velocity(maximumVelocity, maximumVelocity),
)
velocityTracker.resetTracking()
channel?.trySend(DragStopped(velocity))
}
val onDragCancel: () -> Unit = {
channel?.trySend(DragCancelled)
}
val shouldAwaitTouchSlop: () -> Boolean = {
!startDragImmediately()
}
val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
{ change, delta ->
velocityTracker.addPointerInputChange(change)
channel?.trySend(DragDelta(delta))
}
coroutineScope {
try {
detectHorizontalDragGestures(
canDrag = ::canDrag,
onDragStart = onDragStart,
onDragEnd = onDragEnd,
onDragCancel = onDragCancel,
shouldAwaitTouchSlop = shouldAwaitTouchSlop,
onDrag = onDrag,
)
velocityTracker.resetTracking()
channel?.trySend(DragStopped(velocity))
}
val onDragCancel: () -> Unit = {
} catch (cancellation: CancellationException) {
channel?.trySend(DragCancelled)
}
val shouldAwaitTouchSlop: () -> Boolean = {
!startDragImmediately()
}
val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
{ change, delta ->
velocityTracker.addPointerInputChange(change)
channel?.trySend(DragDelta(delta))
}
coroutineScope {
try {
detectHorizontalDragGestures(
canDrag = ::canDrag,
onDragStart = onDragStart,
onDragEnd = onDragEnd,
onDragCancel = onDragCancel,
shouldAwaitTouchSlop = shouldAwaitTouchSlop,
onDrag = onDrag,
)
} catch (cancellation: CancellationException) {
channel?.trySend(DragCancelled)
if (!isActive) throw cancellation
}
if (!isActive) throw cancellation
}
}
}
@@ -1,4 +1,4 @@
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
@file:Suppress("ktlint:standard:property-naming")
package androidx.compose.material3
@@ -230,10 +230,8 @@ class DrawerState2(
fun rememberDrawerState2(
initialValue: DrawerValue,
confirmStateChange: (DrawerValue) -> Boolean = { true },
): DrawerState2 {
return rememberSaveable(saver = DrawerState2.Saver(confirmStateChange)) {
DrawerState2(initialValue, confirmStateChange)
}
): DrawerState2 = rememberSaveable(saver = DrawerState2.Saver(confirmStateChange)) {
DrawerState2(initialValue, confirmStateChange)
}
@Composable
@@ -912,16 +912,12 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
/**
* Convert screen to source x coordinate.
*/
private fun viewToSourceX(vx: Float): Float {
return if (vTranslate == null) Float.NaN else (vx - vTranslate!!.x) / scale
}
private fun viewToSourceX(vx: Float): Float = if (vTranslate == null) Float.NaN else (vx - vTranslate!!.x) / scale
/**
* Convert screen to source y coordinate.
*/
private fun viewToSourceY(vy: Float): Float {
return if (vTranslate == null) Float.NaN else (vy - vTranslate!!.y) / scale
}
private fun viewToSourceY(vy: Float): Float = if (vTranslate == null) Float.NaN else (vy - vTranslate!!.y) / scale
/**
* Convert screen coordinate to source coordinate.
@@ -929,9 +925,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
* @param vxy view X/Y coordinate.
* @return a coordinate representing the corresponding source coordinate.
*/
fun viewToSourceCoord(vxy: PointF): PointF? {
return viewToSourceCoord(vxy.x, vxy.y, PointF())
}
fun viewToSourceCoord(vxy: PointF): PointF? = viewToSourceCoord(vxy.x, vxy.y, PointF())
/**
* Convert screen coordinate to source coordinate.
@@ -952,16 +946,12 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
/**
* Convert source to view x coordinate.
*/
private fun sourceToViewX(sx: Float): Float {
return if (vTranslate == null) Float.NaN else sx * scale + vTranslate!!.x
}
private fun sourceToViewX(sx: Float): Float = if (vTranslate == null) Float.NaN else sx * scale + vTranslate!!.x
/**
* Convert source to view y coordinate.
*/
private fun sourceToViewY(sy: Float): Float {
return if (vTranslate == null) Float.NaN else sy * scale + vTranslate!!.y
}
private fun sourceToViewY(sy: Float): Float = if (vTranslate == null) Float.NaN else sy * scale + vTranslate!!.y
/**
* Convert source coordinate to view coordinate.
@@ -969,9 +959,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
* @param sxy source coordinates to convert.
* @return view coordinates.
*/
fun sourceToViewCoord(sxy: PointF?): PointF? {
return sourceToViewCoord(sxy!!.x, sxy.y, PointF())
}
fun sourceToViewCoord(sxy: PointF?): PointF? = sourceToViewCoord(sxy!!.x, sxy.y, PointF())
/**
* Convert source coordinate to view coordinate.
@@ -1053,9 +1041,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
/**
* Adjust a requested scale to be within the allowed limits.
*/
private fun limitedScale(targetScale: Float): Float {
return targetScale.coerceIn(minScale(), maxScale)
}
private fun limitedScale(targetScale: Float): Float = targetScale.coerceIn(minScale(), maxScale)
/**
* Apply a selected type of easing.
@@ -1067,12 +1053,10 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
* @param duration Anm duration
* @return Current value
*/
private fun ease(type: Int, time: Long, from: Float, change: Float, duration: Long): Float {
return when (type) {
EASE_IN_OUT_QUAD -> easeInOutQuad(time, from, change, duration)
EASE_OUT_QUAD -> easeOutQuad(time, from, change, duration)
else -> throw IllegalStateException("Unexpected easing type: $type")
}
private fun ease(type: Int, time: Long, from: Float, change: Float, duration: Long): Float = when (type) {
EASE_IN_OUT_QUAD -> easeInOutQuad(time, from, change, duration)
EASE_OUT_QUAD -> easeOutQuad(time, from, change, duration)
else -> throw IllegalStateException("Unexpected easing type: $type")
}
/**
@@ -1199,9 +1183,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
*
* @return the minimum scale as a source/view pixels ratio.
*/
fun getMinScale(): Float {
return minScale()
}
fun getMinScale(): Float = minScale()
/**
* Set the minimum scale allowed. A value of 1 means 1:1 pixels at minimum scale. You may wish to set this according
@@ -1325,9 +1307,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
* @param sCenter Target center point
* @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
*/
fun animateCenter(sCenter: PointF): AnimationBuilder? {
return if (isReady) AnimationBuilder(sCenter) else null
}
fun animateCenter(sCenter: PointF): AnimationBuilder? = if (isReady) AnimationBuilder(sCenter) else null
/**
* Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image
@@ -1337,9 +1317,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
* @param sCenter Target source center.
* @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
*/
fun animateScaleAndCenter(scale: Float, sCenter: PointF?): AnimationBuilder? {
return if (isReady) AnimationBuilder(scale, sCenter) else null
}
fun animateScaleAndCenter(scale: Float, sCenter: PointF?): AnimationBuilder? = if (isReady) AnimationBuilder(scale, sCenter) else null
/**
* An event listener, allowing subclasses and activities to be notified of significant events.
@@ -77,7 +77,9 @@ import splitties.init.appCtx
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
private val lifecycleScope = lifecycle.coroutineScope
class EhApplication : Application(), SingletonImageLoader.Factory {
class EhApplication :
Application(),
SingletonImageLoader.Factory {
override fun onCreate() {
// Initialize Settings on first access
lifecycleScope.launchIO {
@@ -100,6 +100,7 @@ fun <T> PrefDelegate<T>.asMutableState(): MutableState<T> {
object Settings : DataStorePreferences(null) {
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
@Suppress("ktlint:standard:backing-property-naming")
private val _favFlow = MutableSharedFlow<Unit>()
val favChangesFlow = _favFlow.debounce(1000)
var favCat by stringArrayPref("fav_cat", 10, "Favorites").emitTo(_favFlow)
@@ -225,7 +226,7 @@ object Settings : DataStorePreferences(null) {
private fun intArrayPref(key: String, count: Int) = object : Delegate<IntArray> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
@Suppress("ktlint:standard:property-naming")
@Suppress("ktlint:standard:backing-property-naming")
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): IntArray = _value.map { it.value }.toIntArray()
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: IntArray) {
@@ -237,7 +238,7 @@ object Settings : DataStorePreferences(null) {
private fun stringArrayPref(key: String, count: Int, defMetaValue: String) = object : Delegate<Array<String>> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
@Suppress("ktlint:standard:property-naming")
@Suppress("ktlint:standard:backing-property-naming")
private var _value = (0 until count).map { stringPref("${key}_$it", "$defMetaValue $it") }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): Array<String> = _value.map { it.value }.toTypedArray()
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Array<String>) {
@@ -29,13 +29,9 @@ private const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
private const val URL_PREFIX_THUMB_EX = "https://s.exhentai.org/"
private const val URL_PREFIX_LARGE_THUMB_EX = URL_PREFIX_THUMB_EX + "t/"
fun getImageKey(gid: Long, index: Int): String {
return "image:$gid:$index"
}
fun getImageKey(gid: Long, index: Int): String = "image:$gid:$index"
fun getThumbKey(url: String): String {
return url.removePrefix(URL_PREFIX_THUMB_E).removePrefix(URL_PREFIX_LARGE_THUMB_EX).removePrefix(URL_PREFIX_THUMB_EX)
}
fun getThumbKey(url: String): String = url.removePrefix(URL_PREFIX_THUMB_E).removePrefix(URL_PREFIX_LARGE_THUMB_EX).removePrefix(URL_PREFIX_THUMB_EX)
val GalleryPreview.url
get() = if (exThumb) {
@@ -63,9 +63,7 @@ object EhTagDatabase : CoroutineScope {
var initialized by mutableStateOf(false)
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
return tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
}
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? = tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
private fun internalSuggestFlow(
tags: Map<String, String>,
@@ -131,24 +129,16 @@ object EhTagDatabase : CoroutineScope {
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
removeSpace().contains(other.removeSpace(), ignoreCase)
private fun getMetadata(context: Context): Array<String>? {
return context.resources.getStringArray(R.array.tag_translation_metadata)
.takeIf { it.size == 4 }
}
private fun getMetadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)
.takeIf { it.size == 4 }
fun isTranslatable(context: Context): Boolean {
return context.resources.getBoolean(R.bool.tag_translatable)
}
fun isTranslatable(context: Context): Boolean = context.resources.getBoolean(R.bool.tag_translatable)
private fun getFileContent(file: File): String? {
return runCatching {
file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }
}.getOrNull()
}
private fun getFileContent(file: File): String? = runCatching {
file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }
}.getOrNull()
private fun checkData(sha1: String?, data: File): Boolean {
return sha1 != null && sha1 == data.asUniFile().sha1()
}
private fun checkData(sha1: String?, data: File): Boolean = sha1 != null && sha1 == data.asUniFile().sha1()
private suspend fun save(client: HttpClient, url: String, file: File) {
runCatching {
@@ -88,21 +88,18 @@ object EhUtils {
fun getCategory(type: String?): Int {
for (entry in CATEGORY_STRINGS) {
for (str in entry.first)
for (str in entry.first) {
if (str.equals(type, ignoreCase = true)) {
return entry.second
}
}
}
return UNKNOWN
}
fun getCategory(type: Int): String {
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
}
fun getCategory(type: Int): String = CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
private fun differenceDegrees(a: Float, b: Float): Float {
return 180.0f - abs(abs(a - b) - 180.0f)
}
private fun differenceDegrees(a: Float, b: Float): Float = 180.0f - abs(abs(a - b) - 180.0f)
private fun sanitizeDegreesDouble(degrees: Float): Float {
val deg = degrees % 360.0f
@@ -159,13 +156,11 @@ object EhUtils {
Settings.needSignIn = true
}
fun getSuitableTitle(gi: GalleryInfo): String {
return if (Settings.showJpnTitle) {
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
} else {
if (gi.title.isNullOrEmpty()) gi.titleJpn else gi.title
}.orEmpty()
}
fun getSuitableTitle(gi: GalleryInfo): String = if (Settings.showJpnTitle) {
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
} else {
if (gi.title.isNullOrEmpty()) gi.titleJpn else gi.title
}.orEmpty()
fun extractTitle(fullTitle: String?): String? {
var title: String = fullTitle ?: return null
@@ -88,7 +88,8 @@ data class BaseGalleryInfo(
@Ignore
override var favoriteNote: String? = null,
) : GalleryInfo, Parcelable {
) : GalleryInfo,
Parcelable {
constructor() : this(0, "")
}
@@ -55,7 +55,8 @@ data class GalleryDetail(
var comments: GalleryCommentList,
val previewPages: Int,
val previewList: List<GalleryPreview>,
) : GalleryInfo by galleryInfo, Parcelable {
) : GalleryInfo by galleryInfo,
Parcelable {
fun fillInfo() {
val index = LANGUAGES.indexOf(language)
if (index != -1) simpleLanguage = S_LANGS[index]
@@ -134,14 +134,10 @@ interface GalleryInfo {
}
}
fun GalleryInfo.findBaseInfo(): BaseGalleryInfo {
return when (this) {
is BaseGalleryInfo -> this
is GalleryDetail -> galleryInfo
else -> unreachable()
}
fun GalleryInfo.findBaseInfo(): BaseGalleryInfo = when (this) {
is BaseGalleryInfo -> this
is GalleryDetail -> galleryInfo
else -> unreachable()
}
fun GalleryInfo.asGalleryDetail(): GalleryDetail? {
return this as? GalleryDetail
}
fun GalleryInfo.asGalleryDetail(): GalleryDetail? = this as? GalleryDetail
@@ -22,4 +22,5 @@ import kotlinx.parcelize.Parcelize
data class GalleryTagGroup(
val groupName: String,
private val tagList: MutableList<String> = mutableListOf(),
) : Parcelable, MutableList<String> by tagList
) : Parcelable,
MutableList<String> by tagList
@@ -83,18 +83,16 @@ data class ListUrlBuilder(
mNext = q.name.substringAfterLast('@', "").ifEmpty { null }
}
fun toQuickSearch(name: String): QuickSearch {
return QuickSearch(
name = name,
mode = mode,
category = category,
keyword = mKeyword,
advanceSearch = advanceSearch,
minRating = minRating,
pageFrom = pageFrom,
pageTo = pageTo,
)
}
fun toQuickSearch(name: String): QuickSearch = QuickSearch(
name = name,
mode = mode,
category = category,
keyword = mKeyword,
advanceSearch = advanceSearch,
minRating = minRating,
pageFrom = pageFrom,
pageTo = pageTo,
)
fun equalsQuickSearch(q: QuickSearch?): Boolean {
if (null == q) {
@@ -259,85 +257,83 @@ data class ListUrlBuilder(
}
}
fun build(): String {
return when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl(EhUrl.WATCHED_PATH.takeIf { mode == MODE_SUBSCRIPTION }) {
val category = if (!Settings.hasSignedIn.value && category <= 0) EhUtils.NON_H else category
if (category > 0) {
addQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
fun build(): String = when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl(EhUrl.WATCHED_PATH.takeIf { mode == MODE_SUBSCRIPTION }) {
val category = if (!Settings.hasSignedIn.value && category <= 0) EhUtils.NON_H else category
if (category > 0) {
addQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
}
val query = mKeyword?.let {
val index = Settings.languageFilter.value
GalleryInfo.S_LANG_TAGS.getOrNull(index)?.let { lang ->
"$it $lang"
} ?: it
}
addQueryParameterIfNotBlank("f_search", query)
addQueryParameterIfNotBlank("prev", mPrev)
addQueryParameterIfNotBlank("next", mNext)
addQueryParameterIfNotBlank("seek", mJumpTo)
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
// Advance search
if (advanceSearch > 0 || minRating > 0 || pageFrom > 0 || pageTo > 0) {
addQueryParameter("advsearch", "1")
if (advanceSearch and AdvanceTable.SH != 0) {
addQueryParameter("f_sh", "on")
}
val query = mKeyword?.let {
val index = Settings.languageFilter.value
GalleryInfo.S_LANG_TAGS.getOrNull(index)?.let { lang ->
"$it $lang"
} ?: it
if (advanceSearch and AdvanceTable.STO != 0) {
addQueryParameter("f_sto", "on")
}
addQueryParameterIfNotBlank("f_search", query)
if (advanceSearch and AdvanceTable.SFL != 0) {
addQueryParameter("f_sfl", "on")
}
if (advanceSearch and AdvanceTable.SFU != 0) {
addQueryParameter("f_sfu", "on")
}
if (advanceSearch and AdvanceTable.SFT != 0) {
addQueryParameter("f_sft", "on")
}
// Set min star
if (minRating > 0) {
addQueryParameter("f_sr", "on")
addQueryParameter("f_srdd", "$minRating")
}
// Pages
if (pageFrom > 0 || pageTo > 0) {
addQueryParameter("f_sp", "on")
addQueryParameterIfNotBlank(
"f_spf",
pageFrom.takeIf { it > 0 }?.toString(),
)
addQueryParameterIfNotBlank(
"f_spt",
pageTo.takeIf { it > 0 }?.toString(),
)
}
}
}.buildString()
MODE_UPLOADER, MODE_TAG -> {
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
ehUrl(listOf(path, requireNotNull(mKeyword))) {
addQueryParameterIfNotBlank("prev", mPrev)
addQueryParameterIfNotBlank("next", mNext)
addQueryParameterIfNotBlank("seek", mJumpTo)
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
// Advance search
if (advanceSearch > 0 || minRating > 0 || pageFrom > 0 || pageTo > 0) {
addQueryParameter("advsearch", "1")
if (advanceSearch and AdvanceTable.SH != 0) {
addQueryParameter("f_sh", "on")
}
if (advanceSearch and AdvanceTable.STO != 0) {
addQueryParameter("f_sto", "on")
}
if (advanceSearch and AdvanceTable.SFL != 0) {
addQueryParameter("f_sfl", "on")
}
if (advanceSearch and AdvanceTable.SFU != 0) {
addQueryParameter("f_sfu", "on")
}
if (advanceSearch and AdvanceTable.SFT != 0) {
addQueryParameter("f_sft", "on")
}
// Set min star
if (minRating > 0) {
addQueryParameter("f_sr", "on")
addQueryParameter("f_srdd", "$minRating")
}
// Pages
if (pageFrom > 0 || pageTo > 0) {
addQueryParameter("f_sp", "on")
addQueryParameterIfNotBlank(
"f_spf",
pageFrom.takeIf { it > 0 }?.toString(),
)
addQueryParameterIfNotBlank(
"f_spt",
pageTo.takeIf { it > 0 }?.toString(),
)
}
}
}.buildString()
MODE_UPLOADER, MODE_TAG -> {
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
ehUrl(listOf(path, requireNotNull(mKeyword))) {
addQueryParameterIfNotBlank("prev", mPrev)
addQueryParameterIfNotBlank("next", mNext)
addQueryParameterIfNotBlank("seek", mJumpTo)
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
}.buildString()
}
MODE_WHATS_HOT -> EhUrl.popularUrl
MODE_IMAGE_SEARCH -> ehUrl {
addQueryParameter("f_shash", requireNotNull(hash))
}.buildString()
MODE_TOPLIST -> {
ehUrl("toplist.php", EhUrl.DOMAIN_E) {
addQueryParameter("tl", requireNotNull(mKeyword))
addQueryParameterIfNotBlank("p", mJumpTo)
}.buildString()
}
else -> throw IllegalStateException("Unexpected value: $mode")
}
MODE_WHATS_HOT -> EhUrl.popularUrl
MODE_IMAGE_SEARCH -> ehUrl {
addQueryParameter("f_shash", requireNotNull(hash))
}.buildString()
MODE_TOPLIST -> {
ehUrl("toplist.php", EhUrl.DOMAIN_E) {
addQueryParameter("tl", requireNotNull(mKeyword))
addQueryParameterIfNotBlank("p", mJumpTo)
}.buildString()
}
else -> throw IllegalStateException("Unexpected value: $mode")
}
@IntDef(
@@ -20,7 +20,5 @@ package com.hippo.ehviewer.client.parser
import org.jsoup.Jsoup
object EventPaneParser {
fun parse(body: String): String? {
return Jsoup.parse(body).getElementById("eventpane")?.html()
}
fun parse(body: String): String? = Jsoup.parse(body).getElementById("eventpane")?.html()
}
@@ -17,12 +17,10 @@ data class FavParserResult(
}
object FavoritesParser {
fun parse(body: ByteBuffer): FavParserResult {
return runSuspendCatching {
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
}.getOrElse {
throw ParseException("Parse favorites error", it)
}
fun parse(body: ByteBuffer): FavParserResult = runSuspendCatching {
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
}.getOrElse {
throw ParseException("Parse favorites error", it)
}
}
@@ -21,15 +21,13 @@ import com.hippo.ehviewer.util.ExceptionUtils
import org.jsoup.Jsoup
object ForumsParser {
fun parse(body: String): String {
return runCatching {
val d = Jsoup.parse(body, EhUrl.URL_FORUMS)
val userlinks = d.getElementById("userlinks")
val child = userlinks!!.child(0).child(0).child(0)
child.attr("href")
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw NotLoggedInException()
}
fun parse(body: String): String = runCatching {
val d = Jsoup.parse(body, EhUrl.URL_FORUMS)
val userlinks = d.getElementById("userlinks")
val child = userlinks!!.child(0).child(0).child(0)
child.attr("href")
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw NotLoggedInException()
}
}
@@ -283,56 +283,50 @@ object GalleryDetailParser {
}
}
private fun parseTagGroup(element: Element): GalleryTagGroup? {
return try {
var nameSpace = element.child(0).text()
// Remove last ':'
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
val group = GalleryTagGroup(nameSpace)
val tags = element.child(1).children()
tags.forEach {
var tag = it.text()
// Sometimes parody tag is followed with '|' and english translate, just remove them
val index = tag.indexOf('|')
if (index >= 0) {
tag = tag.substring(0, index).trim()
}
if (it.className() == "gtw") {
tag = "_$tag" // weak tag
}
group.add(tag)
private fun parseTagGroup(element: Element): GalleryTagGroup? = try {
var nameSpace = element.child(0).text()
// Remove last ':'
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
val group = GalleryTagGroup(nameSpace)
val tags = element.child(1).children()
tags.forEach {
var tag = it.text()
// Sometimes parody tag is followed with '|' and english translate, just remove them
val index = tag.indexOf('|')
if (index >= 0) {
tag = tag.substring(0, index).trim()
}
if (group.size > 0) group else null
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
logcat(e)
null
if (it.className() == "gtw") {
tag = "_$tag" // weak tag
}
group.add(tag)
}
if (group.size > 0) group else null
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
logcat(e)
null
}
/**
* Parse tag groups with html parser
*/
private fun parseTagGroups(document: Document): List<GalleryTagGroup> {
return try {
val taglist = document.getElementById("taglist")!!
val tagGroups = taglist.child(0).child(0).children()
parseTagGroups(tagGroups)
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
logcat(e)
emptyList()
}
private fun parseTagGroups(document: Document): List<GalleryTagGroup> = try {
val taglist = document.getElementById("taglist")!!
val tagGroups = taglist.child(0).child(0).children()
parseTagGroups(tagGroups)
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
logcat(e)
emptyList()
}
private fun parseTagGroups(trs: Elements): List<GalleryTagGroup> {
return try {
trs.mapNotNull { parseTagGroup(it) }
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
logcat(e)
emptyList()
}
private fun parseTagGroups(trs: Elements): List<GalleryTagGroup> = try {
trs.mapNotNull { parseTagGroup(it) }
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
logcat(e)
emptyList()
}
private suspend fun parseComment(element: Element): GalleryComment? {
@@ -449,22 +443,16 @@ object GalleryDetailParser {
/**
* Parse preview pages with regular expressions
*/
fun parsePreviewPages(body: String): Int {
return PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse preview page count error")
}
fun parsePreviewPages(body: String): Int = PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse preview page count error")
/**
* Parse pages with regular expressions
*/
fun parsePages(body: String): Int {
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse pages error")
}
fun parsePages(body: String): Int = PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse pages error")
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> {
return runCatching { parseNormalPreview(body) }.getOrElse { parseLargePreview(body) }
}
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> = runCatching { parseNormalPreview(body) }.getOrElse { parseLargePreview(body) }
private fun parseLargePreview(body: String): Pair<List<GalleryPreview>, List<String>> {
check(PATTERN_LARGE_PREVIEW.containsMatchIn(body))
@@ -22,14 +22,12 @@ import eu.kanade.tachiyomi.util.system.logcat
import org.jsoup.Jsoup
object GalleryNotAvailableParser {
fun parse(body: String): String? {
return runCatching {
val e = Jsoup.parse(body).getElementsByClass("d").first()
e!!.child(0).html().replace("<br>", "\n")
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
logcat(it)
null
}
fun parse(body: String): String? = runCatching {
val e = Jsoup.parse(body).getElementsByClass("d").first()
e!!.child(0).html().replace("<br>", "\n")
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
logcat(it)
null
}
}
@@ -32,13 +32,11 @@ object GalleryTokenApiParser {
* ]
* }
*/
fun parse(body: String): String {
return runCatching {
body.parseAs<Result>().tokenList[0].token
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw EhException(body.parseAs<Error>().error)
}
fun parse(body: String): String = runCatching {
body.parseAs<Result>().tokenList[0].token
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw EhException(body.parseAs<Error>().error)
}
@Serializable
@@ -37,23 +37,13 @@ object ParserUtils {
minute()
}
fun formatDate(time: Long): String {
return formatter.format(time.toLocalDateTime())
}
fun formatDate(time: Long): String = formatter.format(time.toLocalDateTime())
fun parseDate(str: String): Long {
return formatter.parse(str).toEpochMillis()
}
fun parseDate(str: String): Long = formatter.parse(str).toEpochMillis()
fun trim(str: String?): String {
return str?.unescapeXml()?.trim() ?: ""
}
fun trim(str: String?): String = str?.unescapeXml()?.trim() ?: ""
fun parseInt(str: String?, defValue: Int): Int {
return trim(str).replace(",", "").toIntOrDefault(defValue)
}
fun parseInt(str: String?, defValue: Int): Int = trim(str).replace(",", "").toIntOrDefault(defValue)
fun parseLong(str: String?, defValue: Long): Long {
return trim(str).replace(",", "").toLongOrDefault(defValue)
}
fun parseLong(str: String?, defValue: Long): Long = trim(str).replace(",", "").toLongOrDefault(defValue)
}
@@ -22,31 +22,29 @@ import eu.kanade.tachiyomi.util.system.logcat
import org.jsoup.Jsoup
object ProfileParser {
fun parse(body: String): Result {
return runCatching {
val d = Jsoup.parse(body)
val profilename = d.getElementById("profilename")
val displayName = profilename!!.child(0).text()
val avatar = runCatching {
val avatar =
profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr("src")
if (avatar.isEmpty()) {
null
} else if (!avatar.startsWith("http")) {
EhUrl.URL_FORUMS + avatar
} else {
avatar
}
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
logcat { "No avatar" }
fun parse(body: String): Result = runCatching {
val d = Jsoup.parse(body)
val profilename = d.getElementById("profilename")
val displayName = profilename!!.child(0).text()
val avatar = runCatching {
val avatar =
profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr("src")
if (avatar.isEmpty()) {
null
} else if (!avatar.startsWith("http")) {
EhUrl.URL_FORUMS + avatar
} else {
avatar
}
Result(displayName, avatar)
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw ParseException("Parse forums error")
logcat { "No avatar" }
null
}
Result(displayName, avatar)
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw ParseException("Parse forums error")
}
class Result(val displayName: String?, val avatar: String?)
@@ -25,11 +25,9 @@ object SignInParser {
"|<span class=\"postcolor\">(.+?)</span>",
)
fun parse(body: String): String {
return NAME_PATTERN.find(body)?.let {
it.groupValues[1]
} ?: ERROR_PATTERN.find(body)?.let {
throw EhException(it.groupValues[1].ifEmpty { it.groupValues[2] })
} ?: throw ParseException("Can't parse sign in")
}
fun parse(body: String): String = NAME_PATTERN.find(body)?.let {
it.groupValues[1]
} ?: ERROR_PATTERN.find(body)?.let {
throw EhException(it.groupValues[1].ifEmpty { it.groupValues[2] })
} ?: throw ParseException("Can't parse sign in")
}
@@ -21,7 +21,5 @@ import com.hippo.ehviewer.client.parseAs
object VoteTagParser {
// {"error":"The tag \"neko\" is not allowed. Use character:neko or artist:neko"}
fun parse(body: String): String? {
return runCatching { body.parseAs<Error>().error }.getOrNull()
}
fun parse(body: String): String? = runCatching { body.parseAs<Error>().error }.getOrNull()
}
@@ -37,8 +37,7 @@ import splitties.init.appCtx
private const val CHANNEL_ID = "DailyCheckNotification"
class DailyCheckWork(context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
class DailyCheckWork(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = withIOContext {
checkDawn().onFailure {
return@withIOContext Result.retry()
@@ -72,7 +72,8 @@ data class DownloadInfo(
@Embedded
val downloadInfo: DownloadEntity = DownloadEntity(galleryInfo.gid),
) : GalleryInfo by galleryInfo, AbstractDownloadInfo by downloadInfo {
) : GalleryInfo by galleryInfo,
AbstractDownloadInfo by downloadInfo {
companion object {
const val STATE_INVALID = -1
@@ -46,7 +46,10 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineScope {
class DownloadService :
Service(),
DownloadManager.DownloadListener,
CoroutineScope {
override val coroutineContext = Dispatchers.IO + SupervisorJob()
private val deferredMgr = async { DownloadManager.apply { setDownloadListener(this@DownloadService) } }
private var mNotifyManager: NotificationManagerCompat? = null
@@ -149,19 +149,15 @@ class ArchivePageLoader(
FileUtils.getNameFromFilename(file.name)!!
}
override fun getImageExtension(index: Int): String {
return getExtension(index)
}
override fun getImageExtension(index: Int): String = getExtension(index)
override fun save(index: Int, file: UniFile): Boolean {
return runCatching {
file.openFileDescriptor("w").use {
extractToFd(index, it.fd)
}
}.getOrElse {
logcat(it)
false
override fun save(index: Int, file: UniFile): Boolean = runCatching {
file.openFileDescriptor("w").use {
extractToFd(index, it.fd)
}
}.getOrElse {
logcat(it)
false
}
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
@@ -25,7 +25,8 @@ import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
import com.hippo.unifile.UniFile
class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
PageLoader2(mGalleryInfo.gid, startPage), OnSpiderListener {
PageLoader2(mGalleryInfo.gid, startPage),
OnSpiderListener {
private lateinit var mSpiderQueen: SpiderQueen
override fun start() {
super.start()
@@ -45,13 +46,9 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
EhUtils.getSuitableTitle(mGalleryInfo)
}
override fun getImageExtension(index: Int): String? {
return mSpiderQueen.getExtension(index)
}
override fun getImageExtension(index: Int): String? = mSpiderQueen.getExtension(index)
override fun save(index: Int, file: UniFile): Boolean {
return mSpiderQueen.save(index, file)
}
override fun save(index: Int, file: UniFile): Boolean = mSpiderQueen.save(index, file)
override val size: Int
get() = mSpiderQueen.size
@@ -64,9 +61,7 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
mSpiderQueen.forceRequest(index, orgImg)
}
override suspend fun awaitReady(): Boolean {
return super.awaitReady() && mSpiderQueen.awaitReady()
}
override suspend fun awaitReady(): Boolean = super.awaitReady() && mSpiderQueen.awaitReady()
override val isReady: Boolean
get() = ::mSpiderQueen.isInitialized && mSpiderQueen.isReady
@@ -27,7 +27,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
abstract class PageLoader2(private val gid: Long, var startPage: Int) : PageLoader(), CoroutineScope {
abstract class PageLoader2(private val gid: Long, var startPage: Int) :
PageLoader(),
CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
private val progressJob = launch(start = CoroutineStart.LAZY) {
if (startPage == -1) {
@@ -7,17 +7,11 @@ import okio.source
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
val source = inputStream.source().buffer()
fun read(): String {
return source.readUtf8LineStrict()
}
fun read(): String = source.readUtf8LineStrict()
fun readInt(): Int {
return read().toInt()
}
fun readInt(): Int = read().toInt()
fun readLong(): Long {
return read().toLong()
}
fun readLong(): Long = read().toLong()
repeat(2) { read() } // We assert that only info v2
val gid = readLong()
val token = read()
@@ -18,12 +18,10 @@ object DownloadInfoMagics {
}
}
fun decodeMagicRequestOrUrl(encoded: String): Pair<String, String?> {
return if (encoded.startsWith(DOWNLOAD_INFO_DIRNAME_URL_MAGIC)) {
val (a, b) = encoded.removePrefix(DOWNLOAD_INFO_DIRNAME_URL_MAGIC).split(DOWNLOAD_INFO_DIRNAME_URL_SEPARATOR)
a to b
} else {
encoded to null
}
fun decodeMagicRequestOrUrl(encoded: String): Pair<String, String?> = if (encoded.startsWith(DOWNLOAD_INFO_DIRNAME_URL_MAGIC)) {
val (a, b) = encoded.removePrefix(DOWNLOAD_INFO_DIRNAME_URL_MAGIC).split(DOWNLOAD_INFO_DIRNAME_URL_SEPARATOR)
a to b
} else {
encoded to null
}
}
@@ -93,13 +93,9 @@ class SpiderDen(val info: GalleryInfo) {
}
// Search in both directories to maintain compatibility
private fun findImageFile(index: Int): UniFile? {
return tempDownloadDir?.findImageFile(index) ?: downloadDir?.findImageFile(index)
}
private fun findImageFile(index: Int): UniFile? = tempDownloadDir?.findImageFile(index) ?: downloadDir?.findImageFile(index)
private fun containInDownloadDir(index: Int): Boolean {
return findImageFile(index) != null
}
private fun containInDownloadDir(index: Int): Boolean = findImageFile(index) != null
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
val dir = imageDir ?: return false
@@ -116,19 +112,17 @@ class SpiderDen(val info: GalleryInfo) {
}.getOrDefault(false)
}
operator fun contains(index: Int): Boolean {
return when (mode) {
SpiderQueen.MODE_READ -> {
containInCache(index) || containInDownloadDir(index)
}
operator fun contains(index: Int): Boolean = when (mode) {
SpiderQueen.MODE_READ -> {
containInCache(index) || containInDownloadDir(index)
}
SpiderQueen.MODE_DOWNLOAD -> {
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
}
SpiderQueen.MODE_DOWNLOAD -> {
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
}
else -> {
false
}
else -> {
false
}
}
@@ -137,17 +131,11 @@ class SpiderDen(val info: GalleryInfo) {
return sCache.remove(key)
}
private fun removeFromDownloadDir(index: Int): Boolean {
return findImageFile(index)?.delete() ?: false
}
private fun removeFromDownloadDir(index: Int): Boolean = findImageFile(index)?.delete() ?: false
fun remove(index: Int): Boolean {
return removeFromCache(index) or removeFromDownloadDir(index)
}
fun remove(index: Int): Boolean = removeFromCache(index) or removeFromDownloadDir(index)
private fun findDownloadFileForIndex(index: Int, extension: String): UniFile? {
return imageDir?.createFile(perFilename(index, extension))
}
private fun findDownloadFileForIndex(index: Int, extension: String): UniFile? = imageDir?.createFile(perFilename(index, extension))
suspend fun makeHttpCallAndSaveImage(
index: Int,
@@ -346,9 +334,7 @@ class SpiderDen(val info: GalleryInfo) {
private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
private val FileHashRegex = Regex("/([0-9a-f]{40})(?:-\\d+){3}-\\w+")
fun perFilename(index: Int, extension: String = ""): String {
return "%08d.%s".format(index + 1, extension)
}
fun perFilename(index: Int, extension: String = ""): String = "%08d.%s".format(index + 1, extension)
private fun UniFile.findImageFile(index: Int): UniFile? {
val head = perFilename(index)
@@ -58,24 +58,20 @@ private val spiderInfoCache by lazy {
}
}
fun readFromCache(gid: Long): SpiderInfo? {
return spiderInfoCache.read(gid.toString()) {
runCatching {
cbor.decodeFromByteArray<SpiderInfo>(data.toFile().readBytes())
}.onFailure {
logcat(it)
}.getOrNull()
}
}
fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
return runCatching {
file.openInputStream().use {
cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
}
}.getOrNull() ?: runCatching {
file.openInputStream().use { readLegacySpiderInfo(it) }
fun readFromCache(gid: Long): SpiderInfo? = spiderInfoCache.read(gid.toString()) {
runCatching {
cbor.decodeFromByteArray<SpiderInfo>(data.toFile().readBytes())
}.onFailure {
logcat(it)
}.getOrNull()
}
fun readCompatFromUniFile(file: UniFile): SpiderInfo? = runCatching {
file.openInputStream().use {
cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
}
}.getOrNull() ?: runCatching {
file.openInputStream().use { readLegacySpiderInfo(it) }
}.getOrNull()
const val TOKEN_FAILED = "failed"
@@ -320,16 +320,14 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
}
private fun readSpiderInfoFromLocal(): SpiderInfo? {
return mSpiderDen.downloadDir?.run {
findFile(SPIDER_INFO_FILENAME)?.let { file ->
readCompatFromUniFile(file)?.takeIf {
it.gid == galleryInfo.gid && it.token == galleryInfo.token
}
private fun readSpiderInfoFromLocal(): SpiderInfo? = mSpiderDen.downloadDir?.run {
findFile(SPIDER_INFO_FILENAME)?.let { file ->
readCompatFromUniFile(file)?.takeIf {
it.gid == galleryInfo.gid && it.token == galleryInfo.token
}
}
?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
}
?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
private fun readPreviews(body: String, index: Int, spiderInfo: SpiderInfo) {
spiderInfo.previewPages = parsePreviewPages(body)
@@ -349,21 +347,19 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
}
private suspend fun readSpiderInfoFromInternet(): SpiderInfo? {
return runSuspendCatching {
ehRequest(
getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, 0, false),
referer,
).fetchUsingAsText {
val pages = parsePages(this)
val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token, pages)
readPreviews(this, 0, spiderInfo)
spiderInfo
}
}.onFailure {
logcat(it)
}.getOrNull()
}
private suspend fun readSpiderInfoFromInternet(): SpiderInfo? = runSuspendCatching {
ehRequest(
getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, 0, false),
referer,
).fetchUsingAsText {
val pages = parsePages(this)
val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token, pages)
readPreviews(this, 0, spiderInfo)
spiderInfo
}
}.onFailure {
logcat(it)
}.getOrNull()
suspend fun getPTokenFromMultiPageViewer(index: Int): String? {
val spiderInfo = mSpiderInfo
@@ -421,9 +417,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
mSpiderInfo.saveToCache()
}
private fun isStateDone(state: Int): Boolean {
return state == STATE_FINISHED || state == STATE_FAILED
}
private fun isStateDone(state: Int): Boolean = state == STATE_FINISHED || state == STATE_FAILED
fun updatePageState(index: Int, @State state: Int, error: String? = null) {
var oldState: Int
@@ -439,9 +439,7 @@ suspend fun DialogState.awaitSelectDate(): String? {
title = R.string.go_to,
yearRange = initial.year..yesterday.year,
selectableDates = object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
return utcTimeMillis in dateRange
}
override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis in dateRange
},
)
val date = dateMillis?.run { toLocalDateTime().date.toString() }
@@ -98,10 +98,8 @@ fun rememberSliderPagerDoubleSyncState(
lazyListState: LazyListState,
pagerState: PagerState,
pageLoader: PageLoader2,
): SliderPagerDoubleSync {
return remember {
SliderPagerDoubleSync(lazyListState, pagerState, pageLoader)
}
): SliderPagerDoubleSync = remember {
SliderPagerDoubleSync(lazyListState, pagerState, pageLoader)
}
private fun smoothScroll(distance: Int) =
@@ -15,8 +15,10 @@ data class DownloadsFilterState(
) : Parcelable
fun DownloadsFilterState.take(info: DownloadInfo) =
mode.take(info, label) && (state == -1 || info.state == state) &&
mode.take(info, label) &&
(state == -1 || info.state == state) &&
with(info) {
title.containsIgnoreCase(keyword) || titleJpn.containsIgnoreCase(keyword) ||
title.containsIgnoreCase(keyword) ||
titleJpn.containsIgnoreCase(keyword) ||
simpleTags?.any { it.containsIgnoreCase(keyword) } ?: false
}
@@ -184,21 +184,19 @@ data class TokenArgs(
) : GalleryDetailScreenArgs
@StringRes
private fun getRatingText(rating: Float): Int {
return when ((rating * 2).roundToInt()) {
0 -> R.string.rating0
1 -> R.string.rating1
2 -> R.string.rating2
3 -> R.string.rating3
4 -> R.string.rating4
5 -> R.string.rating5
6 -> R.string.rating6
7 -> R.string.rating7
8 -> R.string.rating8
9 -> R.string.rating9
10 -> R.string.rating10
else -> R.string.rating_none
}
private fun getRatingText(rating: Float): Int = when ((rating * 2).roundToInt()) {
0 -> R.string.rating0
1 -> R.string.rating1
2 -> R.string.rating2
3 -> R.string.rating3
4 -> R.string.rating4
5 -> R.string.rating5
6 -> R.string.rating6
7 -> R.string.rating7
8 -> R.string.rating8
9 -> R.string.rating9
10 -> R.string.rating10
else -> R.string.rating_none
}
private fun List<GalleryTagGroup>.getArtistTag(): String? {
@@ -618,14 +616,12 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
)
}
Spacer(modifier = Modifier.size(keylineMargin))
fun getAllRatingText(rating: Float, ratingCount: Int): String {
return getString(
R.string.rating_text,
getString(getRatingText(rating)),
rating,
ratingCount,
)
}
fun getAllRatingText(rating: Float, ratingCount: Int): String = getString(
R.string.rating_text,
getString(getRatingText(rating)),
rating,
ratingCount,
)
var ratingText by rememberSaveable {
mutableStateOf(getAllRatingText(galleryDetail.rating, galleryDetail.ratingCount))
}
@@ -298,22 +298,20 @@ fun SearchBarScreen(
}
}
fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
return if (keyword.endsWith(':')) {
keyword
fun wrapTagKeyword(keyword: String, translate: Boolean = false): String = if (keyword.endsWith(':')) {
keyword
} else {
val tag = keyword.substringAfter(':')
val prefix = keyword.dropLast(tag.length + 1)
if (translate) {
val namespacePrefix = TagNamespace(prefix).toPrefix()
val newPrefix = EhTagDatabase.getTranslation(tag = prefix) ?: prefix
val newTag = EhTagDatabase.getTranslation(namespacePrefix, tag) ?: tag
"$newPrefix:$newTag"
} else if (keyword.contains(' ')) {
"$prefix:\"$tag$\""
} else {
val tag = keyword.substringAfter(':')
val prefix = keyword.dropLast(tag.length + 1)
if (translate) {
val namespacePrefix = TagNamespace(prefix).toPrefix()
val newPrefix = EhTagDatabase.getTranslation(tag = prefix) ?: prefix
val newTag = EhTagDatabase.getTranslation(namespacePrefix, tag) ?: tag
"$newPrefix:$newTag"
} else if (keyword.contains(' ')) {
"$prefix:\"$tag$\""
} else {
"$keyword$"
}
"$keyword$"
}
}
@@ -129,8 +129,9 @@ fun AnnotatedString.toBBCode() = buildString {
val stack = ArrayDeque<AnnotatedString.Range<SpanStyle>>()
val spans = spanStyles.groupBy { it.start }
while (true) {
while (stack.lastOrNull()?.end == current)
while (stack.lastOrNull()?.end == current) {
stack.removeLast().item.pop()
}
if (current == len) break
spans[current]?.sortedByDescending { it.end }?.forEach {
it.item.push()
@@ -209,61 +209,59 @@ class DialogState {
@StringRes checkBoxText: Int,
isNumber: Boolean = false,
invalidator: (suspend Raise<String>.(String, Boolean) -> Unit)? = null,
): Pair<String, Boolean> {
return dialog { cont ->
val coroutineScope = rememberCoroutineScope()
var state by remember(cont) { mutableStateOf(initial) }
var error by remember(cont) { mutableStateOf<String?>(null) }
var checkedState by remember { mutableStateOf(checked) }
AlertDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = {
if (invalidator == null) {
cont.resume(state to checkedState)
} else {
coroutineScope.launch {
error = either { invalidator(state, checkedState) }.leftOrNull()
error ?: cont.resume(state to checkedState)
}
): Pair<String, Boolean> = dialog { cont ->
val coroutineScope = rememberCoroutineScope()
var state by remember(cont) { mutableStateOf(initial) }
var error by remember(cont) { mutableStateOf<String?>(null) }
var checkedState by remember { mutableStateOf(checked) }
AlertDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = {
if (invalidator == null) {
cont.resume(state to checkedState)
} else {
coroutineScope.launch {
error = either { invalidator(state, checkedState) }.leftOrNull()
error ?: cont.resume(state to checkedState)
}
}) {
Text(text = stringResource(id = android.R.string.ok))
}
},
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedTextField(
value = state,
onValueChange = { state = it },
label = hint.ifNotNullThen {
Text(text = stringResource(id = hint!!))
},
trailingIcon = error.ifNotNullThen {
Icon(
imageVector = Icons.Filled.Info,
contentDescription = null,
)
},
supportingText = error.ifNotNullThen {
Text(text = error!!)
},
isError = error != null,
keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default,
)
LabeledCheckbox(
modifier = Modifier.fillMaxWidth(),
checked = checkedState,
onCheckedChange = { checkedState = !checkedState },
label = stringResource(checkBoxText),
)
}
},
)
}
}) {
Text(text = stringResource(id = android.R.string.ok))
}
},
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedTextField(
value = state,
onValueChange = { state = it },
label = hint.ifNotNullThen {
Text(text = stringResource(id = hint!!))
},
trailingIcon = error.ifNotNullThen {
Icon(
imageVector = Icons.Filled.Info,
contentDescription = null,
)
},
supportingText = error.ifNotNullThen {
Text(text = error!!)
},
isError = error != null,
keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default,
)
LabeledCheckbox(
modifier = Modifier.fillMaxWidth(),
checked = checkedState,
onCheckedChange = { checkedState = !checkedState },
label = stringResource(checkBoxText),
)
}
},
)
}
suspend fun awaitPermissionOrCancel(
@@ -275,32 +273,30 @@ class DialogState {
onCancelButtonClick: () -> Unit = {},
secure: Boolean = false,
text: @Composable (() -> Unit)? = null,
) {
return dialog { cont ->
AlertDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = { cont.resume(Unit) }, enabled = confirmButtonEnabled) {
Text(text = stringResource(id = confirmText))
}
},
dismissButton = showCancelButton.ifTrueThen {
TextButton(onClick = {
onCancelButtonClick()
cont.cancel()
}) {
Text(text = stringResource(id = dismissText))
}
},
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
text = text,
properties = if (secure) {
DialogProperties(securePolicy = SecureFlagPolicy.SecureOn)
} else {
DialogProperties()
},
)
}
) = dialog { cont ->
AlertDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = { cont.resume(Unit) }, enabled = confirmButtonEnabled) {
Text(text = stringResource(id = confirmText))
}
},
dismissButton = showCancelButton.ifTrueThen {
TextButton(onClick = {
onCancelButtonClick()
cont.cancel()
}) {
Text(text = stringResource(id = dismissText))
}
},
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
text = text,
properties = if (secure) {
DialogProperties(securePolicy = SecureFlagPolicy.SecureOn)
} else {
DialogProperties()
},
)
}
suspend fun awaitSelectDate(
@@ -311,63 +307,59 @@ class DialogState {
initialDisplayMode: DisplayMode = DisplayMode.Picker,
selectableDates: SelectableDates = DatePickerDefaults.AllDates,
showModeToggle: Boolean = true,
): Long? {
return dialog { cont ->
val state = rememberDatePickerState(
initialSelectedDateMillis,
initialDisplayedMonthMillis,
yearRange,
initialDisplayMode,
selectableDates,
): Long? = dialog { cont ->
val state = rememberDatePickerState(
initialSelectedDateMillis,
initialDisplayedMonthMillis,
yearRange,
initialDisplayMode,
selectableDates,
)
DatePickerDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = { cont.resume(state.selectedDateMillis) }) {
Text(text = stringResource(id = android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { cont.cancel() }) {
Text(text = stringResource(id = android.R.string.cancel))
}
},
) {
DatePicker(
state = state,
title = {
Text(
text = stringResource(id = title),
modifier = Modifier.padding(DatePickerTitlePadding),
)
},
showModeToggle = showModeToggle,
)
DatePickerDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = { cont.resume(state.selectedDateMillis) }) {
Text(text = stringResource(id = android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { cont.cancel() }) {
Text(text = stringResource(id = android.R.string.cancel))
}
},
) {
DatePicker(
state = state,
title = {
Text(
text = stringResource(id = title),
modifier = Modifier.padding(DatePickerTitlePadding),
)
},
showModeToggle = showModeToggle,
)
}
}
}
suspend fun <R> showNoButton(respectDefaultWidth: Boolean = true, block: @Composable DismissDialogScope<R>.() -> Unit): R {
return dialog { cont ->
val impl = remember(cont) {
DismissDialogScope<R> {
cont.resume(it)
}
suspend fun <R> showNoButton(respectDefaultWidth: Boolean = true, block: @Composable DismissDialogScope<R>.() -> Unit): R = dialog { cont ->
val impl = remember(cont) {
DismissDialogScope<R> {
cont.resume(it)
}
BasicAlertDialog(
onDismissRequest = { cont.cancel() },
properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth),
content = {
Surface(
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
shape = AlertDialogDefaults.shape,
color = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
content = { block(impl) },
)
},
)
}
BasicAlertDialog(
onDismissRequest = { cont.cancel() },
properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth),
content = {
Surface(
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
shape = AlertDialogDefaults.shape,
color = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
content = { block(impl) },
)
},
)
}
suspend fun awaitSelectTime(
@@ -55,10 +55,8 @@ fun MetaRatingWidget(rating: Float, ratingSize: Dp, ratingInterval: Dp, modifier
val density = LocalDensity.current
val ratingSizePx = remember { with(density) { ratingSize.toPx() } }
val ratingIntervalPx = remember { with(density) { ratingInterval.toPx() } }
fun calculateRating(offset: Float): Float {
return ((offset * 2 + ratingIntervalPx) / (ratingSizePx + ratingIntervalPx))
.roundToInt().coerceIn(0, 10).div(2f)
}
fun calculateRating(offset: Float): Float = ((offset * 2 + ratingIntervalPx) / (ratingSizePx + ratingIntervalPx))
.roundToInt().coerceIn(0, 10).div(2f)
Row(
modifier = onRatingChange?.let {
modifier
@@ -13,6 +13,4 @@ fun rememberLambda(calculation: @DisallowComposableCalls () -> Unit): () -> Unit
fun rememberLambda(
key1: Any?,
calculation: @DisallowComposableCalls () -> Unit,
): () -> Unit {
return currentComposer.cache(currentComposer.changed(key1)) { calculation }
}
): () -> Unit = currentComposer.cache(currentComposer.changed(key1)) { calculation }
@@ -199,8 +199,7 @@ private fun rememberColumnWidthSums(
horizontalArrangement,
contentPadding,
) {
{
constraints ->
{ constraints ->
require(constraints.maxWidth != Constraints.Infinity) {
"LazyVerticalGrid's width should be bound by parent"
}
@@ -230,8 +229,7 @@ private fun rememberColumnWidthSums(
horizontalArrangement,
contentPadding,
) {
{
constraints ->
{ constraints ->
require(constraints.maxWidth != Constraints.Infinity) {
"LazyVerticalGrid's width should be bound by parent"
}
@@ -57,9 +57,7 @@ object AppConfig {
val defaultDownloadDir: File?
get() = getDirInExternalAppDir(DOWNLOAD, false)
fun getTempDir(filename: String): UniFile? {
return getDirInExternalAppDir(TEMP)?.run { File(this, filename).asUniFile() }
}
fun getTempDir(filename: String): UniFile? = getDirInExternalAppDir(TEMP)?.run { File(this, filename).asUniFile() }
val externalTempDir: File?
get() = appCtx.externalCacheDir?.run { File(this, TEMP).takeIf { it.ensureDirectory() } }
@@ -71,9 +69,7 @@ object AppConfig {
val tempDir: File?
get() = appCtx.cacheDir.run { File(this, TEMP).takeIf { it.ensureDirectory() } }
fun createTempFile(): File? {
return FileUtils.createTempFile(tempDir, null)
}
fun createTempFile(): File? = FileUtils.createTempFile(tempDir, null)
fun getFilesDir(name: String) = File(appCtx.filesDir, name).takeIf { it.ensureDirectory() }
}
@@ -8,11 +8,10 @@ inline fun <R : Any, C : MutableCollection<in R>> LongArray.mapNotNullTo(destina
inline fun <T> Collection<T>.mapToLongArray(transform: (T) -> Long): LongArray {
val result = LongArray(size)
var index = 0
for (element in this)
for (element in this) {
result[index++] = transform(element)
}
return result
}
inline fun <R : Any> LongArray.mapNotNull(transform: (Long) -> R?): List<R> {
return mapNotNullTo(ArrayList(), transform)
}
inline fun <R : Any> LongArray.mapNotNull(transform: (Long) -> R?): List<R> = mapNotNullTo(ArrayList(), transform)
@@ -9,14 +9,10 @@ import java.io.FileWriter
import java.io.OutputStreamWriter
import java.io.PrintWriter
private fun joinIfStringArray(any: Any?): String {
return if (any is Array<*>) any.joinToString() else any.toString()
}
private fun joinIfStringArray(any: Any?): String = if (any is Array<*>) any.joinToString() else any.toString()
private fun collectClassStaticInfo(clazz: Class<*>): String {
return clazz.fields.joinToString("\n") {
"${it.name}=${joinIfStringArray(it.get(null))}"
}
private fun collectClassStaticInfo(clazz: Class<*>): String = clazz.fields.joinToString("\n") {
"${it.name}=${joinIfStringArray(it.get(null))}"
}
object Crash {
@@ -57,7 +57,8 @@ suspend fun <I, O> Context.awaitActivityResult(contract: ActivityResultContract<
}
return withUIContext {
lifecycle.addObserver(observer)
suspendCoroutine { cont -> // No cancellation support here since we cannot cancel a launched Intent
suspendCoroutine { cont ->
// No cancellation support here since we cannot cancel a launched Intent
val activity = findActivity<ComponentActivity>()
launcher = activity.activityResultRegistry.register(key, contract) {
launcher?.unregister()
@@ -34,7 +34,9 @@ package com.hippo.ehviewer.util
*
* @param T the type of ranges in this set
*/
abstract class RangeSet<T : Comparable<T>> : MutableSet<ClosedRange<T>>, Cloneable {
abstract class RangeSet<T : Comparable<T>> :
MutableSet<ClosedRange<T>>,
Cloneable {
private val ranges = ArrayDeque<ClosedRange<T>>()
/**
@@ -93,10 +95,11 @@ abstract class RangeSet<T : Comparable<T>> : MutableSet<ClosedRange<T>>, Cloneab
* @return whether all values are all contained within the set
*/
override fun contains(element: ClosedRange<T>): Boolean {
for (range in ranges)
for (range in ranges) {
if (element.start >= range.start && element.endInclusive <= range.endInclusive) {
return true
}
}
return false
}
@@ -218,9 +221,7 @@ abstract class RangeSet<T : Comparable<T>> : MutableSet<ClosedRange<T>>, Cloneab
* @param elements ranges of values to add
* @return whether any values were added
*/
override fun addAll(elements: Collection<ClosedRange<T>>): Boolean {
return elements.map { add(it) }.any()
}
override fun addAll(elements: Collection<ClosedRange<T>>): Boolean = elements.map { add(it) }.any()
/**
* Removes all values from the set.
@@ -319,9 +320,7 @@ abstract class RangeSet<T : Comparable<T>> : MutableSet<ClosedRange<T>>, Cloneab
* @param elements ranges of values to remove
* @return whether any values were removed
*/
override fun removeAll(elements: Collection<ClosedRange<T>>): Boolean {
return elements.map { remove(it) }.any { it }
}
override fun removeAll(elements: Collection<ClosedRange<T>>): Boolean = elements.map { remove(it) }.any { it }
/**
* Removes all values from set except those contained in the specified range; intersection set logic.
@@ -441,9 +440,7 @@ abstract class RangeSet<T : Comparable<T>> : MutableSet<ClosedRange<T>>, Cloneab
*
* @return new set containing values not present in this set within the specified range
*/
fun difference(element: ClosedRange<T>): RangeSet<T> {
return differenceAll(listOf(element))
}
fun difference(element: ClosedRange<T>): RangeSet<T> = differenceAll(listOf(element))
/**
* Creates a new [RangeSet] containing values not present in this set within the specified ranges.
@@ -476,19 +473,13 @@ abstract class RangeSet<T : Comparable<T>> : MutableSet<ClosedRange<T>>, Cloneab
*
* @return new set containing missing values between the ranges of this set
*/
fun gaps(): RangeSet<T> {
return if (ranges.isEmpty()) clone().apply { clear() } else difference(ranges.first().endInclusive..ranges.last().start)
}
fun gaps(): RangeSet<T> = if (ranges.isEmpty()) clone().apply { clear() } else difference(ranges.first().endInclusive..ranges.last().start)
override fun iterator(): MutableIterator<ClosedRange<T>> = ranges.iterator()
override fun hashCode(): Int {
return ranges.hashCode()
}
override fun hashCode(): Int = ranges.hashCode()
override fun equals(other: Any?): Boolean {
return this === other || (other is RangeSet<*> && ranges == other.ranges)
}
override fun equals(other: Any?): Boolean = this === other || (other is RangeSet<*> && ranges == other.ranges)
protected abstract fun createRange(start: T, endInclusive: T): ClosedRange<T>
@@ -143,17 +143,15 @@ object ReadableTime {
}
}
fun getShortTimeInterval(time: Long): String {
return buildString {
for (i in 0 until SIZE) {
val multiple = MULTIPLES[i]
val quotient = time / multiple
if (time > multiple * 1.5 || i == SIZE - 1) {
append(quotient)
.append(" ")
.append(resources.getQuantityString(UNITS[i], quotient.toInt()))
break
}
fun getShortTimeInterval(time: Long): String = buildString {
for (i in 0 until SIZE) {
val multiple = MULTIPLES[i]
val quotient = time / multiple
if (time > multiple * 1.5 || i == SIZE - 1) {
append(quotient)
.append(" ")
.append(resources.getQuantityString(UNITS[i], quotient.toInt()))
break
}
}
}
@@ -25,9 +25,7 @@ class StatusCodeException(val responseCode: Int) : Exception() {
val isIdentifiedResponseCode: Boolean
get() = DEFAULT_ERROR_MESSAGE != message
override fun getLocalizedMessage(): String {
return message
}
override fun getLocalizedMessage(): String = message
companion object {
private val ERROR_MESSAGE_ARRAY = SparseArray<String>(24)
@@ -2,24 +2,14 @@ package com.hippo.ehviewer.util
import org.jsoup.parser.Parser
fun String.unescapeXml(): String {
return Parser.unescapeEntities(this, true)
}
fun String.unescapeXml(): String = Parser.unescapeEntities(this, true)
inline infix fun <T> CharSequence.trimAnd(block: CharSequence.() -> T): T {
return block(trim())
}
inline infix fun <T> CharSequence.trimAnd(block: CharSequence.() -> T): T = block(trim())
fun String.toIntOrDefault(defaultValue: Int): Int {
return toIntOrNull() ?: defaultValue
}
fun String.toIntOrDefault(defaultValue: Int): Int = toIntOrNull() ?: defaultValue
fun String.toLongOrDefault(defaultValue: Long): Long {
return toLongOrNull() ?: defaultValue
}
fun String.toLongOrDefault(defaultValue: Long): Long = toLongOrNull() ?: defaultValue
fun String.toFloatOrDefault(defaultValue: Float): Float {
return toFloatOrNull() ?: defaultValue
}
fun String.toFloatOrDefault(defaultValue: Float): Float = toFloatOrNull() ?: defaultValue
fun String?.containsIgnoreCase(other: String) = this?.contains(other, true) ?: false
@@ -28,21 +28,17 @@ object DocumentsContractApi21 {
fun createDirectory(self: Uri, displayName: String) = createFile(self, DocumentsContract.Document.MIME_TYPE_DIR, displayName)
fun prepareTreeUri(treeUri: Uri): Uri {
return DocumentsContract.buildDocumentUriUsingTree(
treeUri,
DocumentsContract.getTreeDocumentId(treeUri),
)
}
fun prepareTreeUri(treeUri: Uri): Uri = DocumentsContract.buildDocumentUriUsingTree(
treeUri,
DocumentsContract.getTreeDocumentId(treeUri),
)
// This is not a good approach as the document identifier is an opaque implementation detail of
// the provider, but works for ExternalStorageProvider
fun buildChildUri(uri: Uri, displayName: String): Uri {
return DocumentsContract.buildDocumentUriUsingTree(
uri,
DocumentsContract.getDocumentId(uri) + "/" + displayName,
)
}
fun buildChildUri(uri: Uri, displayName: String): Uri = DocumentsContract.buildDocumentUriUsingTree(
uri,
DocumentsContract.getDocumentId(uri) + "/" + displayName,
)
fun renameTo(self: Uri, displayName: String) = runCatching {
DocumentsContract.renameDocument(resolver, self, displayName)
@@ -14,16 +14,12 @@ import okio.Path
/**
* Use Native IO/NIO directly if possible, unless you need process file content on JVM!
*/
fun UniFile.openInputStream(): FileInputStream {
return AutoCloseInputStream(openFileDescriptor("r"))
}
fun UniFile.openInputStream(): FileInputStream = AutoCloseInputStream(openFileDescriptor("r"))
/**
* Use Native IO/NIO directly if possible, unless you need process file content on JVM!
*/
fun UniFile.openOutputStream(): FileOutputStream {
return AutoCloseOutputStream(openFileDescriptor("wt"))
}
fun UniFile.openOutputStream(): FileOutputStream = AutoCloseOutputStream(openFileDescriptor("wt"))
fun UniFile.sha1() = openFileDescriptor("r").use { sha1(it.fd) }
@@ -10,10 +10,6 @@ fun Any?.ifNotNullThen(content: @Composable () -> Unit): (@Composable () -> Unit
fun Boolean.ifTrueThen(content: @Composable () -> Unit): (@Composable () -> Unit)? =
if (this) content else null
fun roundToDP(value: Double, places: Int): Double {
return BigDecimal(value).setScale(places, RoundingMode.HALF_EVEN).toDouble()
}
fun roundToDP(value: Double, places: Int): Double = BigDecimal(value).setScale(places, RoundingMode.HALF_EVEN).toDouble()
fun roundToDP(value: Float, places: Int): Float {
return BigDecimal(value.toDouble()).setScale(places, RoundingMode.HALF_EVEN).toFloat()
}
fun roundToDP(value: Float, places: Int): Float = BigDecimal(value.toDouble()).setScale(places, RoundingMode.HALF_EVEN).toFloat()
@@ -24,22 +24,16 @@ sealed class AndroidPreference<T>(
abstract fun write(key: String, value: T): Editor.() -> Unit
override fun key(): String {
return key
}
override fun key(): String = key
override fun get(): T {
return read(preferences, key, defaultValue)
}
override fun get(): T = read(preferences, key, defaultValue)
// Async, don't call get() immediately after this
override fun set(value: T) {
preferences.edit(action = write(key, value))
}
override fun isSet(): Boolean {
return preferences.contains(key)
}
override fun isSet(): Boolean = preferences.contains(key)
override fun delete() {
preferences.edit {
@@ -47,21 +41,15 @@ sealed class AndroidPreference<T>(
}
}
override fun defaultValue(): T {
return defaultValue
}
override fun defaultValue(): T = defaultValue
override fun changes(): Flow<T> {
return keyFlow
.filter { it == key || it == null }
.onStart { emit("ignition") }
.map { get() }
.conflate()
}
override fun changes(): Flow<T> = keyFlow
.filter { it == key || it == null }
.onStart { emit("ignition") }
.map { get() }
.conflate()
override fun stateIn(scope: CoroutineScope): StateFlow<T> {
return changes().stateIn(scope, SharingStarted.Eagerly, get())
}
override fun stateIn(scope: CoroutineScope): StateFlow<T> = changes().stateIn(scope, SharingStarted.Eagerly, get())
class StringPrimitive(
preferences: SharedPreferences,
@@ -69,9 +57,7 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: String,
) : AndroidPreference<String>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: String): String {
return preferences.getString(key, defaultValue) ?: defaultValue
}
override fun read(preferences: SharedPreferences, key: String, defaultValue: String): String = preferences.getString(key, defaultValue) ?: defaultValue
override fun write(key: String, value: String): Editor.() -> Unit = {
putString(key, value)
@@ -84,9 +70,7 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: Long,
) : AndroidPreference<Long>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Long): Long {
return preferences.getLong(key, defaultValue)
}
override fun read(preferences: SharedPreferences, key: String, defaultValue: Long): Long = preferences.getLong(key, defaultValue)
override fun write(key: String, value: Long): Editor.() -> Unit = {
putLong(key, value)
@@ -99,9 +83,7 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: Int,
) : AndroidPreference<Int>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Int): Int {
return preferences.getInt(key, defaultValue)
}
override fun read(preferences: SharedPreferences, key: String, defaultValue: Int): Int = preferences.getInt(key, defaultValue)
override fun write(key: String, value: Int): Editor.() -> Unit = {
putInt(key, value)
@@ -114,9 +96,7 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: Float,
) : AndroidPreference<Float>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Float): Float {
return preferences.getFloat(key, defaultValue)
}
override fun read(preferences: SharedPreferences, key: String, defaultValue: Float): Float = preferences.getFloat(key, defaultValue)
override fun write(key: String, value: Float): Editor.() -> Unit = {
putFloat(key, value)
@@ -129,9 +109,7 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: Boolean,
) : AndroidPreference<Boolean>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Boolean): Boolean {
return preferences.getBoolean(key, defaultValue)
}
override fun read(preferences: SharedPreferences, key: String, defaultValue: Boolean): Boolean = preferences.getBoolean(key, defaultValue)
override fun write(key: String, value: Boolean): Editor.() -> Unit = {
putBoolean(key, value)
@@ -144,9 +122,7 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: Set<String>,
) : AndroidPreference<Set<String>>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Set<String>): Set<String> {
return preferences.getStringSet(key, defaultValue) ?: defaultValue
}
override fun read(preferences: SharedPreferences, key: String, defaultValue: Set<String>): Set<String> = preferences.getStringSet(key, defaultValue) ?: defaultValue
override fun write(key: String, value: Set<String>): Editor.() -> Unit = {
putStringSet(key, value)
@@ -161,12 +137,10 @@ sealed class AndroidPreference<T>(
val serializer: (T) -> String,
val deserializer: (String) -> T,
) : AndroidPreference<T>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T {
return try {
preferences.getString(key, null)?.let(deserializer) ?: defaultValue
} catch (e: Exception) {
defaultValue
}
override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T = try {
preferences.getString(key, null)?.let(deserializer) ?: defaultValue
} catch (e: Exception) {
defaultValue
}
override fun write(key: String, value: T): Editor.() -> Unit = {
@@ -18,45 +18,31 @@ object AndroidPreferenceStore : PreferenceStore {
private val keyFlow = sharedPreferences.keyFlow
override fun getString(key: String, defaultValue: String): Preference<String> {
return StringPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getString(key: String, defaultValue: String): Preference<String> = StringPrimitive(sharedPreferences, keyFlow, key, defaultValue)
override fun getLong(key: String, defaultValue: Long): Preference<Long> {
return LongPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getLong(key: String, defaultValue: Long): Preference<Long> = LongPrimitive(sharedPreferences, keyFlow, key, defaultValue)
override fun getInt(key: String, defaultValue: Int): Preference<Int> {
return IntPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getInt(key: String, defaultValue: Int): Preference<Int> = IntPrimitive(sharedPreferences, keyFlow, key, defaultValue)
override fun getFloat(key: String, defaultValue: Float): Preference<Float> {
return FloatPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getFloat(key: String, defaultValue: Float): Preference<Float> = FloatPrimitive(sharedPreferences, keyFlow, key, defaultValue)
override fun getBoolean(key: String, defaultValue: Boolean): Preference<Boolean> {
return BooleanPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getBoolean(key: String, defaultValue: Boolean): Preference<Boolean> = BooleanPrimitive(sharedPreferences, keyFlow, key, defaultValue)
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> = StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue)
override fun <T> getObject(
key: String,
defaultValue: T,
serializer: (T) -> String,
deserializer: (String) -> T,
): Preference<T> {
return Object(
preferences = sharedPreferences,
keyFlow = keyFlow,
key = key,
defaultValue = defaultValue,
serializer = serializer,
deserializer = deserializer,
)
}
): Preference<T> = Object(
preferences = sharedPreferences,
keyFlow = keyFlow,
key = key,
defaultValue = defaultValue,
serializer = serializer,
deserializer = deserializer,
)
}
private val SharedPreferences.keyFlow
@@ -25,17 +25,15 @@ interface PreferenceStore {
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
key: String,
defaultValue: T,
): Preference<T> {
return getObject(
key = key,
defaultValue = defaultValue,
serializer = { it.name },
deserializer = {
try {
enumValueOf(it)
} catch (e: IllegalArgumentException) {
defaultValue
}
},
)
}
): Preference<T> = getObject(
key = key,
defaultValue = defaultValue,
serializer = { it.name },
deserializer = {
try {
enumValueOf(it)
} catch (e: IllegalArgumentException) {
defaultValue
}
},
)
@@ -376,15 +376,13 @@ class ReaderActivity : EhActivity() {
private suspend fun makeToast(@StringRes resId: Int) = makeToast(getString(resId))
private fun provideImage(index: Int): Uri? {
return AppConfig.externalTempDir?.let { dir ->
mGalleryProvider?.saveToDir(index, dir.asUniFile())?.name?.let {
FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID + ".fileprovider",
File(dir, it),
)
}
private fun provideImage(index: Int): Uri? = AppConfig.externalTempDir?.let { dir ->
mGalleryProvider?.saveToDir(index, dir.asUniFile())?.name?.let {
FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID + ".fileprovider",
File(dir, it),
)
}
}
@@ -672,28 +670,26 @@ class ReaderActivity : EhActivity() {
*/
private inner class ReaderConfig {
private fun getCombinedPaint(grayscale: Boolean, invertedColors: Boolean): Paint {
return Paint().apply {
colorFilter = ColorMatrixColorFilter(
ColorMatrix().apply {
if (grayscale) {
setSaturation(0f)
}
if (invertedColors) {
postConcat(
ColorMatrix(
floatArrayOf(
-1f, 0f, 0f, 0f, 255f,
0f, -1f, 0f, 0f, 255f,
0f, 0f, -1f, 0f, 255f,
0f, 0f, 0f, 1f, 0f,
),
private fun getCombinedPaint(grayscale: Boolean, invertedColors: Boolean): Paint = Paint().apply {
colorFilter = ColorMatrixColorFilter(
ColorMatrix().apply {
if (grayscale) {
setSaturation(0f)
}
if (invertedColors) {
postConcat(
ColorMatrix(
floatArrayOf(
-1f, 0f, 0f, 0f, 255f,
0f, -1f, 0f, 0f, 255f,
0f, 0f, -1f, 0f, 255f,
0f, 0f, 0f, 1f, 0f,
),
)
}
},
)
}
),
)
}
},
)
}
/**
@@ -759,12 +755,10 @@ class ReaderActivity : EhActivity() {
/**
* Picks background color for [ReaderActivity] based on light/dark theme preference
*/
private fun automaticBackgroundColor(): Int {
return if (baseContext.isNightMode()) {
R.color.reader_background_dark
} else {
android.R.color.white
}
private fun automaticBackgroundColor(): Int = if (baseContext.isNightMode()) {
R.color.reader_background_dark
} else {
android.R.color.white
}
/**
@@ -50,15 +50,13 @@ enum class ReadingModeType(
fun fromSpinner(position: Int?) = entries.find { value -> value.prefValue == position } ?: DEFAULT
fun toViewer(preference: Int?, activity: ReaderActivity): BaseViewer {
return when (fromPreference(preference)) {
LEFT_TO_RIGHT -> L2RPagerViewer(activity)
RIGHT_TO_LEFT -> R2LPagerViewer(activity)
VERTICAL -> VerticalPagerViewer(activity)
WEBTOON -> WebtoonViewer(activity)
CONTINUOUS_VERTICAL -> WebtoonViewer(activity, isContinuous = false)
DEFAULT -> L2RPagerViewer(activity)
}
fun toViewer(preference: Int?, activity: ReaderActivity): BaseViewer = when (fromPreference(preference)) {
LEFT_TO_RIGHT -> L2RPagerViewer(activity)
RIGHT_TO_LEFT -> R2LPagerViewer(activity)
VERTICAL -> VerticalPagerViewer(activity)
WEBTOON -> WebtoonViewer(activity)
CONTINUOUS_VERTICAL -> WebtoonViewer(activity, isContinuous = false)
DEFAULT -> L2RPagerViewer(activity)
}
}
}
@@ -286,9 +286,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
this@ReaderPageImageView.onImageLoaded()
}
private fun Int.getSystemScaledDuration(): Int {
return (this * context.animatorDurationScale).toInt().coerceAtLeast(1)
}
private fun Int.getSystemScaledDuration(): Int = (this * context.animatorDurationScale).toInt().coerceAtLeast(1)
/**
* All of the config except [zoomDuration] will only be used for non-animated image.
@@ -69,28 +69,24 @@ open class Pager(
* Whether the given [ev] should be intercepted. Only used to prevent crashes when child
* views manipulate [requestDisallowInterceptTouchEvent].
*/
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return try {
super.onInterceptTouchEvent(ev)
} catch (e: IllegalArgumentException) {
false
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = try {
super.onInterceptTouchEvent(ev)
} catch (e: IllegalArgumentException) {
false
}
/**
* Handles a touch event. Only used to prevent crashes when child views manipulate
* [requestDisallowInterceptTouchEvent].
*/
override fun onTouchEvent(ev: MotionEvent): Boolean {
return try {
super.onTouchEvent(ev)
} catch (e: NullPointerException) {
false
} catch (e: IndexOutOfBoundsException) {
false
} catch (e: IllegalArgumentException) {
false
}
override fun onTouchEvent(ev: MotionEvent): Boolean = try {
super.onTouchEvent(ev)
} catch (e: NullPointerException) {
false
} catch (e: IndexOutOfBoundsException) {
false
} catch (e: IllegalArgumentException) {
false
}
/**
@@ -115,11 +115,9 @@ class PagerConfig(
field = value.also { it.invertMode = this.tappingInverted }
}
override fun defaultNavigation(): ViewerNavigation {
return when (viewer) {
is VerticalPagerViewer -> LNavigation()
else -> RightAndLeftNavigation()
}
override fun defaultNavigation(): ViewerNavigation = when (viewer) {
is VerticalPagerViewer -> LNavigation()
else -> RightAndLeftNavigation()
}
override fun updateNavigation(navigationMode: Int) {
@@ -24,7 +24,8 @@ class PagerPageHolder(
readerThemedContext: Context,
val viewer: PagerViewer,
val page: ReaderPage,
) : ReaderPageImageView(readerThemedContext), ViewPagerAdapter.PositionableView {
) : ReaderPageImageView(readerThemedContext),
ViewPagerAdapter.PositionableView {
/**
* Item that identifies this view. Needed by the adapter to not recreate views.
@@ -146,9 +146,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
/**
* Returns the view this viewer uses.
*/
override fun getView(): View {
return pager
}
override fun getView(): View = pager
/**
* Returns the PagerPageHolder for the provided page
@@ -44,9 +44,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
/**
* Returns the amount of items of the adapter.
*/
override fun getCount(): Int {
return items.size
}
override fun getCount(): Int = items.size
/**
* Creates a new view for the item at the given [position].
@@ -9,9 +9,7 @@ class L2RPagerViewer(activity: ReaderActivity) : PagerViewer(activity) {
/**
* Creates a new left to right pager.
*/
override fun createPager(): Pager {
return Pager(activity)
}
override fun createPager(): Pager = Pager(activity)
}
/**
@@ -21,9 +19,7 @@ class R2LPagerViewer(activity: ReaderActivity) : PagerViewer(activity) {
/**
* Creates a new right to left pager.
*/
override fun createPager(): Pager {
return Pager(activity)
}
override fun createPager(): Pager = Pager(activity)
/**
* Moves to the next page. On a R2L pager the next page is the one at the left.
@@ -47,7 +43,5 @@ class VerticalPagerViewer(activity: ReaderActivity) : PagerViewer(activity) {
/**
* Creates a new vertical pager.
*/
override fun createPager(): Pager {
return Pager(activity, isHorizontal = false)
}
override fun createPager(): Pager = Pager(activity, isHorizontal = false)
}
@@ -42,9 +42,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
/**
* Returns the amount of items of the adapter.
*/
override fun getItemCount(): Int {
return items.size
}
override fun getItemCount(): Int = items.size
/**
* Creates a new view holder for an item with the given [viewType].
@@ -71,9 +71,7 @@ class WebtoonConfig(
field = value.also { it.invertMode = tappingInverted }
}
override fun defaultNavigation(): ViewerNavigation {
return LNavigation()
}
override fun defaultNavigation(): ViewerNavigation = LNavigation()
override fun updateNavigation(navigationMode: Int) {
this.navigator = when (navigationMode) {
@@ -63,17 +63,13 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
* Fling listener used to delegate events to the recycler view.
*/
inner class FlingListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
return true
}
override fun onDown(e: MotionEvent): Boolean = true
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float,
): Boolean {
return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
}
): Boolean = recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
}
}
@@ -28,9 +28,7 @@ class WebtoonLayoutManager(activity: ReaderActivity) : LinearLayoutManager(activ
* Returns the custom extra layout space.
*/
@Deprecated("Deprecated in Java")
override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
return extraLayoutSpace
}
override fun getExtraLayoutSpace(state: RecyclerView.State): Int = extraLayoutSpace
/**
* Returns the position of the last item whose end side is visible on screen.
@@ -14,7 +14,5 @@ class WebtoonSubsamplingImageView @JvmOverloads constructor(
attrs: AttributeSet? = null,
) : SubsamplingScaleImageView(context, attrs) {
override fun onTouchEvent(event: MotionEvent): Boolean {
return false
}
override fun onTouchEvent(event: MotionEvent): Boolean = false
}
@@ -132,16 +132,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
frame.addView(recycler)
}
override fun getViewForInsets(): View {
return recycler
}
override fun getViewForInsets(): View = recycler
/**
* Returns the view this viewer uses.
*/
override fun getView(): View {
return frame
}
override fun getView(): View = frame
/**
* Destroys this viewer. Called when leaving the reader or swapping viewers.
@@ -265,9 +261,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
* Called from the containing activity when a generic motion [event] is received. It should
* return true if the event was handled, false otherwise.
*/
override fun handleGenericMotionEvent(event: MotionEvent): Boolean {
return false
}
override fun handleGenericMotionEvent(event: MotionEvent): Boolean = false
override fun refreshAdapter() {
refreshAdapter(layoutManager.findLastEndVisibleItemPosition())
@@ -22,16 +22,12 @@ val Context.animatorDurationScale: Float
private const val TABLET_UI_REQUIRED_SCREEN_WIDTH_DP = 720
fun Configuration.isTabletUi(): Boolean {
return smallestScreenWidthDp >= TABLET_UI_REQUIRED_SCREEN_WIDTH_DP
}
fun Configuration.isTabletUi(): Boolean = smallestScreenWidthDp >= TABLET_UI_REQUIRED_SCREEN_WIDTH_DP
/**
* Returns true if current context is in night mode
*/
fun Context.isNightMode(): Boolean {
return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
fun Context.isNightMode(): Boolean = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
/**
* Creates night mode Context depending on reader theme/background
@@ -23,9 +23,7 @@ abstract class ViewPagerAdapter : PagerAdapter() {
container.removeView(view)
}
override fun isViewFromObject(view: View, obj: Any): Boolean {
return view === obj
}
override fun isViewFromObject(view: View, obj: Any): Boolean = view === obj
interface PositionableView {
val item: Any
@@ -3,11 +3,12 @@ package moe.tarsin.coroutines
inline fun <T> MutableIterable<T>.filterInPlaceInline(predicate: (T) -> Boolean, predicateResultToRemove: Boolean): Boolean {
var result = false
with(iterator()) {
while (hasNext())
while (hasNext()) {
if (predicate(next()) == predicateResultToRemove) {
remove()
result = true
}
}
}
return result
}
@@ -31,8 +32,9 @@ private suspend inline fun <T> MutableList<T>.filterInPlaceInline(predicate: (T)
writeIndex++
}
return if (writeIndex < size) {
for (removeIndex in lastIndex downTo writeIndex)
for (removeIndex in lastIndex downTo writeIndex) {
removeAt(removeIndex)
}
true
} else {
false
+2
View File
@@ -92,6 +92,8 @@ jsoup = "org.jsoup:jsoup:1.17.2"
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
ktlint = "com.pinterest:ktlint:1.3.0"
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
logcat = "com.squareup.logcat:logcat:0.1"