Skip to content

Commit

Permalink
[feature|optimize|build] Added GIF support on the reading page; optim…
Browse files Browse the repository at this point in the history
…ize image download code on the reading page; update dependencies
  • Loading branch information
SkyD666 committed Aug 24, 2024
1 parent c8da826 commit 44909d1
Show file tree
Hide file tree
Showing 21 changed files with 654 additions and 117 deletions.
16 changes: 8 additions & 8 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ android {
minSdk = 24
targetSdk = 35
versionCode = 22
versionName = "2.1-alpha19"
versionName = "2.1-alpha21"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down Expand Up @@ -163,13 +163,13 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha14")
implementation("androidx.navigation:navigation-compose:2.7.7")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.4")
implementation("androidx.compose.ui:ui:1.7.0-beta07")
implementation("androidx.compose.material:material:1.7.0-beta07")
implementation("androidx.compose.material3:material3:1.3.0-beta05")
implementation("androidx.compose.material3:material3-window-size-class:1.3.0-beta05")
implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-beta04")
implementation("androidx.compose.material3.adaptive:adaptive-layout:1.0.0-beta04")
implementation("androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-beta04")
implementation("androidx.compose.ui:ui:1.7.0-rc01")
implementation("androidx.compose.material:material:1.7.0-rc01")
implementation("androidx.compose.material3:material3:1.3.0-rc01")
implementation("androidx.compose.material3:material3-window-size-class:1.3.0-rc01")
implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-rc01")
implementation("androidx.compose.material3.adaptive:adaptive-layout:1.0.0-rc01")
implementation("androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-rc01")
implementation("androidx.compose.material:material-icons-extended:1.6.8")
implementation("com.materialkolor:material-kolor:1.7.0")
implementation("androidx.room:room-runtime:2.6.1")
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/skyd/anivu/config/Const.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ object Const {
.apply { if (!exists()) mkdirs() }

val PICTURES_DIR = appContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
val ANIVU_PICTURES_DIR = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"AniVu"
).apply { if (!exists()) mkdirs() }

val INTERNAL_STORAGE: String = Environment.getExternalStorageDirectory().absolutePath
}
102 changes: 102 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/ArrayExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.skyd.anivu.ext

import kotlin.math.max

/**
* Boyer–Moore string-search algorithm
*
* Returns the index within this string of the first occurrence of the
* specified substring. If it is not a substring, return -1.
*
* There is no Galil because it only generates one match.
*
* @param needle The target string to search
* @return The start index of the substring
*/
fun <T> Array<T>.indexOf(needle: Array<T>): Int {
if (needle.isEmpty()) {
return 0
}
val charTable = makeCharTable(needle)
val offsetTable = makeOffsetTable(needle)
var i = needle.size - 1
var j: Int
while (i < this.size) {
j = needle.size - 1
while (needle[j] == this[i]) {
if (j == 0) return i
i--
j--
}
// i += needle.length - j; // For naive method
i += max(offsetTable[needle.size - 1 - j], charTable[this[i]]!!)
}
return -1
}

/**
* Makes the jump table based on the mismatched character information.
* (bad-character rule)
*/
private fun <T> makeCharTable(needle: Array<T>): HashMap<T, Int> {
val table = hashMapOf<T, Int>()
for (i in needle) {
table[i] = needle.size
}
for (i in needle.indices) {
table[needle[i]] = needle.size - 1 - i
}
return table
}

/**
* Makes the jump table based on the scan offset which mismatch occurs.
* (good suffix rule)
*/
private fun <T> makeOffsetTable(needle: Array<T>): IntArray {
val table = IntArray(needle.size)
var lastPrefixPosition = needle.size
for (i in needle.size downTo 1) {
if (isPrefix(needle, i)) {
lastPrefixPosition = i
}
table[needle.size - i] = lastPrefixPosition - i + needle.size
}
for (i in 0 until needle.size - 1) {
val slen = suffixLength(needle, i)
table[slen] = needle.size - 1 - i + slen
}
return table
}

/**
* Is needle[p:end] a prefix of needle?
*/
private fun <T> isPrefix(needle: Array<T>, p: Int): Boolean {
var i = p
var j = 0
while (i < needle.size) {
if (needle[i] != needle[j]) {
return false
}
i++
j++
}
return true
}

/**
* Returns the maximum length of the substring ends at p and is a suffix.
* (good-suffix rule)
*/
private fun <T> suffixLength(needle: Array<T>, p: Int): Int {
var len = 0
var i = p
var j = needle.size - 1
while (i >= 0 && needle[i] == needle[j]) {
len += 1
--i
--j
}
return len
}
19 changes: 18 additions & 1 deletion app/src/main/java/com/skyd/anivu/ext/ContextExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Point
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.view.Window
import androidx.core.content.ContextCompat
import androidx.core.content.pm.PackageInfoCompat
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder

