diff --git a/app/build.gradle b/app/build.gradle index e1fa4462..6e4f3192 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 } diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/AnimeExtensionManager.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/AnimeExtensionManager.kt index 7435a711..c77120f9 100644 --- a/app/src/main/java/ani/dantotsu/aniyomi/anime/AnimeExtensionManager.kt +++ b/app/src/main/java/ani/dantotsu/aniyomi/anime/AnimeExtensionManager.kt @@ -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." } } } } diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index efe837f1..76c6c47a 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -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() { private var extensions: List = emptyList() - fun updateData(newExtensions: List) { - extensions = newExtensions - println("Extensions update: $extensions") + fun updateData(newExtensions: List, installedExtensions: List = 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 = 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppInfo.kt b/app/src/main/java/eu/kanade/tachiyomi/AppInfo.kt new file mode 100644 index 00000000..dfcde476 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/AppInfo.kt @@ -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 = ImageUtil.ImageType.values().map { it.mime } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/core/util/system/ImageUtil.kt new file mode 100644 index 00000000..b88f017b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/core/util/system/ImageUtil.kt @@ -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) } diff --git a/app/src/main/res/layout/activity_extensions.xml b/app/src/main/res/layout/activity_extensions.xml index d85e154a..790fdf63 100644 --- a/app/src/main/res/layout/activity_extensions.xml +++ b/app/src/main/res/layout/activity_extensions.xml @@ -17,27 +17,42 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + android:orientation="horizontal"> - - + + + + + + + + android:layout_height="match_parent" /> + android:text="All Extensions" + android:textSize="28sp" /> + android:layout_height="match_parent" /> diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..a3709d3f --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + +