Code cleanup and refactor

This commit is contained in:
Koitharu
2024-05-11 11:44:27 +03:00
parent 7c82b4effb
commit 0e10fdaf36
36 changed files with 785 additions and 955 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 639 versionCode = 640
versionName = '7.0-rc2' versionName = '7.0-rc3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {
@@ -87,8 +87,8 @@ dependencies {
} }
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.23' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.core:core-ktx:1.13.1'
@@ -151,14 +151,14 @@ dependencies {
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20240303' testImplementation 'org.json:json:20240303'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
androidTestImplementation 'androidx.room:room-testing:2.6.1' androidTestImplementation 'androidx.room:room-testing:2.6.1'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.1' androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.1'

View File

@@ -36,7 +36,7 @@ class CaptchaNotifier(
.build() .build()
manager.createNotificationChannel(channel) manager.createNotificationChannel(channel)
val intent = CloudFlareActivity.newIntent(context, exception.url, exception.headers) val intent = CloudFlareActivity.newIntent(context, exception)
.setData(exception.url.toUri()) .setData(exception.url.toUri())
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(channel.name) .setContentTitle(channel.name)

View File

@@ -23,12 +23,15 @@ import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.configureForParser import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -137,6 +140,10 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
override fun onCheckPassed() { override fun onCheckPassed() {
pendingResult = RESULT_OK pendingResult = RESULT_OK
val source = intent?.getStringExtra(ARG_SOURCE)
if (source != null) {
CaptchaNotifier(this).dismiss(MangaSource(source))
}
finishAfterTransition() finishAfterTransition()
} }
@@ -174,9 +181,9 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
} }
} }
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() { class Contract : ActivityResultContract<CloudFlareProtectedException, TaggedActivityResult>() {
override fun createIntent(context: Context, input: Pair<String, Headers?>): Intent { override fun createIntent(context: Context, input: CloudFlareProtectedException): Intent {
return newIntent(context, input.first, input.second) return newIntent(context, input)
} }
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult { override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
@@ -188,13 +195,23 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
const val TAG = "CloudFlareActivity" const val TAG = "CloudFlareActivity"
private const val ARG_UA = "ua" private const val ARG_UA = "ua"
private const val ARG_SOURCE = "_source"
fun newIntent( fun newIntent(context: Context, exception: CloudFlareProtectedException) = newIntent(
context = context,
url = exception.url,
source = exception.source,
headers = exception.headers,
)
private fun newIntent(
context: Context, context: Context,
url: String, url: String,
source: MangaSource?,
headers: Headers?, headers: Headers?,
) = Intent(context, CloudFlareActivity::class.java).apply { ) = Intent(context, CloudFlareActivity::class.java).apply {
data = url.toUri() data = url.toUri()
putExtra(ARG_SOURCE, source?.name)
headers?.get(CommonHeaders.USER_AGENT)?.let { headers?.get(CommonHeaders.USER_AGENT)?.let {
putExtra(ARG_UA, it) putExtra(ARG_UA, it)
} }

View File

@@ -20,8 +20,8 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources ORDER BY sort_key") @Query("SELECT * FROM sources ORDER BY sort_key")
abstract suspend fun findAll(): List<MangaSourceEntity> abstract suspend fun findAll(): List<MangaSourceEntity>
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key") @Query("SELECT source FROM sources WHERE enabled = 1")
abstract suspend fun findAllDisabled(): List<MangaSourceEntity> abstract suspend fun findAllEnabledNames(): List<String>
@Query("SELECT * FROM sources ORDER BY sort_key") @Query("SELECT * FROM sources ORDER BY sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>> abstract fun observeAll(): Flow<List<MangaSourceEntity>>

View File

@@ -6,7 +6,6 @@ import androidx.annotation.StringRes
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import okhttp3.Headers
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
@@ -30,7 +29,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
private val activity: FragmentActivity? private val activity: FragmentActivity?
private val fragment: Fragment? private val fragment: Fragment?
private val sourceAuthContract: ActivityResultLauncher<MangaSource> private val sourceAuthContract: ActivityResultLauncher<MangaSource>
private val cloudflareContract: ActivityResultLauncher<Pair<String, Headers?>> private val cloudflareContract: ActivityResultLauncher<CloudFlareProtectedException>
constructor(activity: FragmentActivity) { constructor(activity: FragmentActivity) {
this.activity = activity this.activity = activity
@@ -55,7 +54,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
} }
suspend fun resolve(e: Throwable): Boolean = when (e) { suspend fun resolve(e: Throwable): Boolean = when (e) {
is CloudFlareProtectedException -> resolveCF(e.url, e.headers) is CloudFlareProtectedException -> resolveCF(e)
is AuthRequiredException -> resolveAuthException(e.source) is AuthRequiredException -> resolveAuthException(e.source)
is NotFoundException -> { is NotFoundException -> {
openInBrowser(e.url) openInBrowser(e.url)
@@ -70,9 +69,9 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
else -> false else -> false
} }
private suspend fun resolveCF(url: String, headers: Headers): Boolean = suspendCoroutine { cont -> private suspend fun resolveCF(e: CloudFlareProtectedException): Boolean = suspendCoroutine { cont ->
continuations[CloudFlareActivity.TAG] = cont continuations[CloudFlareActivity.TAG] = cont
cloudflareContract.launch(url to headers) cloudflareContract.launch(e)
} }
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont -> private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->

View File

@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
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.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
@@ -91,6 +92,14 @@ class AppShortcutManager @Inject constructor(
false false
} }
fun getMangaShortcuts(): Set<Long> {
val shortcuts = ShortcutManagerCompat.getShortcuts(
context,
ShortcutManagerCompat.FLAG_MATCH_CACHED or ShortcutManagerCompat.FLAG_MATCH_PINNED or ShortcutManagerCompat.FLAG_MATCH_DYNAMIC,
)
return shortcuts.mapNotNullToSet { it.id.toLongOrNull() }
}
@VisibleForTesting @VisibleForTesting
suspend fun await(): Boolean { suspend fun await(): Boolean {
return shortcutsUpdateJob?.join() != null return shortcutsUpdateJob?.join() != null

View File

@@ -22,11 +22,11 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@@ -38,15 +38,10 @@ class MangaLoaderContextImpl @Inject constructor(
) : MangaLoaderContext() { ) : MangaLoaderContext() {
private var webViewCached: WeakReference<WebView>? = null private var webViewCached: WeakReference<WebView>? = null
private val webViewUserAgent by lazy { obtainWebViewUserAgent() }
private val userAgentLazy = SuspendLazy {
withContext(Dispatchers.Main) {
obtainWebView().settings.userAgentString
}.sanitizeHeaderValue()
}
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main) { override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main.immediate) {
val webView = obtainWebView() val webView = obtainWebView()
suspendCoroutine { cont -> suspendCoroutine { cont ->
webView.evaluateJavascript(script) { result -> webView.evaluateJavascript(script) { result ->
@@ -55,13 +50,7 @@ class MangaLoaderContextImpl @Inject constructor(
} }
} }
override fun getDefaultUserAgent(): String = runCatching { override fun getDefaultUserAgent(): String = webViewUserAgent
runBlocking {
userAgentLazy.get()
}
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
override fun getConfig(source: MangaSource): MangaSourceConfig { override fun getConfig(source: MangaSource): MangaSourceConfig {
return SourceSettings(androidContext, source) return SourceSettings(androidContext, source)
@@ -86,4 +75,22 @@ class MangaLoaderContextImpl @Inject constructor(
webViewCached = WeakReference(it) webViewCached = WeakReference(it)
} }
} }
private fun obtainWebViewUserAgent(): String {
val mainDispatcher = Dispatchers.Main.immediate
return if (!mainDispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
obtainWebViewUserAgentImpl()
} else {
runBlocking(mainDispatcher) {
obtainWebViewUserAgentImpl()
}
}
}
@MainThread
private fun obtainWebViewUserAgentImpl() = runCatching {
obtainWebView().settings.userAgentString.sanitizeHeaderValue()
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
} }

View File

@@ -38,7 +38,7 @@ import java.util.Locale
class RemoteMangaRepository( class RemoteMangaRepository(
private val parser: MangaParser, private val parser: MangaParser,
private val cache: MemoryContentCache, // TODO fix concurrency private val cache: MemoryContentCache,
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor, private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
) : MangaRepository, Interceptor { ) : MangaRepository, Interceptor {

View File

@@ -1,162 +0,0 @@
package org.koitharu.kotatsu.core.ui.image
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import androidx.annotation.ReturnThis
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.toIntUp
import com.google.android.material.R as materialR
class CardDrawable(
context: Context,
private var corners: Int,
) : Drawable() {
private val cornerSize = context.resources.resolveDp(12f)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val cornersF = FloatArray(8)
private val boundsF = RectF()
private val color: ColorStateList
private val path = Path()
private var alpha = 255
private var state: IntArray? = null
private var horizontalInset: Int = 0
init {
paint.style = Paint.Style.FILL
color = context.getThemeColorStateList(materialR.attr.colorSurfaceContainerHighest)
?: ColorStateList.valueOf(Color.TRANSPARENT)
setCorners(corners)
updateColor()
}
override fun draw(canvas: Canvas) {
canvas.drawPath(path, paint)
}
override fun setAlpha(alpha: Int) {
this.alpha = alpha
updateColor()
}
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
}
override fun getColorFilter(): ColorFilter? = paint.colorFilter
override fun getOutline(outline: Outline) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
outline.setPath(path)
} else if (path.isConvex) {
outline.setConvexPath(path)
}
outline.alpha = 1f
}
override fun getPadding(padding: Rect): Boolean {
padding.set(
horizontalInset,
0,
horizontalInset,
0,
)
if (corners or TOP != 0) {
padding.top += cornerSize.toIntUp()
}
if (corners or BOTTOM != 0) {
padding.bottom += cornerSize.toIntUp()
}
return horizontalInset != 0
}
override fun onStateChange(state: IntArray): Boolean {
this.state = state
if (color.isStateful) {
updateColor()
return true
} else {
return false
}
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSPARENT
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
boundsF.set(bounds)
boundsF.inset(horizontalInset.toFloat(), 0f)
path.reset()
path.addRoundRect(boundsF, cornersF, Path.Direction.CW)
path.close()
}
@ReturnThis
fun setCorners(corners: Int): CardDrawable {
this.corners = corners
val topLeft = if (corners and TOP_LEFT == TOP_LEFT) cornerSize else 0f
val topRight = if (corners and TOP_RIGHT == TOP_RIGHT) cornerSize else 0f
val bottomRight = if (corners and BOTTOM_RIGHT == BOTTOM_RIGHT) cornerSize else 0f
val bottomLeft = if (corners and BOTTOM_LEFT == BOTTOM_LEFT) cornerSize else 0f
cornersF[0] = topLeft
cornersF[1] = topLeft
cornersF[2] = topRight
cornersF[3] = topRight
cornersF[4] = bottomRight
cornersF[5] = bottomRight
cornersF[6] = bottomLeft
cornersF[7] = bottomLeft
invalidateSelf()
return this
}
fun setHorizontalInset(inset: Int) {
horizontalInset = inset
invalidateSelf()
}
private fun updateColor() {
paint.color = color.getColorForState(state, color.defaultColor)
paint.alpha = alpha
}
companion object {
const val TOP_LEFT = 1
const val TOP_RIGHT = 2
const val BOTTOM_LEFT = 4
const val BOTTOM_RIGHT = 8
const val LEFT = TOP_LEFT or BOTTOM_LEFT
const val TOP = TOP_LEFT or TOP_RIGHT
const val RIGHT = TOP_RIGHT or BOTTOM_RIGHT
const val BOTTOM = BOTTOM_LEFT or BOTTOM_RIGHT
const val NONE = 0
const val ALL = TOP_LEFT or TOP_RIGHT or BOTTOM_RIGHT or BOTTOM_LEFT
fun from(d: Drawable?): CardDrawable? = when (d) {
null -> null
is CardDrawable -> d
is LayerDrawable -> (0 until d.numberOfLayers).firstNotNullOfOrNull { i ->
from(d.getDrawable(i))
}
else -> null
}
}
}

View File

@@ -1,25 +0,0 @@
package org.koitharu.kotatsu.core.util.ext
import android.app.Activity
import android.graphics.Rect
import android.os.Build
import android.util.DisplayMetrics
import android.view.Display
@Suppress("DEPRECATION")
val Activity.displayCompat: Display
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display ?: windowManager.defaultDisplay
} else {
windowManager.defaultDisplay
}
fun Activity.getDisplaySize(): Rect {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowManager.currentWindowMetrics.bounds
} else {
val dm = DisplayMetrics()
displayCompat.getRealMetrics(dm)
Rect(0, 0, dm.widthPixels, dm.heightPixels)
}
}

View File

@@ -110,10 +110,7 @@ class MangaPrefetchService : CoroutineIntentService() {
} }
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean { private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
if (source == MangaSource.LOCAL) { if (source == MangaSource.LOCAL || context.isPowerSaveMode()) {
return false
}
if (context.isPowerSaveMode()) {
return false return false
} }
val entryPoint = EntryPointAccessors.fromApplication( val entryPoint = EntryPointAccessors.fromApplication(

View File

@@ -173,7 +173,7 @@ class DetailsViewModel @Inject constructor(
} else { } else {
emptyList() emptyList()
} }
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
val branches: StateFlow<List<MangaBranch>> = combine( val branches: StateFlow<List<MangaBranch>> = combine(
details, details,
@@ -220,7 +220,7 @@ class DetailsViewModel @Inject constructor(
chaptersQuery, chaptersQuery,
) { list, reversed, query -> ) { list, reversed, query ->
(if (reversed) list.asReversed() else list).filterSearch(query) (if (reversed) list.asReversed() else list).filterSearch(query)
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val readingTime = combine( val readingTime = combine(
details, details,
@@ -228,7 +228,7 @@ class DetailsViewModel @Inject constructor(
history, history,
) { m, b, h -> ) { m, b, h ->
readingTimeUseCase.invoke(m, b, h) readingTimeUseCase.invoke(m, b, h)
}.stateIn(viewModelScope, SharingStarted.Lazily, null) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
val selectedBranchValue: String? val selectedBranchValue: String?
get() = selectedBranch.value get() = selectedBranch.value

View File

@@ -322,7 +322,7 @@ class DownloadsViewModel @Inject constructor(
emit(mapChapters()) emit(mapChapters())
} }
} }
}.stateIn(viewModelScope, SharingStarted.Eagerly, null) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
private suspend fun tryLoad(manga: Manga) = runCatchingCancellable { private suspend fun tryLoad(manga: Manga) = runCatchingCancellable {
(mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).getDetails(manga) (mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).getDetails(manga)

View File

@@ -17,7 +17,6 @@ import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.parsers.model.ContentType
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.parsers.util.mapToSet
import java.util.Collections import java.util.Collections
@@ -48,8 +47,17 @@ class MangaSourcesRepository @Inject constructor(
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order) return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
} }
suspend fun getDisabledSources(): List<MangaSource> { suspend fun getDisabledSources(): Set<MangaSource> {
return dao.findAllDisabled().toSources(settings.isNsfwContentDisabled, null) val result = EnumSet.copyOf(remoteSources)
val enabled = dao.findAllEnabledNames()
for (name in enabled) {
val source = MangaSource(name)
result.remove(source)
}
if (settings.isNsfwContentDisabled) {
result.removeAll { it.isNsfw() }
}
return result
} }
fun observeIsEnabled(source: MangaSource): Flow<Boolean> { fun observeIsEnabled(source: MangaSource): Flow<Boolean> {
@@ -143,6 +151,7 @@ class MangaSourcesRepository @Inject constructor(
result result
}.distinctUntilChanged() }.distinctUntilChanged()
} else { } else {
assimilateNewSources()
flowOf(emptySet()) flowOf(emptySet())
} }
} }
@@ -199,7 +208,7 @@ class MangaSourcesRepository @Inject constructor(
val result = ArrayList<MangaSource>(size) val result = ArrayList<MangaSource>(size)
for (entity in this) { for (entity in this) {
val source = MangaSource(entity.source) val source = MangaSource(entity.source)
if (skipNsfwSources && source.contentType == ContentType.HENTAI) { if (skipNsfwSources && source.isNsfw()) {
continue continue
} }
if (source in remoteSources) { if (source in remoteSources) {

View File

@@ -17,11 +17,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
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.bookmarks.ui.AllBookmarksActivity import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.dialog.TwoButtonsAlertDialog import org.koitharu.kotatsu.core.ui.dialog.TwoButtonsAlertDialog
import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.ListSelectionController
@@ -34,7 +32,6 @@ import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentExploreBinding import org.koitharu.kotatsu.databinding.FragmentExploreBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
@@ -64,9 +61,6 @@ class ExploreFragment :
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@Inject
lateinit var shortcutManager: AppShortcutManager
private val viewModel by viewModels<ExploreViewModel>() private val viewModel by viewModels<ExploreViewModel>()
private var exploreAdapter: ExploreAdapter? = null private var exploreAdapter: ExploreAdapter? = null
private var sourceSelectionController: ListSelectionController? = null private var sourceSelectionController: ListSelectionController? = null
@@ -213,9 +207,7 @@ class ExploreFragment :
R.id.action_shortcut -> { R.id.action_shortcut -> {
val source = selectedSources.singleOrNull() ?: return false val source = selectedSources.singleOrNull() ?: return false
viewLifecycleScope.launch { viewModel.requestPinShortcut(source)
shortcutManager.requestPinShortcut(source)
}
mode.finish() mode.finish()
} }

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
@@ -43,6 +44,7 @@ class ExploreViewModel @Inject constructor(
private val suggestionRepository: SuggestionRepository, private val suggestionRepository: SuggestionRepository,
private val exploreRepository: ExploreRepository, private val exploreRepository: ExploreRepository,
private val sourcesRepository: MangaSourcesRepository, private val sourcesRepository: MangaSourcesRepository,
private val shortcutManager: AppShortcutManager,
) : BaseViewModel() { ) : BaseViewModel() {
val isGrid = settings.observeAsStateFlow( val isGrid = settings.observeAsStateFlow(
@@ -106,6 +108,12 @@ class ExploreViewModel @Inject constructor(
} }
} }
fun requestPinShortcut(source: MangaSource) {
launchLoadingJob(Dispatchers.Default) {
shortcutManager.requestPinShortcut(source)
}
}
fun respondSuggestionTip(isAccepted: Boolean) { fun respondSuggestionTip(isAccepted: Boolean) {
settings.isSuggestionsEnabled = isAccepted settings.isSuggestionsEnabled = isAccepted
settings.closeTip(TIP_SUGGESTIONS) settings.closeTip(TIP_SUGGESTIONS)

View File

@@ -1,7 +1,5 @@
package org.koitharu.kotatsu.favourites.ui.categories package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -177,10 +175,4 @@ class FavouriteCategoriesActivity :
viewModel.saveOrder(adapter.items ?: return) viewModel.saveOrder(adapter.items ?: return)
} }
} }
@Deprecated("")
companion object {
fun newIntent(context: Context) = Intent(context, FavouriteCategoriesActivity::class.java)
}
} }

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import android.content.Intent
import android.view.View import android.view.View
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -16,7 +17,7 @@ fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, List
val onClickListener = View.OnClickListener { v -> val onClickListener = View.OnClickListener { v ->
val intent = when (v.id) { val intent = when (v.id) {
R.id.chip_create -> FavouritesCategoryEditActivity.newIntent(v.context) R.id.chip_create -> FavouritesCategoryEditActivity.newIntent(v.context)
R.id.chip_manage -> FavouriteCategoriesActivity.newIntent(v.context) R.id.chip_manage -> Intent(v.context, FavouriteCategoriesActivity::class.java)
else -> return@OnClickListener else -> return@OnClickListener
} }
v.context.startActivity(intent) v.context.startActivity(intent)

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.favourites.ui.container package org.koitharu.kotatsu.favourites.ui.container
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -102,7 +103,7 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_retry -> startActivity( R.id.button_retry -> startActivity(
FavouriteCategoriesActivity.newIntent(v.context), Intent(v.context, FavouriteCategoriesActivity::class.java),
) )
} }
} }

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.favourites.ui.container package org.koitharu.kotatsu.favourites.ui.container
import android.content.Context import android.content.Context
import android.content.Intent
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
@@ -19,7 +20,7 @@ class FavouritesContainerMenuProvider(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.action_manage -> { R.id.action_manage -> {
context.startActivity(FavouriteCategoriesActivity.newIntent(context)) context.startActivity(Intent(context, FavouriteCategoriesActivity::class.java))
} }
else -> return false else -> return false

View File

@@ -82,7 +82,7 @@ class ListConfigViewModel @Inject constructor(
ListConfigSection.General -> null ListConfigSection.General -> null
ListConfigSection.Updated -> null ListConfigSection.Updated -> null
ListConfigSection.History -> settings.historySortOrder ListConfigSection.History -> settings.historySortOrder
ListConfigSection.Suggestions -> ListSortOrder.RELEVANCE // TODO ListConfigSection.Suggestions -> ListSortOrder.RELEVANCE
} }
fun setSortOrder(position: Int) { fun setSortOrder(position: Int) {

View File

@@ -72,9 +72,6 @@ class SingleMangaImporter @Inject constructor(
return LocalMangaInput.of(dest).getManga() return LocalMangaInput.of(dest).getManga()
} }
/**
* TODO: progress
*/
private suspend fun DocumentFile.copyTo(destDir: File) { private suspend fun DocumentFile.copyTo(destDir: File) {
if (isDirectory) { if (isDirectory) {
val subDir = File(destDir, requireName()) val subDir = File(destDir, requireName())

View File

@@ -45,7 +45,6 @@ import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.util.OptionsMenuBadgeHelper import org.koitharu.kotatsu.core.ui.util.OptionsMenuBadgeHelper
import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView
import org.koitharu.kotatsu.core.util.ext.hideKeyboard import org.koitharu.kotatsu.core.util.ext.hideKeyboard
import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
@@ -77,7 +76,7 @@ private const val TAG_SEARCH = "search"
class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNavOwner, class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNavOwner,
View.OnClickListener, View.OnClickListener,
View.OnFocusChangeListener, SearchSuggestionListener, View.OnFocusChangeListener, SearchSuggestionListener,
MainNavigationDelegate.OnFragmentChangedListener { MainNavigationDelegate.OnFragmentChangedListener, View.OnLayoutChangeListener {
@Inject @Inject
lateinit var settings: AppSettings lateinit var settings: AppSettings
@@ -136,6 +135,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
} }
viewModel.isBottomNavPinned.observe(this, ::setNavbarPinned) viewModel.isBottomNavPinned.observe(this, ::setNavbarPinned)
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged) searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
viewBinding.bottomNav?.addOnLayoutChangeListener(this)
} }
override fun onRestoreInstanceState(savedInstanceState: Bundle) { override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -211,6 +211,22 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
) )
} }
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
if (top != oldTop || bottom != oldBottom) {
updateContainerBottomMargin()
}
}
override fun onFocusChange(v: View?, hasFocus: Boolean) { override fun onFocusChange(v: View?, hasFocus: Boolean) {
val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH)
if (v?.id == R.id.searchView && hasFocus) { if (v?.id == R.id.searchView && hasFocus) {
@@ -418,12 +434,17 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
view.layoutParams = lp view.layoutParams = lp
} }
} }
viewBinding.container.updateLayoutParams<MarginLayoutParams> { updateContainerBottomMargin()
bottomMargin = if (isPinned) { }
(bottomNavBar?.measureHeight()
?.coerceAtLeast(resources.getDimensionPixelSize(materialR.dimen.m3_bottom_nav_min_height)) ?: 0) private fun updateContainerBottomMargin() {
} else { val bottomNavBar = viewBinding.bottomNav ?: return
0 val newMargin = if (bottomNavBar.isPinned) bottomNavBar.height else 0
with(viewBinding.container) {
val params = layoutParams as MarginLayoutParams
if (params.bottomMargin != newMargin) {
params.bottomMargin = newMargin
layoutParams = params
} }
} }
} }

