added search to extension list, and bugfix for certain extensions

This commit is contained in:
Finnley Somdahl 2023-10-18 00:28:27 -05:00
parent 7aa8b2db52
commit 3007e7d86e
7 changed files with 278 additions and 41 deletions

View file

@ -21,7 +21,7 @@ android {
minSdk 23 minSdk 23
targetSdk 34 targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger()) versionCode ((System.currentTimeMillis() / 60000).toInteger())
versionName "0.0.1" versionName "0.0.2"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }

View file

@ -21,6 +21,7 @@ import logcat.LogPriority
import rx.Observable import rx.Observable
import ani.dantotsu.aniyomi.util.logcat import ani.dantotsu.aniyomi.util.logcat
import ani.dantotsu.aniyomi.util.withUIContext 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 * 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 -> { else -> {
logcat(LogPriority.ERROR) { "Error loading anime extension: $result" } logcat(LogPriority.ERROR) { "Error loading anime extension: $result." }
} }
} }
} }

View file

@ -14,6 +14,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.SearchView
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -29,6 +31,7 @@ import ani.dantotsu.databinding.ActivityExtensionsBinding
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -47,16 +50,6 @@ class ExtensionsActivity : AppCompatActivity() {
animeExtensionManager.uninstallExtension(pkgName) animeExtensionManager.uninstallExtension(pkgName)
} }
private val allExtensionsAdapter = AllExtensionsAdapter(lifecycleScope) { 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 = val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -98,7 +91,6 @@ class ExtensionsActivity : AppCompatActivity() {
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -119,12 +111,44 @@ class ExtensionsActivity : AppCompatActivity() {
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
animeExtensionManager.availableExtensionsFlow.collect { extensions -> combine(
allExtensionsAdapter.updateData(extensions) 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) initActivity(this)
@ -180,9 +204,10 @@ class ExtensionsActivity : AppCompatActivity() {
private val onButtonClicked: (AnimeExtension.Available) -> Unit) : RecyclerView.Adapter<AllExtensionsAdapter.ViewHolder>() { private val onButtonClicked: (AnimeExtension.Available) -> Unit) : RecyclerView.Adapter<AllExtensionsAdapter.ViewHolder>() {
private var extensions: List<AnimeExtension.Available> = emptyList() private var extensions: List<AnimeExtension.Available> = emptyList()
fun updateData(newExtensions: List<AnimeExtension.Available>) { fun updateData(newExtensions: List<AnimeExtension.Available>, installedExtensions: List<AnimeExtension.Installed> = emptyList()) {
extensions = newExtensions val installedPkgNames = installedExtensions.map { it.pkgName }.toSet()
println("Extensions update: $extensions") extensions = newExtensions.filter { it.pkgName !in installedPkgNames }
filteredExtensions = extensions
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -193,7 +218,7 @@ class ExtensionsActivity : AppCompatActivity() {
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = extensions[position] val extension = filteredExtensions[position]
holder.extensionNameTextView.text = extension.name holder.extensionNameTextView.text = extension.name
coroutineScope.launch { coroutineScope.launch {
val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl) 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) { inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)

View 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 }
}

View file

@ -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) }

View file

@ -17,27 +17,42 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<androidx.cardview.widget.CardView <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:orientation="horizontal">
android:layout_marginTop="32dp"
app:cardBackgroundColor="@color/nav_bg_inv"
app:cardCornerRadius="16dp"
app:cardElevation="0dp">
<ImageButton <androidx.cardview.widget.CardView
android:id="@+id/settingsBack" android:layout_width="wrap_content"
android:layout_width="64dp" android:layout_height="wrap_content"
android:layout_height="64dp" android:layout_marginTop="32dp"
android:background="@color/nav_bg_inv" app:cardBackgroundColor="@color/nav_bg_inv"
android:padding="16dp" app:cardCornerRadius="16dp"
app:srcCompat="@drawable/ic_round_arrow_back_ios_new_24" app:cardElevation="0dp"
app:tint="@color/bg_opp" >
tools:ignore="ContentDescription,SpeakableTextPresentCheck" />
</androidx.cardview.widget.CardView> <ImageButton
android:id="@+id/settingsBack"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@color/nav_bg_inv"
android:padding="16dp"
app:srcCompat="@drawable/ic_round_arrow_back_ios_new_24"
app:tint="@color/bg_opp"
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 <LinearLayout
android:id="@+id/extensionsHeader"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@ -77,20 +92,20 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/extensionsRecyclerView" android:id="@+id/extensionsRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:layout_weight="1" android:layout_weight="1"
android:textSize="28sp" android:text="All Extensions"
android:text="All Extensions"/> android:textSize="28sp" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/allExtensionsRecyclerView" android:id="@+id/allExtensionsRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent" />
</LinearLayout> </LinearLayout>

View 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>