Skip to content

Commit

Permalink
[feature|optimize] Support downloading or opening images in the brows…
Browse files Browse the repository at this point in the history
…er from article content; support configuring player seek method; pptimize download page experience
  • Loading branch information
SkyD666 committed Aug 23, 2024
1 parent 2b57955 commit c8da826
Show file tree
Hide file tree
Showing 21 changed files with 361 additions and 9 deletions.
2 changes: 1 addition & 1 deletion 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-alpha18"
versionName = "2.1-alpha19"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down
32 changes: 32 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/DrawableExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 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()
)
}

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
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.skyd.anivu.model.preference.player.PlayerAutoPipPreference
import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference
import com.skyd.anivu.model.preference.player.PlayerMaxBackCacheSizePreference
import com.skyd.anivu.model.preference.player.PlayerMaxCacheSizePreference
import com.skyd.anivu.model.preference.player.PlayerSeekOptionPreference
import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference
import com.skyd.anivu.model.preference.player.PlayerShowScreenshotButtonPreference
import com.skyd.anivu.model.preference.proxy.ProxyHostnamePreference
Expand Down Expand Up @@ -101,6 +102,7 @@ fun Preferences.toSettings(): Settings {
playerAutoPip = PlayerAutoPipPreference.fromPreferences(this),
playerMaxCacheSize = PlayerMaxCacheSizePreference.fromPreferences(this),
playerMaxBackCacheSize = PlayerMaxBackCacheSizePreference.fromPreferences(this),
playerSeekOption = PlayerSeekOptionPreference.fromPreferences(this),

// Data
useAutoDelete = UseAutoDeletePreference.fromPreferences(this),
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/skyd/anivu/model/preference/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import com.skyd.anivu.model.preference.player.PlayerAutoPipPreference
import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference
import com.skyd.anivu.model.preference.player.PlayerMaxBackCacheSizePreference
import com.skyd.anivu.model.preference.player.PlayerMaxCacheSizePreference
import com.skyd.anivu.model.preference.player.PlayerSeekOptionPreference
import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference
import com.skyd.anivu.model.preference.player.PlayerShowScreenshotButtonPreference
import com.skyd.anivu.model.preference.proxy.ProxyHostnamePreference
Expand Down Expand Up @@ -83,6 +84,7 @@ import com.skyd.anivu.ui.local.LocalPlayerAutoPip
import com.skyd.anivu.ui.local.LocalPlayerDoubleTap
import com.skyd.anivu.ui.local.LocalPlayerMaxBackCacheSize
import com.skyd.anivu.ui.local.LocalPlayerMaxCacheSize
import com.skyd.anivu.ui.local.LocalPlayerSeekOption
import com.skyd.anivu.ui.local.LocalPlayerShow85sButton
import com.skyd.anivu.ui.local.LocalPlayerShowScreenshotButton
import com.skyd.anivu.ui.local.LocalProxyHostname
Expand Down Expand Up @@ -150,6 +152,7 @@ data class Settings(
val playerAutoPip: Boolean = PlayerAutoPipPreference.default,
val playerMaxCacheSize: Long = PlayerMaxCacheSizePreference.default,
val playerMaxBackCacheSize: Long = PlayerMaxBackCacheSizePreference.default,
val playerSeekOption: String = PlayerSeekOptionPreference.default,
// Data
val useAutoDelete: Boolean = UseAutoDeletePreference.default,
val autoDeleteArticleFrequency: Long = AutoDeleteArticleFrequencyPreference.default,
Expand Down Expand Up @@ -217,6 +220,7 @@ fun SettingsProvider(
LocalPlayerAutoPip provides settings.playerAutoPip,
LocalPlayerMaxCacheSize provides settings.playerMaxCacheSize,
LocalPlayerMaxBackCacheSize provides settings.playerMaxBackCacheSize,
LocalPlayerSeekOption provides settings.playerSeekOption,
// Data
LocalUseAutoDelete provides settings.useAutoDelete,
LocalAutoDeleteArticleFrequency provides settings.autoDeleteArticleFrequency,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.skyd.anivu.model.preference.player

import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import com.skyd.anivu.R
import com.skyd.anivu.base.BasePreference
import com.skyd.anivu.ext.dataStore
import com.skyd.anivu.ext.getOrDefault
import com.skyd.anivu.ext.put
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

object PlayerSeekOptionPreference : BasePreference<String> {
private const val PLAYER_SEEK_OPTION = "playerSeekOption"

const val FAST = "Fast"
const val EXACT = "Exact"

val values = arrayOf(FAST, EXACT)

override val default = FAST

val key = stringPreferencesKey(PLAYER_SEEK_OPTION)

fun put(context: Context, scope: CoroutineScope, value: String) {
scope.launch(Dispatchers.IO) {
context.dataStore.put(key, value)
}
}

override fun fromPreferences(preferences: Preferences): String = preferences[key] ?: default

fun isPrecise(
context: Context,
value: String = context.dataStore.getOrDefault(this),
): Boolean = value == EXACT

fun toDisplayName(
context: Context,
value: String = context.dataStore.getOrDefault(this),
): String = when (value) {
FAST -> context.getString(R.string.player_seek_fast)
EXACT -> context.getString(R.string.player_seek_exact)
else -> context.getString(R.string.unknown)
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
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 @@ -26,6 +34,7 @@ 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 @@ -133,6 +142,28 @@ 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
14 changes: 12 additions & 2 deletions app/src/main/java/com/skyd/anivu/ui/component/html/HtmlText.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.skyd.anivu.ui.component.html

import android.text.Html
import android.text.method.LinkMovementMethod
import android.widget.TextView
import androidx.compose.material3.LocalContentColor
Expand All @@ -21,6 +22,7 @@ fun HtmlText(
modifier: Modifier = Modifier,
htmlFlags: Int = FROM_HTML_MODE_LEGACY,
text: String,
onImageClick: ((String) -> Unit)? = null,
) {
val context = LocalContext.current
val textColor = LocalContentColor.current
Expand All @@ -31,8 +33,10 @@ fun HtmlText(
},
factory = { c ->
TextView(c).apply {
movementMethod = LinkMovementMethod.getInstance()
// setTextIsSelectable should come before movementMethod,
// otherwise movementMethod is invalid.
setTextIsSelectable(true)
movementMethod = LinkMovementMethod.getInstance()
setTextColor(textColor.toArgb())
}
},
Expand All @@ -46,7 +50,13 @@ fun HtmlText(
textView.text = textView.text
}
),
tagHandler = null,
tagHandler = TagHandler(
handlers = mutableListOf<Html.TagHandler>().apply {
if (onImageClick != null) {
add(ImgTagHandler(onImageClick))
}
}
),
)
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.skyd.anivu.ui.component.html

import android.text.Editable
import android.text.Html.TagHandler
import android.text.Spanned
import android.text.style.ClickableSpan
import android.text.style.ImageSpan
import android.view.View
import org.xml.sax.XMLReader

internal class ImgTagHandler(private val onClick: (String) -> Unit) : TagHandler {
override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) {
if (tag.equals("img", ignoreCase = true)) {
val len = output.length
val images = output.getSpans(0, len, ImageSpan::class.java)
val imgURL = images.firstOrNull()?.source ?: return
output.setSpan(
ClickableImage(imgURL, onClick),
0,
len,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE,
)
}
}
}

internal class ClickableImage(
private val url: String,
private val onClick: (String) -> Unit,
) : ClickableSpan() {
override fun onClick(widget: View) {
onClick(url)
}
}
11 changes: 11 additions & 0 deletions app/src/main/java/com/skyd/anivu/ui/component/html/TagHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.skyd.anivu.ui.component.html

import android.text.Editable
import android.text.Html.TagHandler
import org.xml.sax.XMLReader

internal class TagHandler(private val handlers: List<TagHandler>) : TagHandler {
override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) {
handlers.forEach { it.handleTag(opening, tag, output, xmlReader) }
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.skyd.anivu.model.preference.player.PlayerAutoPipPreference
import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference
import com.skyd.anivu.model.preference.player.PlayerMaxBackCacheSizePreference
import com.skyd.anivu.model.preference.player.PlayerMaxCacheSizePreference
import com.skyd.anivu.model.preference.player.PlayerSeekOptionPreference
import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference
import com.skyd.anivu.model.preference.player.PlayerShowScreenshotButtonPreference
import com.skyd.anivu.model.preference.proxy.ProxyHostnamePreference
Expand Down Expand Up @@ -116,6 +117,7 @@ val LocalHardwareDecode = compositionLocalOf { HardwareDecodePreference.default
val LocalPlayerAutoPip = compositionLocalOf { PlayerAutoPipPreference.default }
val LocalPlayerMaxCacheSize = compositionLocalOf { PlayerMaxCacheSizePreference.default }
val LocalPlayerMaxBackCacheSize = compositionLocalOf { PlayerMaxBackCacheSizePreference.default }
val LocalPlayerSeekOption = compositionLocalOf { PlayerSeekOptionPreference.default }

// Data
val LocalUseAutoDelete = compositionLocalOf { UseAutoDeletePreference.default }
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.skyd.anivu.ext.getOrDefault
import com.skyd.anivu.model.preference.player.HardwareDecodePreference
import com.skyd.anivu.model.preference.player.PlayerMaxBackCacheSizePreference
import com.skyd.anivu.model.preference.player.PlayerMaxCacheSizePreference
import com.skyd.anivu.model.preference.player.PlayerSeekOptionPreference
import com.skyd.anivu.ui.mpv.controller.bar.toDurationString
import `is`.xyz.mpv.MPVLib
import `is`.xyz.mpv.MPVLib.mpvFormat.MPV_FORMAT_DOUBLE
Expand Down Expand Up @@ -487,7 +488,7 @@ class MPVView(context: Context, attrs: AttributeSet?) : SurfaceView(context, att
MPVLib.command(arrayOf("stop"))
}

fun seek(position: Int, precise: Boolean = false) {
fun seek(position: Int, precise: Boolean = PlayerSeekOptionPreference.isPrecise(context)) {
if (precise) {
timePos = position
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ fun DownloadItem(
}
},
imageVector = pauseButtonIcon,
contentDescription = pauseButtonContentDescription,
)
AniVuIconButton(
enabled = cancelButtonEnabled,
Expand All @@ -191,6 +192,7 @@ fun DownloadItem(
cancelButtonEnabled = false
},
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(id = R.string.delete)
)
}
ProgressIndicator(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package com.skyd.anivu.ui.screen.download
import android.os.Bundle
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Download
Expand All @@ -21,6 +23,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -104,6 +109,7 @@ fun DownloadScreen(downloadLink: String? = null, viewModel: DownloadViewModel =

is DownloadListState.Success -> DownloadList(
downloadInfoBeanList = downloadListState.downloadInfoBeanList,
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
contentPadding = paddingValues + PaddingValues(bottom = fabHeight + 16.dp)
)
}
Expand Down Expand Up @@ -137,11 +143,15 @@ fun DownloadScreen(downloadLink: String? = null, viewModel: DownloadViewModel =
@Composable
private fun DownloadList(
downloadInfoBeanList: List<DownloadInfoBean>,
nestedScrollConnection: NestedScrollConnection,
contentPadding: PaddingValues,
) {
if (downloadInfoBeanList.isNotEmpty()) {
val context = LocalContext.current
LazyColumn(contentPadding = contentPadding) {
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection),
contentPadding = contentPadding,
) {
itemsIndexed(
items = downloadInfoBeanList,
key = { _, item -> item.link },
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/skyd/anivu/ui/screen/read/ReadEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ sealed interface ReadEvent : MviSingleEvent {
sealed interface ReadArticleResultEvent : ReadEvent {
data class Failed(val msg: String) : ReadArticleResultEvent
}

sealed interface DownloadImageResultEvent : ReadEvent {
data class Success(val url: String) : ReadArticleResultEvent
data class Failed(val msg: String) : ReadArticleResultEvent
}
}
Loading

0 comments on commit c8da826

Please sign in to comment.