diff --git a/app/build.gradle b/app/build.gradle
index 9f01df521..1f5456819 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -13,8 +13,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 31
- versionCode 379
- versionName '2.1.3'
+ versionCode 380
+ versionName '2.1.4'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -24,10 +24,6 @@ android {
}
}
}
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
buildTypes {
debug {
applicationIdSuffix = '.debug'
@@ -45,16 +41,10 @@ android {
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
- lintOptions {
- disable 'MissingTranslation'
- abortOnError false
- }
- testOptions {
- unitTests.includeAndroidResources = true
- unitTests.returnDefaultValues = false
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
}
-}
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [
@@ -62,6 +52,14 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
'-Xopt-in=kotlin.contracts.ExperimentalContracts',
]
}
+ lintOptions {
+ disable 'MissingTranslation'
+ abortOnError false
+ }
+ testOptions {
+ unitTests.includeAndroidResources = true
+ unitTests.returnDefaultValues = false
+ }
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
@@ -70,13 +68,13 @@ dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.activity:activity-ktx:1.4.0'
- implementation 'androidx.fragment:fragment-ktx:1.4.0'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
- implementation 'androidx.lifecycle:lifecycle-service:2.4.0'
- implementation 'androidx.lifecycle:lifecycle-process:2.4.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
+ implementation 'androidx.fragment:fragment-ktx:1.4.1'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
+ implementation 'androidx.lifecycle:lifecycle-service:2.4.1'
+ implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
@@ -84,11 +82,11 @@ dependencies {
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.material:material:1.4.0'
//noinspection LifecycleAnnotationProcessorWithJava8
- kapt 'androidx.lifecycle:lifecycle-compiler:2.4.0'
+ kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
- implementation 'androidx.room:room-runtime:2.4.0'
- implementation 'androidx.room:room-ktx:2.4.0'
- kapt 'androidx.room:room-compiler:2.4.0'
+ implementation 'androidx.room:room-runtime:2.4.1'
+ implementation 'androidx.room:room-ktx:2.4.1'
+ kapt 'androidx.room:room-compiler:2.4.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okio:okio:2.10.0'
@@ -100,7 +98,7 @@ dependencies {
implementation 'io.insert-koin:koin-android:3.1.4'
implementation 'io.coil-kt:coil-base:1.4.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
- implementation 'com.github.solkin:disk-lru-cache:1.3'
+ implementation 'com.github.solkin:disk-lru-cache:1.4'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
@@ -114,6 +112,6 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
- androidTestImplementation 'androidx.room:room-testing:2.4.0'
+ androidTestImplementation 'androidx.room:room-testing:2.4.1'
androidTestImplementation 'com.google.truth:truth:1.1.3'
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2cc8e53e2..de52fe2fb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -32,7 +32,7 @@
+ android:value="org.koitharu.kotatsu.ui.search.SearchActivity" />
-
+
): Boolean? {
try {
val page = pages.medianOrNull() ?: return null
val url = page.source.repository.getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
- val zip = ZipFile(uri.schemeSpecificPart)
- val entry = zip.getEntry(uri.fragment)
- zip.getInputStream(entry).use {
- getBitmapSize(it)
+ runInterruptible(Dispatchers.IO) {
+ val zip = ZipFile(uri.schemeSpecificPart)
+ val entry = zip.getEntry(uri.fragment)
+ zip.getInputStream(entry).use {
+ getBitmapSize(it)
+ }
}
} else {
val client = get()
@@ -45,7 +47,9 @@ object MangaUtils : KoinComponent {
.cacheControl(CacheUtils.CONTROL_DISABLED)
.build()
client.newCall(request).await().use {
- getBitmapSize(it.body?.byteStream())
+ withContext(Dispatchers.IO) {
+ getBitmapSize(it.body?.byteStream())
+ }
}
}
return size.width * 2 < size.height
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupArchive.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupArchive.kt
index c0af8d788..d0ec8a8cb 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupArchive.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupArchive.kt
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.backup
import android.content.Context
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.koitharu.kotatsu.R
@@ -33,8 +34,7 @@ class BackupArchive(file: File) : MutableZipFile(file) {
private const val DIR_BACKUPS = "backups"
- @Suppress("BlockingMethodInNonBlockingContext")
- suspend fun createNew(context: Context): BackupArchive = withContext(Dispatchers.IO) {
+ suspend fun createNew(context: Context): BackupArchive = runInterruptible(Dispatchers.IO) {
val dir = context.run {
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt
new file mode 100644
index 000000000..44f1ca040
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt
@@ -0,0 +1,18 @@
+package org.koitharu.kotatsu.core.parser
+
+import android.net.Uri
+import coil.map.Mapper
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import org.koitharu.kotatsu.core.model.MangaSource
+
+class FaviconMapper() : Mapper {
+
+ override fun map(data: Uri): HttpUrl {
+ val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
+ val repo = MangaRepository(mangaSource) as RemoteMangaRepository
+ return repo.getFaviconUrl().toHttpUrl()
+ }
+
+ override fun handles(data: Uri) = data.scheme == "favicon"
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt
index 0062bc1d0..293bed94b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt
@@ -29,6 +29,8 @@ abstract class RemoteMangaRepository(
override suspend fun getTags(): Set = emptySet()
+ open fun getFaviconUrl() = "https://${getDomain()}/favicon.ico"
+
open fun onCreatePreferences(map: MutableMap) {
map[SourceSettings.KEY_DOMAIN] = defaultDomain
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt
index 03853cd1f..e71378eec 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt
@@ -21,6 +21,10 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
SortOrder.NEWEST
)
+ override fun getFaviconUrl(): String {
+ return "https://cdn.${getDomain()}/favicons/favicon.png"
+ }
+
override suspend fun getList2(
offset: Int,
query: String?,
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt
index 8032d9783..148fa409d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt
@@ -5,6 +5,7 @@ import coil.ImageLoader
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
+import org.koitharu.kotatsu.core.parser.FaviconMapper
import org.koitharu.kotatsu.local.data.CbzFetcher
val uiModule
@@ -15,6 +16,7 @@ val uiModule
.componentRegistry(
ComponentRegistry.Builder()
.add(CbzFetcher())
+ .add(FaviconMapper())
.build()
).build()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
index d44204533..03712d889 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
@@ -16,7 +16,6 @@ import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -79,6 +78,11 @@ class DownloadService : BaseService() {
return binder ?: DownloadBinder(this).also { binder = it }
}
+ override fun onUnbind(intent: Intent?): Boolean {
+ binder = null
+ return super.onUnbind(intent)
+ }
+
override fun onDestroy() {
unregisterReceiver(controlReceiver)
binder = null
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt
index cd4f5ea83..4e2746cec 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt
@@ -9,23 +9,24 @@ import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runInterruptible
import okio.buffer
import okio.source
import java.util.zip.ZipFile
class CbzFetcher : Fetcher {
- @Suppress("BlockingMethodInNonBlockingContext")
override suspend fun fetch(
pool: BitmapPool,
data: Uri,
size: Size,
options: Options,
- ): FetchResult {
+ ): FetchResult = runInterruptible(Dispatchers.IO) {
val zip = ZipFile(data.schemeSpecificPart)
val entry = zip.getEntry(data.fragment)
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
- return SourceResult(
+ SourceResult(
source = ExtraCloseableBufferedSource(
zip.getInputStream(entry).source().buffer(),
zip,
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt
index b7c5f7b9f..fbc2637aa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.local.data
import androidx.annotation.CheckResult
import kotlinx.coroutines.*
+import org.koitharu.kotatsu.utils.ext.deleteAwait
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
@@ -13,7 +14,6 @@ class WritableCbzFile(private val file: File) {
private val dir = File(file.parentFile, file.nameWithoutExtension)
- @Suppress("BlockingMethodInNonBlockingContext")
suspend fun prepare() = withContext(Dispatchers.IO) {
check(dir.list().isNullOrEmpty()) {
"Dir ${dir.name} is not empty"
@@ -45,11 +45,10 @@ class WritableCbzFile(private val file: File) {
}
@CheckResult
- @Suppress("BlockingMethodInNonBlockingContext")
suspend fun flush() = withContext(Dispatchers.IO) {
val tempFile = File(file.path + ".tmp")
if (tempFile.exists()) {
- tempFile.delete()
+ tempFile.deleteAwait()
}
try {
runInterruptible {
@@ -63,7 +62,7 @@ class WritableCbzFile(private val file: File) {
tempFile.renameTo(file)
} finally {
if (tempFile.exists()) {
- tempFile.delete()
+ tempFile.deleteAwait()
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt
index 3742f007e..822da1c24 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt
@@ -8,6 +8,7 @@ import androidx.collection.ArraySet
import androidx.core.net.toFile
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -42,38 +43,39 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
getFromFile(Uri.parse(manga.url).toFile())
} else manga
- @Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getPages(chapter: MangaChapter): List {
- val uri = Uri.parse(chapter.url)
- val file = uri.toFile()
- val zip = ZipFile(file)
- val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex)
- var entries = zip.entries().asSequence()
- entries = if (index != null) {
- val pattern = index.getChapterNamesPattern(chapter)
- entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) }
- } else {
- val parent = uri.fragment.orEmpty()
- entries.filter { x ->
- !x.isDirectory && x.name.substringBeforeLast(
- File.separatorChar,
- ""
- ) == parent
+ return runInterruptible(Dispatchers.IO){
+ val uri = Uri.parse(chapter.url)
+ val file = uri.toFile()
+ val zip = ZipFile(file)
+ val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex)
+ var entries = zip.entries().asSequence()
+ entries = if (index != null) {
+ val pattern = index.getChapterNamesPattern(chapter)
+ entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) }
+ } else {
+ val parent = uri.fragment.orEmpty()
+ entries.filter { x ->
+ !x.isDirectory && x.name.substringBeforeLast(
+ File.separatorChar,
+ ""
+ ) == parent
+ }
}
+ entries
+ .toList()
+ .sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
+ .map { x ->
+ val entryUri = zipUri(file, x.name)
+ MangaPage(
+ id = entryUri.longHashCode(),
+ url = entryUri,
+ preview = null,
+ referer = chapter.url,
+ source = MangaSource.LOCAL,
+ )
+ }
}
- return entries
- .toList()
- .sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
- .map { x ->
- val entryUri = zipUri(file, x.name)
- MangaPage(
- id = entryUri.longHashCode(),
- url = entryUri,
- preview = null,
- referer = chapter.url,
- source = MangaSource.LOCAL,
- )
- }
}
suspend fun delete(manga: Manga): Boolean {
@@ -137,20 +139,18 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
val file = runCatching {
Uri.parse(localManga.url).toFile()
}.getOrNull() ?: return null
- return withContext(Dispatchers.IO) {
- @Suppress("BlockingMethodInNonBlockingContext")
+ return runInterruptible(Dispatchers.IO) {
ZipFile(file).use { zip ->
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
- val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return@withContext null
- index.getMangaInfo()
+ val index = entry?.let(zip::readText)?.let(::MangaIndex)
+ index?.getMangaInfo()
}
}
}
- suspend fun findSavedManga(remoteManga: Manga): Manga? = withContext(Dispatchers.IO) {
+ suspend fun findSavedManga(remoteManga: Manga): Manga? = runInterruptible(Dispatchers.IO) {
val files = getAllFiles()
for (file in files) {
- @Suppress("BlockingMethodInNonBlockingContext")
val index = ZipFile(file).use { zip ->
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
entry?.let(zip::readText)?.let(::MangaIndex)
@@ -158,7 +158,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
val info = index.getMangaInfo() ?: continue
if (info.id == remoteManga.id) {
val fileUri = file.toUri().toString()
- return@withContext info.copy(
+ return@runInterruptible info.copy(
source = MangaSource.LOCAL,
url = fileUri,
chapters = info.chapters?.map { c -> c.copy(url = fileUri) }
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
index 8e40277c9..b1fc2493e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
@@ -81,10 +82,11 @@ class LocalListViewModel(
}
val dest = settings.getStorageDir(context)?.let { File(it, name) }
?: throw IOException("External files dir unavailable")
- @Suppress("BlockingMethodInNonBlockingContext")
- contentResolver.openInputStream(uri)?.use { source ->
- dest.outputStream().use { output ->
- source.copyTo(output)
+ runInterruptible {
+ contentResolver.openInputStream(uri)?.use { source ->
+ dest.outputStream().use { output ->
+ source.copyTo(output)
+ }
}
} ?: throw IOException("Cannot open input stream: $uri")
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
index c745d2071..4ed70ba02 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
@@ -238,8 +238,8 @@ class ReaderActivity : BaseFullscreenActivity(),
) {
false
} else {
- val targets = binding.root.hitTest(rawX, rawY)
- targets.none { it.hasOnClickListeners() }
+ val touchables = window.peekDecorView()?.touchables
+ touchables?.none { it.hasGlobalPoint(rawX, rawY) } ?: true
}
}
@@ -281,7 +281,7 @@ class ReaderActivity : BaseFullscreenActivity(),
private fun onPageSaved(uri: Uri?) {
if (uri != null) {
- Snackbar.make(binding.container, R.string.page_saved, Snackbar.LENGTH_LONG)
+ Snackbar.make(binding.container, R.string.page_saved, Snackbar.LENGTH_INDEFINITE)
.setAnchorView(binding.appbarBottom)
.setAction(R.string.share) {
ShareHelper(this).shareImage(uri)
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt
index 1948b6585..ed2613fb8 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt
@@ -5,6 +5,7 @@ import android.net.Uri
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.backup.BackupArchive
@@ -32,8 +33,7 @@ class RestoreViewModel(
}
val contentResolver = context.contentResolver
- @Suppress("BlockingMethodInNonBlockingContext")
- val backup = withContext(Dispatchers.IO) {
+ val backup = runInterruptible(Dispatchers.IO) {
val tempFile = File.createTempFile("backup_", ".tmp")
(contentResolver.openInputStream(uri)
?: throw FileNotFoundException()).use { input ->
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt
index 25893f670..dd9c4235a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt
@@ -1,13 +1,13 @@
package org.koitharu.kotatsu.settings.sources
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
+import android.view.*
+import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
+import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -19,14 +19,13 @@ import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourcesSettingsFragment : BaseFragment(),
- SourceConfigListener {
+ SourceConfigListener, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {
- private lateinit var reorderHelper: ItemTouchHelper
+ private var reorderHelper: ItemTouchHelper? = null
private val viewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- reorderHelper = ItemTouchHelper(SourcesReorderCallback())
setHasOptionsMenu(true)
}
@@ -42,12 +41,14 @@ class SourcesSettingsFragment : BaseFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val sourcesAdapter = SourceConfigAdapter(this)
+ val sourcesAdapter = SourceConfigAdapter(this, get(), viewLifecycleOwner)
with(binding.recyclerView) {
setHasFixedSize(true)
addItemDecoration(SourceConfigItemDecoration(view.context))
adapter = sourcesAdapter
- reorderHelper.attachToRecyclerView(this)
+ reorderHelper = ItemTouchHelper(SourcesReorderCallback()).also {
+ it.attachToRecyclerView(this)
+ }
}
viewModel.items.observe(viewLifecycleOwner) {
sourcesAdapter.items = it
@@ -55,10 +56,21 @@ class SourcesSettingsFragment : BaseFragment(),
}
override fun onDestroyView() {
- reorderHelper.attachToRecyclerView(null)
+ reorderHelper = null
super.onDestroyView()
}
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ super.onCreateOptionsMenu(menu, inflater)
+ inflater.inflate(R.menu.opt_sources, menu)
+ val searchMenuItem = menu.findItem(R.id.action_search)
+ searchMenuItem.setOnActionExpandListener(this)
+ val searchView = searchMenuItem.actionView as SearchView
+ searchView.setOnQueryTextListener(this)
+ searchView.setIconifiedByDefault(false)
+ searchView.queryHint = searchMenuItem.title
+ }
+
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding(
bottom = insets.bottom,
@@ -76,13 +88,27 @@ class SourcesSettingsFragment : BaseFragment(),
}
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) {
- reorderHelper.startDrag(holder)
+ reorderHelper?.startDrag(holder)
}
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) {
viewModel.expandOrCollapse(header.localeId)
}
+ override fun onQueryTextSubmit(query: String?): Boolean = false
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ viewModel.performSearch(newText)
+ return true
+ }
+
+ override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true
+
+ override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
+ (item.actionView as SearchView).setQuery("", false)
+ return true
+ }
+
private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
0,
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt
index 52125df63..a908ccf4e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt
@@ -21,6 +21,7 @@ class SourcesSettingsViewModel(
val items = MutableLiveData>(emptyList())
private val expandedGroups = HashSet()
+ private var searchQuery: String? = null
init {
buildList()
@@ -63,9 +64,30 @@ class SourcesSettingsViewModel(
buildList()
}
+ fun performSearch(query: String?) {
+ searchQuery = query?.trim()
+ buildList()
+ }
+
private fun buildList() {
val sources = MangaProviderFactory.getSources(settings, includeHidden = true)
val hiddenSources = settings.hiddenSources
+ val query = searchQuery
+ if (!query.isNullOrEmpty()) {
+ items.value = sources.mapNotNull {
+ if (!it.title.contains(query, ignoreCase = true)) {
+ return@mapNotNull null
+ }
+ SourceConfigItem.SourceItem(
+ source = it,
+ isEnabled = it.name !in hiddenSources,
+ isDraggable = false,
+ )
+ }.ifEmpty {
+ listOf(SourceConfigItem.EmptySearchResult)
+ }
+ return
+ }
val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) {
if (it.name !in hiddenSources) {
KEY_ENABLED
@@ -81,6 +103,7 @@ class SourcesSettingsViewModel(
SourceConfigItem.SourceItem(
source = it,
isEnabled = true,
+ isDraggable = true,
)
}
}
@@ -102,6 +125,7 @@ class SourcesSettingsViewModel(
SourceConfigItem.SourceItem(
source = it,
isEnabled = false,
+ isDraggable = false,
)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt
index d04d22fcc..d580684be 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt
@@ -1,13 +1,19 @@
package org.koitharu.kotatsu.settings.sources.adapter
+import androidx.lifecycle.LifecycleOwner
+import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourceConfigAdapter(
listener: SourceConfigListener,
+ coil: ImageLoader,
+ lifecycleOwner: LifecycleOwner,
) : AsyncListDifferDelegationAdapter(
SourceConfigDiffCallback(),
sourceConfigHeaderDelegate(),
sourceConfigGroupDelegate(listener),
- sourceConfigItemDelegate(listener),
+ sourceConfigItemDelegate(listener, coil, lifecycleOwner),
+ sourceConfigDraggableItemDelegate(listener),
+ sourceConfigEmptySearchDelegate(),
)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt
index df7435bac..aa8c9fb35 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt
@@ -4,14 +4,19 @@ import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View
import android.widget.CompoundButton
-import androidx.core.view.isVisible
-import androidx.core.view.updatePaddingRelative
+import androidx.lifecycle.LifecycleOwner
+import coil.ImageLoader
+import coil.request.Disposable
+import coil.request.ImageRequest
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemExpandableBinding
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
+import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
+import org.koitharu.kotatsu.utils.ext.enqueueWith
fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding(
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
@@ -38,11 +43,44 @@ fun sourceConfigGroupDelegate(
}
}
-@SuppressLint("ClickableViewAccessibility")
fun sourceConfigItemDelegate(
listener: SourceConfigListener,
+ coil: ImageLoader,
+ lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding(
- { layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) }
+ { layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) },
+ on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable }
+) {
+
+ var imageRequest: Disposable? = null
+
+ binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
+ listener.onItemEnabledChanged(item, isChecked)
+ }
+
+ bind {
+ binding.textViewTitle.text = item.source.title
+ binding.switchToggle.isChecked = item.isEnabled
+ imageRequest = ImageRequest.Builder(context)
+ .data(item.faviconUrl)
+ .error(R.drawable.ic_favicon_fallback)
+ .target(binding.imageViewIcon)
+ .lifecycle(lifecycleOwner)
+ .enqueueWith(coil)
+ }
+
+ onViewRecycled {
+ imageRequest?.dispose()
+ imageRequest = null
+ }
+}
+
+@SuppressLint("ClickableViewAccessibility")
+fun sourceConfigDraggableItemDelegate(
+ listener: SourceConfigListener,
+) = adapterDelegateViewBinding(
+ { layoutInflater, parent -> ItemSourceConfigDraggableBinding.inflate(layoutInflater, parent, false) },
+ on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable }
) {
val eventListener = object : View.OnClickListener, View.OnTouchListener,
@@ -70,11 +108,9 @@ fun sourceConfigItemDelegate(
bind {
binding.textViewTitle.text = item.source.title
binding.switchToggle.isChecked = item.isEnabled
- binding.imageViewHandle.isVisible = item.isEnabled
- binding.imageViewConfig.isVisible = item.isEnabled
- binding.root.updatePaddingRelative(
- start = if (item.isEnabled) 0 else binding.imageViewHandle.paddingStart * 2,
- end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd,
- )
}
-}
\ No newline at end of file
+}
+
+fun sourceConfigEmptySearchDelegate() = adapterDelegate(
+ R.layout.item_sources_empty
+) { }
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt
index 370cca88d..8bab50c2a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt
@@ -2,21 +2,25 @@ package org.koitharu.kotatsu.settings.sources.adapter
import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
+import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.*
class SourceConfigDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean {
return when {
oldItem.javaClass != newItem.javaClass -> false
- oldItem is SourceConfigItem.LocaleGroup && newItem is SourceConfigItem.LocaleGroup -> {
+ oldItem is LocaleGroup && newItem is LocaleGroup -> {
oldItem.localeId == newItem.localeId
}
- oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> {
+ oldItem is SourceItem && newItem is SourceItem -> {
oldItem.source == newItem.source
}
- oldItem is SourceConfigItem.Header && newItem is SourceConfigItem.Header -> {
+ oldItem is Header && newItem is Header -> {
oldItem.titleResId == newItem.titleResId
}
+ oldItem == EmptySearchResult && newItem == EmptySearchResult -> {
+ true
+ }
else -> false
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt
index 965ea1171..dd998ddac 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources.model
+import android.net.Uri
import androidx.annotation.StringRes
import org.koitharu.kotatsu.core.model.MangaSource
@@ -49,8 +50,12 @@ sealed interface SourceConfigItem {
class SourceItem(
val source: MangaSource,
val isEnabled: Boolean,
+ val isDraggable: Boolean,
) : SourceConfigItem {
+ val faviconUrl: Uri
+ get() = Uri.fromParts("favicon", source.name, null)
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -59,6 +64,7 @@ sealed interface SourceConfigItem {
if (source != other.source) return false
if (isEnabled != other.isEnabled) return false
+ if (isDraggable != other.isDraggable) return false
return true
}
@@ -66,7 +72,10 @@ sealed interface SourceConfigItem {
override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + isEnabled.hashCode()
+ result = 31 * result + isDraggable.hashCode()
return result
}
}
+
+ object EmptySearchResult : SourceConfigItem
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt b/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt
index a57bae29c..b785a62d2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils
import androidx.annotation.CheckResult
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
@@ -11,12 +12,11 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
-@Suppress("BlockingMethodInNonBlockingContext")
open class MutableZipFile(val file: File) {
protected val dir = File(file.parentFile, file.nameWithoutExtension)
- suspend fun unpack(): Unit = withContext(Dispatchers.IO) {
+ suspend fun unpack(): Unit = runInterruptible(Dispatchers.IO) {
check(dir.list().isNullOrEmpty()) {
"Dir ${dir.name} is not empty"
}
@@ -24,7 +24,7 @@ open class MutableZipFile(val file: File) {
dir.mkdir()
}
if (!file.exists()) {
- return@withContext
+ return@runInterruptible
}
ZipInputStream(FileInputStream(file)).use { zip ->
var entry = zip.nextEntry
@@ -45,7 +45,7 @@ open class MutableZipFile(val file: File) {
}
@CheckResult
- suspend fun flush(): Boolean = withContext(Dispatchers.IO) {
+ suspend fun flush(): Boolean = runInterruptible(Dispatchers.IO) {
val tempFile = File(file.path + ".tmp")
if (tempFile.exists()) {
tempFile.delete()
@@ -57,7 +57,7 @@ open class MutableZipFile(val file: File) {
}
zip.flush()
}
- return@withContext tempFile.renameTo(file)
+ tempFile.renameTo(file)
} finally {
if (tempFile.exists()) {
tempFile.delete()
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt
index fdfb8f20d..b06b18d16 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt
@@ -46,7 +46,7 @@ class ShareHelper(private val context: Context) {
fun shareImage(uri: Uri) {
val intent = Intent(Intent.ACTION_SEND)
- intent.setDataAndType(uri, context.contentResolver.getType(uri))
+ intent.setDataAndType(uri, context.contentResolver.getType(uri) ?: "image/*")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_image))
context.startActivity(shareIntent)
diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt b/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt
index 74e24e159..ccad1811d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt
@@ -7,6 +7,6 @@ import org.koin.android.ext.android.get
class RecentWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
- return RecentListFactory(this, get(), get())
+ return RecentListFactory(applicationContext, get(), get())
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt
index 3f590235a..89d0a8862 100644
--- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt
@@ -12,6 +12,6 @@ class ShelfWidgetService : RemoteViewsService() {
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
- return ShelfListFactory(this, get(), get(), widgetId)
+ return ShelfListFactory(applicationContext, get(), get(), widgetId)
}
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_favicon_fallback.xml b/app/src/main/res/drawable/ic_favicon_fallback.xml
new file mode 100644
index 000000000..24996b554
--- /dev/null
+++ b/app/src/main/res/drawable/ic_favicon_fallback.xml
@@ -0,0 +1,16 @@
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_source_config.xml b/app/src/main/res/layout/item_source_config.xml
index ffa9a68e5..a13432590 100644
--- a/app/src/main/res/layout/item_source_config.xml
+++ b/app/src/main/res/layout/item_source_config.xml
@@ -4,17 +4,18 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
- android:background="?android:windowBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
+ android:id="@+id/imageView_icon"
+ android:layout_width="?android:listPreferredItemHeightSmall"
+ android:layout_height="?android:listPreferredItemHeightSmall"
+ android:layout_marginHorizontal="?listPreferredItemPaddingStart"
+ android:labelFor="@id/textView_title"
+ android:padding="8dp"
+ android:scaleType="fitCenter"
+ tools:src="@tools:sample/avatars" />
-
-
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="?listPreferredItemPaddingEnd" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_source_config_draggable.xml b/app/src/main/res/layout/item_source_config_draggable.xml
new file mode 100644
index 000000000..ffa9a68e5
--- /dev/null
+++ b/app/src/main/res/layout/item_source_config_draggable.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_sources_empty.xml b/app/src/main/res/layout/item_sources_empty.xml
new file mode 100644
index 000000000..3aad1bbe4
--- /dev/null
+++ b/app/src/main/res/layout/item_sources_empty.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/opt_sources.xml b/app/src/main/res/menu/opt_sources.xml
index 35c7034be..5128fbe66 100644
--- a/app/src/main/res/menu/opt_sources.xml
+++ b/app/src/main/res/menu/opt_sources.xml
@@ -4,9 +4,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
+ android:id="@+id/action_search"
+ android:icon="@drawable/ic_search"
+ android:title="@string/search"
+ app:actionViewClass="androidx.appcompat.widget.SearchView"
+ app:showAsAction="ifRoom|collapseActionView" />
\ No newline at end of file