View File

@@ -4,7 +4,6 @@ import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.view.animation.Interpolator import android.view.animation.Interpolator
import android.widget.Scroller import android.widget.Scroller
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.LinearSmoothScroller
@@ -57,7 +56,7 @@ class DoublePageSnapHelper : SnapHelper() {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager val layoutManager = recyclerView.layoutManager as LinearLayoutManager
check(layoutManager.canScrollHorizontally()) { "RecyclerView must be scrollable" } check(layoutManager.canScrollHorizontally()) { "RecyclerView must be scrollable" }
orientationHelper = OrientationHelper.createHorizontalHelper(layoutManager) orientationHelper = OrientationHelper.createHorizontalHelper(layoutManager)
layoutDirectionHelper = LayoutDirectionHelper(ViewCompat.getLayoutDirection(recyclerView)) layoutDirectionHelper = LayoutDirectionHelper(recyclerView.layoutDirection)
scroller = Scroller(target.context, snapInterpolator) scroller = Scroller(target.context, snapInterpolator)
initItemDimensionIfNeeded(layoutManager) initItemDimensionIfNeeded(layoutManager)
} }

View File

@@ -18,7 +18,6 @@ import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.OverScroller import android.widget.OverScroller
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewConfigurationCompat import androidx.core.view.ViewConfigurationCompat
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
@@ -39,7 +38,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
ZoomControl.ZoomControlListener { ZoomControl.ZoomControlListener {
private val scaleDetector = ScaleGestureDetector(context, this) private val scaleDetector = ScaleGestureDetector(context, this)
private val gestureDetector = GestureDetectorCompat(context, GestureListener()) private val gestureDetector = GestureDetector(context, GestureListener())
private val overScroller = OverScroller(context, AccelerateDecelerateInterpolator()) private val overScroller = OverScroller(context, AccelerateDecelerateInterpolator())
private val transformMatrix = Matrix() private val transformMatrix = Matrix()
@@ -339,7 +338,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
if (overScroller.computeScrollOffset()) { if (overScroller.computeScrollOffset()) {
transformMatrix.postTranslate( transformMatrix.postTranslate(
overScroller.currX.toFloat() - prevPos.x, overScroller.currX.toFloat() - prevPos.x,
overScroller.currY.toFloat() - prevPos.y overScroller.currY.toFloat() - prevPos.y,
) )
prevPos.set(overScroller.currX, overScroller.currY) prevPos.set(overScroller.currX, overScroller.currY)
invalidateTarget() invalidateTarget()

View File

@@ -31,7 +31,7 @@ class ScrobblerStorage(context: Context, service: ScrobblerService) {
ScrobblerUser( ScrobblerUser(
id = lines[0].toLong(), id = lines[0].toLong(),
nickname = lines[1], nickname = lines[1],
avatar = lines[2], avatar = lines[2].takeUnless(String::isEmpty),
service = ScrobblerService.valueOf(lines[3]), service = ScrobblerService.valueOf(lines[3]),
) )
} }
@@ -43,7 +43,7 @@ class ScrobblerStorage(context: Context, service: ScrobblerService) {
val str = StringJoiner("\n") val str = StringJoiner("\n")
.add(value.id) .add(value.id)
.add(value.nickname) .add(value.nickname)
.add(value.avatar) .add(value.avatar.orEmpty())
.add(value.service.name) .add(value.service.name)
.complete() .complete()
putString(KEY_USER, str) putString(KEY_USER, str)

View File

@@ -67,10 +67,8 @@ class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
if (intent != null) { setIntent(intent)
setIntent(intent) processIntent(intent)
processIntent(intent)
}
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {

View File

@@ -20,7 +20,7 @@ class SplitSwitchPreference @JvmOverloads constructor(
var onContainerClickListener: OnPreferenceClickListener? = null var onContainerClickListener: OnPreferenceClickListener? = null
private val containerClickListener = View.OnClickListener { v -> private val containerClickListener = View.OnClickListener {
onContainerClickListener?.onPreferenceClick(this) onContainerClickListener?.onPreferenceClick(this)
} }

View File

@@ -1,35 +1,18 @@
package org.koitharu.kotatsu.stats.ui.views package org.koitharu.kotatsu.stats.ui.views
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color
import android.graphics.DashPathEffect import android.graphics.DashPathEffect
import android.graphics.Paint import android.graphics.Paint
import android.graphics.PathEffect
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF import android.graphics.RectF
import android.graphics.Xfermode
import android.util.AttributeSet import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.collection.MutableIntList
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.minus
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.setPadding
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.replaceWith import org.koitharu.kotatsu.parsers.util.replaceWith
import org.koitharu.kotatsu.parsers.util.toIntUp import org.koitharu.kotatsu.parsers.util.toIntUp
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sqrt
import kotlin.random.Random import kotlin.random.Random
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -126,7 +109,7 @@ class BarChartView @JvmOverloads constructor(
bars.clear() bars.clear()
return return
} }
var fullWidth = rawData.size * (barWidth + minBarSpacing) + minBarSpacing val fullWidth = rawData.size * (barWidth + minBarSpacing) + minBarSpacing
val windowSize = (fullWidth / width.toFloat()).toIntUp() val windowSize = (fullWidth / width.toFloat()).toIntUp()
bars.replaceWith( bars.replaceWith(
rawData.chunked(windowSize) { it.average() }, rawData.chunked(windowSize) { it.average() },

View File

@@ -3,28 +3,17 @@ package org.koitharu.kotatsu.stats.ui.views
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF import android.graphics.RectF
import android.graphics.Xfermode
import android.util.AttributeSet import android.util.AttributeSet
import android.view.GestureDetector import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.annotation.ColorInt
import androidx.collection.MutableIntList
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.graphics.minus
import androidx.core.view.GestureDetectorCompat
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.replaceWith import org.koitharu.kotatsu.parsers.util.replaceWith
import kotlin.math.absoluteValue
import kotlin.math.sqrt import kotlin.math.sqrt
import com.google.android.material.R as materialR
class PieChartView @JvmOverloads constructor( class PieChartView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
@@ -34,8 +23,8 @@ class PieChartView @JvmOverloads constructor(
private val segments = ArrayList<Segment>() private val segments = ArrayList<Segment>()
private val chartBounds = RectF() private val chartBounds = RectF()
private val clearColor = context.getThemeColor(android.R.attr.colorBackground) private val clearColor = context.getThemeColor(android.R.attr.colorBackground)
private val touchDetector = GestureDetectorCompat(context, this) private val touchDetector = GestureDetector(context, this)
private var hightlightedSegment = -1 private var highlightedSegment = -1
var onSegmentClickListener: OnSegmentClickListener? = null var onSegmentClickListener: OnSegmentClickListener? = null
@@ -49,7 +38,7 @@ class PieChartView @JvmOverloads constructor(
var angle = 0f var angle = 0f
for ((i, segment) in segments.withIndex()) { for ((i, segment) in segments.withIndex()) {
paint.color = segment.color paint.color = segment.color
if (i == hightlightedSegment) { if (i == highlightedSegment) {
paint.color = ColorUtils.setAlphaComponent(paint.color, 180) paint.color = ColorUtils.setAlphaComponent(paint.color, 180)
} }
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
@@ -91,7 +80,7 @@ class PieChartView @JvmOverloads constructor(
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_CANCEL || event.actionMasked == MotionEvent.ACTION_UP) { if (event.actionMasked == MotionEvent.ACTION_CANCEL || event.actionMasked == MotionEvent.ACTION_UP) {
hightlightedSegment = -1 highlightedSegment = -1
invalidate() invalidate()
} }
return super.onTouchEvent(event) || touchDetector.onTouchEvent(event) return super.onTouchEvent(event) || touchDetector.onTouchEvent(event)
@@ -102,8 +91,8 @@ class PieChartView @JvmOverloads constructor(
return false return false
} }
val segment = findSegmentIndex(e.x, e.y) val segment = findSegmentIndex(e.x, e.y)
if (segment != hightlightedSegment) { if (segment != highlightedSegment) {
hightlightedSegment = segment highlightedSegment = segment
invalidate() invalidate()
return true return true
} else { } else {

View File

@@ -79,12 +79,10 @@ class SyncHelper @AssistedInject constructor(
.build() .build()
val response = httpClient.newCall(request).execute().log().parseJsonOrNull() val response = httpClient.newCall(request).execute().log().parseJsonOrNull()
if (response != null) { if (response != null) {
val timestamp = response.getLong(FIELD_TIMESTAMP) val categoriesResult = upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES))
val categoriesResult =
upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES), timestamp)
stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L
stats.numInserts += categoriesResult.drop(1).sumOf { it.count?.toLong() ?: 0L } stats.numInserts += categoriesResult.drop(1).sumOf { it.count?.toLong() ?: 0L }
val favouritesResult = upsertFavourites(response.getJSONArray(TABLE_FAVOURITES), timestamp) val favouritesResult = upsertFavourites(response.getJSONArray(TABLE_FAVOURITES))
stats.numDeletes += favouritesResult.first().count?.toLong() ?: 0L stats.numDeletes += favouritesResult.first().count?.toLong() ?: 0L
stats.numInserts += favouritesResult.drop(1).sumOf { it.count?.toLong() ?: 0L } stats.numInserts += favouritesResult.drop(1).sumOf { it.count?.toLong() ?: 0L }
stats.numEntries += stats.numInserts + stats.numDeletes stats.numEntries += stats.numInserts + stats.numDeletes
@@ -105,7 +103,6 @@ class SyncHelper @AssistedInject constructor(
if (response != null) { if (response != null) {
val result = upsertHistory( val result = upsertHistory(
json = response.getJSONArray(TABLE_HISTORY), json = response.getJSONArray(TABLE_HISTORY),
timestamp = response.getLong(FIELD_TIMESTAMP),
) )
stats.numDeletes += result.first().count?.toLong() ?: 0L stats.numDeletes += result.first().count?.toLong() ?: 0L
stats.numInserts += result.drop(1).sumOf { it.count?.toLong() ?: 0L } stats.numInserts += result.drop(1).sumOf { it.count?.toLong() ?: 0L }
@@ -122,12 +119,12 @@ class SyncHelper @AssistedInject constructor(
fun onSyncComplete(result: SyncResult) { fun onSyncComplete(result: SyncResult) {
if (logger.isEnabled) { if (logger.isEnabled) {
logger.log("Sync finshed: ${result.toDebugString()}") logger.log("Sync finished: ${result.toDebugString()}")
logger.flushBlocking() logger.flushBlocking()
} }
} }
private fun upsertHistory(json: JSONArray, timestamp: Long): Array<ContentProviderResult> { private fun upsertHistory(json: JSONArray): Array<ContentProviderResult> {
val uri = uri(authorityHistory, TABLE_HISTORY) val uri = uri(authorityHistory, TABLE_HISTORY)
val operations = ArrayList<ContentProviderOperation>() val operations = ArrayList<ContentProviderOperation>()
json.mapJSONTo(operations) { jo -> json.mapJSONTo(operations) { jo ->
@@ -139,7 +136,7 @@ class SyncHelper @AssistedInject constructor(
return provider.applyBatch(operations) return provider.applyBatch(operations)
} }
private fun upsertFavouriteCategories(json: JSONArray, timestamp: Long): Array<ContentProviderResult> { private fun upsertFavouriteCategories(json: JSONArray): Array<ContentProviderResult> {
val uri = uri(authorityFavourites, TABLE_FAVOURITE_CATEGORIES) val uri = uri(authorityFavourites, TABLE_FAVOURITE_CATEGORIES)
val operations = ArrayList<ContentProviderOperation>() val operations = ArrayList<ContentProviderOperation>()
json.mapJSONTo(operations) { jo -> json.mapJSONTo(operations) { jo ->
@@ -150,7 +147,7 @@ class SyncHelper @AssistedInject constructor(
return provider.applyBatch(operations) return provider.applyBatch(operations)
} }
private fun upsertFavourites(json: JSONArray, timestamp: Long): Array<ContentProviderResult> { private fun upsertFavourites(json: JSONArray): Array<ContentProviderResult> {
val uri = uri(authorityFavourites, TABLE_FAVOURITES) val uri = uri(authorityFavourites, TABLE_FAVOURITES)
val operations = ArrayList<ContentProviderOperation>() val operations = ArrayList<ContentProviderOperation>()
json.mapJSONTo(operations) { jo -> json.mapJSONTo(operations) { jo ->

View File

@@ -31,6 +31,7 @@ class RecentWidgetProvider : BaseAppWidgetProvider() {
} else { } else {
views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root) views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root)
} }
// TODO security
val adapter = Intent(context, RecentWidgetService::class.java) val adapter = Intent(context, RecentWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId) adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME)) adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))

View File

@@ -31,6 +31,7 @@ class ShelfWidgetProvider : BaseAppWidgetProvider() {
} else { } else {
views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root) views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root)
} }
// TODO security
val adapter = Intent(context, ShelfWidgetService::class.java) val adapter = Intent(context, ShelfWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId) adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME)) adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))

View File

@@ -21,7 +21,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:indeterminate="true" android:indeterminate="true"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/appbar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar" app:layout_constraintTop_toBottomOf="@id/appbar"

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,9 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.4.0' classpath 'com.android.tools.build:gradle:8.4.0'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1' classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1'
classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.23-1.0.20' classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.24-1.0.20'
} }
} }