val Context.activity: Activity
get() {
Expand Down Expand Up @@ -66,7 +71,7 @@ fun Context.screenWidth(includeVirtualKey: Boolean): Int {
fun Context.getAppVersionName(): String {
var appVersionName = ""
try {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val packageInfo = if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0L))
} else {
packageManager.getPackageInfo(packageName, 0)
Expand Down Expand Up @@ -105,4 +110,16 @@ fun Context.getAppName(): String? {
fun Context.inDarkMode(): Boolean {
return (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
}

fun Context.imageLoaderBuilder(): ImageLoader.Builder {
return ImageLoader.Builder(this)
.components {
if (SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
add(SvgDecoder.Factory())
}
}
32 changes: 14 additions & 18 deletions app/src/main/java/com/skyd/anivu/ext/DrawableExt.kt
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
package com.skyd.anivu.ext

import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.os.Environment
import android.provider.MediaStore
import android.os.Build
import com.skyd.anivu.config.Const
import com.skyd.anivu.ext.content.saveToGallery
import kotlin.random.Random

fun BitmapDrawable.saveToGallery(
context: Context,
filename: String = System.currentTimeMillis().toString() + "_" + Random.nextInt(),
): Boolean {
val contentResolver = context.contentResolver

val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$filename.png")
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
put(
MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + "/" + context.getAppName()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
context.contentResolver.saveToGallery(
fileNameWithExt = "$filename.png",
mimetype = "image/png",
output = { outputStream ->
bitmap?.compress(Bitmap.CompressFormat.PNG, 100, outputStream) == true
}
)
} else {
Const.ANIVU_PICTURES_DIR.outputStream().use { outputStream ->
bitmap?.compress(Bitmap.CompressFormat.PNG, 100, outputStream) == true
}
}

val uri = contentResolver
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return false
contentResolver.openOutputStream(uri)?.use { outputStream ->
val out = bitmap?.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
return out == true
} ?: return false
}
40 changes: 16 additions & 24 deletions app/src/main/java/com/skyd/anivu/ext/FileExt.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package com.skyd.anivu.ext

import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.skyd.anivu.R
import com.skyd.anivu.config.Const.ANIVU_PICTURES_DIR
import com.skyd.anivu.ext.content.saveToGallery
import com.skyd.anivu.ui.component.showToast
import java.io.File

Expand Down Expand Up @@ -38,30 +37,23 @@ fun File.getMimeType(): String? {
return type
}

fun File.savePictureToMediaStore(context: Context, autoDelete: Boolean = true) {
fun File.savePictureToMediaStore(
context: Context,
mimetype: String? = null,
fileName: String = name,
autoDelete: Boolean = true,
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues()
contentValues.put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/AniVu",
)
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, name)
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)!!
context.contentResolver.openOutputStream(uri)?.use { output ->
inputStream().use { input ->
input.copyTo(output)
context.contentResolver.saveToGallery(
fileNameWithExt = fileName,
mimetype = mimetype,
output = { output ->
inputStream().use { input -> input.copyTo(output) }
true
}
}
} else {
val dir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"AniVu"
)
if (!dir.exists()) dir.mkdirs()
this.copyTo(File(dir, name))
} else {
this.copyTo(File(ANIVU_PICTURES_DIR, fileName))
}
context.getString(R.string.save_picture_to_media_store_saved).showToast()
if (autoDelete) delete()
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/content/ContentResolverExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.skyd.anivu.ext.content

import android.content.ContentResolver
import android.content.ContentValues
import android.provider.MediaStore
import java.io.OutputStream

fun ContentResolver.saveToGallery(
fileNameWithExt: String,
mimetype: String? = null,
output: (OutputStream) -> Boolean,
): Boolean {
val contentValues = ContentValues().gallery(fileNameWithExt, mimetype)
val uri = insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
) ?: return false
return openOutputStream(uri)?.use(output) ?: false
}
19 changes: 19 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/content/ContentValuesExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.skyd.anivu.ext.content

import android.content.ContentValues
import android.os.Environment
import android.provider.MediaStore

fun ContentValues.gallery(
fileNameWithExt: String,
mimetype: String? = null,
) = apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileNameWithExt)
if (mimetype != null) {
put(MediaStore.Images.Media.MIME_TYPE, mimetype)
}
put(
MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + "/AniVu"
)
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
package com.skyd.anivu.model.repository

import android.database.DatabaseUtils
import android.graphics.drawable.BitmapDrawable
import android.os.Parcelable
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.sqlite.db.SimpleSQLiteQuery
import coil.Coil
import coil.request.ErrorResult
import coil.request.ImageRequest
import coil.request.SuccessResult
import com.skyd.anivu.appContext
import com.skyd.anivu.base.BaseRepository
import com.skyd.anivu.ext.saveToGallery
import com.skyd.anivu.ext.validateFileName
import com.skyd.anivu.model.bean.ARTICLE_TABLE_NAME
import com.skyd.anivu.model.bean.ArticleBean
import com.skyd.anivu.model.bean.ArticleWithFeed
Expand All @@ -34,7 +26,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
import kotlin.random.Random

@Parcelize
sealed class ArticleSort(open val asc: Boolean) : Parcelable {
Expand Down Expand Up @@ -142,28 +133,6 @@ class ArticleRepository @Inject constructor(
}.flowOn(Dispatchers.IO)
}

fun downloadImage(url: String, title: String?): Flow<Unit> {
return flow {
val request = ImageRequest.Builder(appContext)
.data(url)
.build()
when (val result = Coil.imageLoader(appContext).execute(request)) {
is ErrorResult -> throw result.throwable
is SuccessResult -> {
check(
(result.drawable as? BitmapDrawable)?.saveToGallery(
context = appContext,
filename = (title.orEmpty()
.ifEmpty { url.substringAfterLast('/') } +
"_" + Random.nextInt()).validateFileName(),
) ?: false
) { "saveToGallery failed" }
}
}
emit(Unit)
}.flowOn(Dispatchers.IO)
}

companion object {
fun genSql(
feedUrls: List<String>,
Expand Down
Loading

0 comments on commit 44909d1

Please sign in to comment.