added search to extension list, and bugfix for certain extensions
This commit is contained in:
parent
7aa8b2db52
commit
3007e7d86e
7 changed files with 278 additions and 41 deletions
|
@ -21,7 +21,7 @@ android {
|
|||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "0.0.1"
|
||||
versionName "0.0.2"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import logcat.LogPriority
|
|||
import rx.Observable
|
||||
import ani.dantotsu.aniyomi.util.logcat
|
||||
import ani.dantotsu.aniyomi.util.withUIContext
|
||||
import ani.dantotsu.logger
|
||||
|
||||
/**
|
||||
* The manager of anime extensions installed as another apk which extend the available sources. It handles
|
||||
|
@ -116,11 +117,13 @@ class AnimeExtensionManager(
|
|||
}*/
|
||||
|
||||
|
||||
}else{
|
||||
println("${sc.name} is not AnimeCatalogueSource")
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
logcat(LogPriority.ERROR) { "Error loading anime extension: $result" }
|
||||
logcat(LogPriority.ERROR) { "Error loading anime extension: $result." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.SearchView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -29,6 +31,7 @@ import ani.dantotsu.databinding.ActivityExtensionsBinding
|
|||
import com.bumptech.glide.Glide
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
@ -47,16 +50,6 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||
animeExtensionManager.uninstallExtension(pkgName)
|
||||
}
|
||||
private val allExtensionsAdapter = AllExtensionsAdapter(lifecycleScope) { pkgName ->
|
||||
if (SDK_INT >= VERSION_CODES.O) {
|
||||
// If we don't have permission to install unknown apps, request it
|
||||
if (!packageManager.canRequestPackageInstalls()) {
|
||||
startActivityForResult(
|
||||
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(
|
||||
Uri.parse("package:$packageName")
|
||||
), 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val notificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
@ -98,7 +91,6 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -119,12 +111,44 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
animeExtensionManager.availableExtensionsFlow.collect { extensions ->
|
||||
allExtensionsAdapter.updateData(extensions)
|
||||
combine(
|
||||
animeExtensionManager.availableExtensionsFlow,
|
||||
animeExtensionManager.installedExtensionsFlow
|
||||
) { availableExtensions, installedExtensions ->
|
||||
// Pair of available and installed extensions
|
||||
Pair(availableExtensions, installedExtensions)
|
||||
}.collect { pair ->
|
||||
val (availableExtensions, installedExtensions) = pair
|
||||
allExtensionsAdapter.updateData(availableExtensions, installedExtensions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val searchView: SearchView = findViewById(R.id.searchView)
|
||||
val extensionsRecyclerView: RecyclerView = findViewById(R.id.extensionsRecyclerView)
|
||||
val extensionsHeader: LinearLayout = findViewById(R.id.extensionsHeader)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
if (newText.isNullOrEmpty()) {
|
||||
allExtensionsAdapter.filter("") // Reset the filter
|
||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
||||
extensionsHeader.visibility = View.VISIBLE
|
||||
extensionsRecyclerView.visibility = View.VISIBLE
|
||||
} else {
|
||||
allExtensionsAdapter.filter(newText)
|
||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
||||
extensionsRecyclerView.visibility = View.GONE
|
||||
extensionsHeader.visibility = View.GONE
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
initActivity(this)
|
||||
|
||||
|
||||
|
@ -180,9 +204,10 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||
private val onButtonClicked: (AnimeExtension.Available) -> Unit) : RecyclerView.Adapter<AllExtensionsAdapter.ViewHolder>() {
|
||||
private var extensions: List<AnimeExtension.Available> = emptyList()
|
||||
|
||||
fun updateData(newExtensions: List<AnimeExtension.Available>) {
|
||||
extensions = newExtensions
|
||||
println("Extensions update: $extensions")
|
||||
fun updateData(newExtensions: List<AnimeExtension.Available>, installedExtensions: List<AnimeExtension.Installed> = emptyList()) {
|
||||
val installedPkgNames = installedExtensions.map { it.pkgName }.toSet()
|
||||
extensions = newExtensions.filter { it.pkgName !in installedPkgNames }
|
||||
filteredExtensions = extensions
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
@ -193,7 +218,7 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = extensions[position]
|
||||
val extension = filteredExtensions[position]
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
coroutineScope.launch {
|
||||
val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl)
|
||||
|
@ -205,7 +230,18 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = extensions.size
|
||||
override fun getItemCount(): Int = filteredExtensions.size
|
||||
|
||||
private var filteredExtensions: List<AnimeExtension.Available> = emptyList()
|
||||
|
||||
fun filter(query: String) {
|
||||
filteredExtensions = if (query.isEmpty()) {
|
||||
extensions
|
||||
} else {
|
||||
extensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||
|
|
33
app/src/main/java/eu/kanade/tachiyomi/AppInfo.kt
Normal file
33
app/src/main/java/eu/kanade/tachiyomi/AppInfo.kt
Normal file
|
@ -0,0 +1,33 @@
|
|||
package eu.kanade.tachiyomi
|
||||
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
|
||||
/**
|
||||
* Used by extensions.
|
||||
*/
|
||||
@Suppress("UNUSED")
|
||||
object AppInfo {
|
||||
/**
|
||||
* Version code of the host application. May be useful for sharing as User-Agent information.
|
||||
* Note that this value differs between forks so logic should not rely on it.
|
||||
*
|
||||
* @since extension-lib 1.3
|
||||
*/
|
||||
fun getVersionCode(): Int = ani.dantotsu.BuildConfig.VERSION_CODE
|
||||
|
||||
/**
|
||||
* Version name of the host application. May be useful for sharing as User-Agent information.
|
||||
* Note that this value differs between forks so logic should not rely on it.
|
||||
*
|
||||
* @since extension-lib 1.3
|
||||
*/
|
||||
fun getVersionName(): String = ani.dantotsu.BuildConfig.VERSION_NAME
|
||||
|
||||
/**
|
||||
* A list of supported image MIME types by the reader.
|
||||
* e.g. ["image/jpeg", "image/png", ...]
|
||||
*
|
||||
* @since extension-lib 1.5
|
||||
*/
|
||||
fun getSupportedImageMimeTypes(): List<String> = ImageUtil.ImageType.values().map { it.mime }
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package tachiyomi.core.util.system
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.BitmapRegionDecoder
|
||||
import android.graphics.Color
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.get
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import com.hippo.unifile.UniFile
|
||||
import logcat.LogPriority
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URLConnection
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object ImageUtil {
|
||||
|
||||
|
||||
enum class ImageType(val mime: String, val extension: String) {
|
||||
AVIF("image/avif", "avif"),
|
||||
GIF("image/gif", "gif"),
|
||||
HEIF("image/heif", "heif"),
|
||||
JPEG("image/jpeg", "jpg"),
|
||||
JXL("image/jxl", "jxl"),
|
||||
PNG("image/png", "png"),
|
||||
WEBP("image/webp", "webp"),
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the 'side' part from imageStream and return it as InputStream.
|
||||
*/
|
||||
fun splitInHalf(imageStream: InputStream, side: Side): InputStream {
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
val height = imageBitmap.height
|
||||
val width = imageBitmap.width
|
||||
|
||||
val singlePage = Rect(0, 0, width / 2, height)
|
||||
|
||||
val half = createBitmap(width / 2, height)
|
||||
val part = when (side) {
|
||||
Side.RIGHT -> Rect(width - width / 2, 0, width, height)
|
||||
Side.LEFT -> Rect(0, 0, width / 2, height)
|
||||
}
|
||||
half.applyCanvas {
|
||||
drawBitmap(imageBitmap, part, singlePage, null)
|
||||
}
|
||||
val output = ByteArrayOutputStream()
|
||||
half.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
|
||||
return ByteArrayInputStream(output.toByteArray())
|
||||
}
|
||||
|
||||
fun rotateImage(imageStream: InputStream, degrees: Float): InputStream {
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
val rotated = rotateBitMap(imageBitmap, degrees)
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
rotated.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
|
||||
return ByteArrayInputStream(output.toByteArray())
|
||||
}
|
||||
|
||||
private fun rotateBitMap(bitmap: Bitmap, degrees: Float): Bitmap {
|
||||
val matrix = Matrix().apply { postRotate(degrees) }
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the image into left and right parts, then merge them into a new image.
|
||||
*/
|
||||
fun splitAndMerge(imageStream: InputStream, upperSide: Side): InputStream {
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
val height = imageBitmap.height
|
||||
val width = imageBitmap.width
|
||||
|
||||
val result = createBitmap(width / 2, height * 2)
|
||||
result.applyCanvas {
|
||||
// right -> upper
|
||||
val rightPart = when (upperSide) {
|
||||
Side.RIGHT -> Rect(width - width / 2, 0, width, height)
|
||||
Side.LEFT -> Rect(0, 0, width / 2, height)
|
||||
}
|
||||
val upperPart = Rect(0, 0, width / 2, height)
|
||||
drawBitmap(imageBitmap, rightPart, upperPart, null)
|
||||
// left -> bottom
|
||||
val leftPart = when (upperSide) {
|
||||
Side.LEFT -> Rect(width - width / 2, 0, width, height)
|
||||
Side.RIGHT -> Rect(0, 0, width / 2, height)
|
||||
}
|
||||
val bottomPart = Rect(0, height, width / 2, height * 2)
|
||||
drawBitmap(imageBitmap, leftPart, bottomPart, null)
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
return ByteArrayInputStream(output.toByteArray())
|
||||
}
|
||||
|
||||
enum class Side {
|
||||
RIGHT,
|
||||
LEFT,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val getDisplayMaxHeightInPx: Int
|
||||
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
|
|
@ -17,14 +17,19 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="32dp"
|
||||
app:cardBackgroundColor="@color/nav_bg_inv"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
app:cardElevation="0dp"
|
||||
>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/settingsBack"
|
||||
|
@ -37,7 +42,17 @@
|
|||
tools:ignore="ContentDescription,SpeakableTextPresentCheck" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<SearchView
|
||||
android:id="@+id/searchView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layoutDirection="rtl"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/extensionsHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
|
@ -77,20 +92,20 @@
|
|||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/extensionsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_weight="1"
|
||||
android:textSize="28sp"
|
||||
android:text="All Extensions"/>
|
||||
android:text="All Extensions"
|
||||
android:textSize="28sp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/allExtensionsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
17
app/src/main/res/xml/network_security_config.xml
Normal file
17
app/src/main/res/xml/network_security_config.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Need to allow cleartext traffic for some sources -->
|
||||
<base-config
|
||||
cleartextTrafficPermitted="true"
|
||||
tools:ignore="InsecureBaseConfiguration">
|
||||
<trust-anchors>
|
||||
<!-- Trust preinstalled CAs -->
|
||||
<certificates src="system" />
|
||||
|
||||
<!-- Additionally trust user added CAs -->
|
||||
<certificates
|
||||
src="user"
|
||||
tools:ignore="AcceptsUserCertificates" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
Loading…
Add table
Add a link
Reference in a new issue