feat: download section

This commit is contained in:
rhunk
2023-08-05 19:34:31 +02:00
parent e6e75123f8
commit 7b6b0bfd70
18 changed files with 386 additions and 93 deletions

View File

@ -91,13 +91,16 @@ android {
dependencies {
implementation(project(":core"))
implementation(libs.androidx.material.icons.core)
implementation(libs.androidx.material.ripple)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.material3)
implementation(libs.androidx.material)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.documentfile)
implementation(libs.gson)
implementation(libs.coil.compose)
implementation(libs.coil.video)
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")

View File

@ -16,16 +16,15 @@ import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.RemoteSideContext
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.bridge.DownloadCallback
import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.download.data.DownloadMediaType
import me.rhunk.snapenhance.download.data.DownloadMetadata
import me.rhunk.snapenhance.download.data.DownloadObject
import me.rhunk.snapenhance.download.data.DownloadRequest
import me.rhunk.snapenhance.download.data.DownloadStage
import me.rhunk.snapenhance.download.data.InputMedia
import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
import me.rhunk.snapenhance.download.data.PendingDownload
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
import java.io.File
@ -60,7 +59,7 @@ class DownloadProcessor (
) {
private val translation by lazy {
SharedContext.translation.getCategory("download_processor")
remoteSideContext.translation.getCategory("download_processor")
}
private val gson by lazy {
@ -118,7 +117,7 @@ class DownloadProcessor (
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) {
private suspend fun saveMediaToGallery(inputFile: File, downloadObject: DownloadObject) {
if (coroutineContext.job.isCancelled) return
runCatching {
@ -128,12 +127,12 @@ class DownloadProcessor (
return
}
val fileName = pendingDownload.metadata.outputPath.substringAfterLast("/") + "." + fileType.fileExtension
val fileName = downloadObject.metadata.outputPath.substringAfterLast("/") + "." + fileType.fileExtension
val outputFolder = DocumentFile.fromTreeUri(remoteSideContext.androidContext, Uri.parse(remoteSideContext.config.root.downloader.saveFolder.get()))
?: throw Exception("Failed to open output folder")
val outputFileFolder = pendingDownload.metadata.outputPath.let {
val outputFileFolder = downloadObject.metadata.outputPath.let {
if (it.contains("/")) {
it.substringBeforeLast("/").split("/").fold(outputFolder) { folder, name ->
folder.findFile(name) ?: folder.createDirectory(name)!!
@ -150,8 +149,8 @@ class DownloadProcessor (
inputStream.copyTo(outputStream)
}
pendingDownload.outputFile = outputFile.uri.toString()
pendingDownload.downloadStage = DownloadStage.SAVED
downloadObject.outputFile = outputFile.uri.toString()
downloadObject.downloadStage = DownloadStage.SAVED
runCatching {
val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE")
@ -167,7 +166,7 @@ class DownloadProcessor (
}.onFailure { exception ->
Logger.error(exception)
callbackOnFailure(translation.format("failed_gallery_toast", "error" to exception.toString()), exception.message)
pendingDownload.downloadStage = DownloadStage.FAILED
downloadObject.downloadStage = DownloadStage.FAILED
}
}
@ -226,13 +225,13 @@ class DownloadProcessor (
downloadedMedias
}
private suspend fun downloadRemoteMedia(pendingDownloadObject: PendingDownload, downloadedMedias: Map<InputMedia, DownloadedFile>, downloadRequest: DownloadRequest) {
private suspend fun downloadRemoteMedia(downloadObjectObject: DownloadObject, downloadedMedias: Map<InputMedia, DownloadedFile>, downloadRequest: DownloadRequest) {
downloadRequest.inputMedias.first().let { inputMedia ->
val mediaType = inputMedia.type
val media = downloadedMedias[inputMedia]!!
if (!downloadRequest.isDashPlaylist) {
saveMediaToGallery(media.file, pendingDownloadObject)
saveMediaToGallery(media.file, downloadObjectObject)
media.file.delete()
return
}
@ -261,12 +260,12 @@ class DownloadProcessor (
output = outputFile,
startTime = dashOptions.offsetTime,
duration = dashOptions.duration)
saveMediaToGallery(outputFile, pendingDownloadObject)
saveMediaToGallery(outputFile, downloadObjectObject)
}.onFailure { exception ->
if (coroutineContext.job.isCancelled) return@onFailure
Logger.error(exception)
callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message)
pendingDownloadObject.downloadStage = DownloadStage.FAILED
downloadObjectObject.downloadStage = DownloadStage.FAILED
}
dashPlaylistFile.delete()
@ -297,11 +296,11 @@ class DownloadProcessor (
return@launch
}
val pendingDownloadObject = PendingDownload(
val downloadObjectObject = DownloadObject(
metadata = downloadMetadata
).apply { downloadTaskManager = remoteSideContext.downloadTaskManager }
pendingDownloadObject.also {
downloadObjectObject.also {
remoteSideContext.downloadTaskManager.addTask(it)
}.apply {
job = coroutineContext.job
@ -345,7 +344,7 @@ class DownloadProcessor (
val mergedOverlay: File = File.createTempFile("merged", "." + media.fileType.fileExtension)
runCatching {
callbackOnProgress(translation.format("download_toast", "path" to media.file.nameWithoutExtension))
pendingDownloadObject.downloadStage = DownloadStage.MERGING
downloadObjectObject.downloadStage = DownloadStage.MERGING
MediaDownloaderHelper.mergeOverlayFile(
media = renamedMedia,
@ -353,12 +352,12 @@ class DownloadProcessor (
output = mergedOverlay
)
saveMediaToGallery(mergedOverlay, pendingDownloadObject)
saveMediaToGallery(mergedOverlay, downloadObjectObject)
}.onFailure { exception ->
if (coroutineContext.job.isCancelled) return@onFailure
Logger.error(exception)
callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message)
pendingDownloadObject.downloadStage = DownloadStage.MERGE_FAILED
downloadObjectObject.downloadStage = DownloadStage.MERGE_FAILED
}
mergedOverlay.delete()
@ -367,9 +366,9 @@ class DownloadProcessor (
return@launch
}
downloadRemoteMedia(pendingDownloadObject, downloadedMedias, downloadRequest)
downloadRemoteMedia(downloadObjectObject, downloadedMedias, downloadRequest)
}.onFailure { exception ->
pendingDownloadObject.downloadStage = DownloadStage.FAILED
downloadObjectObject.downloadStage = DownloadStage.FAILED
Logger.error(exception)
callbackOnFailure(translation["failed_generic_toast"], exception.message)
}

View File

@ -71,6 +71,8 @@ class Navigation(
Icon(Icons.Filled.ArrowBack, contentDescription = null)
}
}
}, actions = {
currentSection.TopBarActions(this)
})
}

View File

@ -1,5 +1,6 @@
package me.rhunk.snapenhance.ui.manager
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Download
@ -14,7 +15,7 @@ import androidx.navigation.compose.composable
import me.rhunk.snapenhance.RemoteSideContext
import me.rhunk.snapenhance.ui.manager.sections.HomeSection
import me.rhunk.snapenhance.ui.manager.sections.NotImplemented
import me.rhunk.snapenhance.ui.manager.sections.download.DownloadSection
import me.rhunk.snapenhance.ui.manager.sections.downloads.DownloadsSection
import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection
import kotlin.reflect.KClass
@ -26,7 +27,7 @@ enum class EnumSection(
DOWNLOADS(
route = "downloads",
icon = Icons.Filled.Download,
section = DownloadSection::class
section = DownloadsSection::class
),
FEATURES(
route = "features",
@ -70,6 +71,9 @@ open class Section {
@Composable
open fun Content() { NotImplemented() }
@Composable
open fun TopBarActions(rowScope: RowScope) {}
open fun build(navGraphBuilder: NavGraphBuilder) {
navGraphBuilder.composable(enumSection.route) {
Content()

View File

@ -1,12 +0,0 @@
package me.rhunk.snapenhance.ui.manager.sections.download
import androidx.compose.runtime.Composable
import me.rhunk.snapenhance.ui.manager.Section
import me.rhunk.snapenhance.ui.manager.sections.NotImplemented
class DownloadSection : Section() {
@Composable
override fun Content() {
NotImplemented()
}
}

View File

@ -0,0 +1,296 @@
package me.rhunk.snapenhance.ui.manager.sections.downloads
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
import coil.decode.VideoFrameDecoder
import coil.memory.MemoryCache
import coil.request.ImageRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.R
import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.download.data.DownloadObject
import me.rhunk.snapenhance.ui.download.MediaFilter
import me.rhunk.snapenhance.ui.manager.Section
class DownloadsSection : Section() {
private val loadedDownloads = mutableStateOf(mapOf<Int, DownloadObject>())
private var currentFilter = mutableStateOf(MediaFilter.NONE)
private val imageLoader by lazy {
ImageLoader.Builder(context.androidContext)
.dispatcher(Dispatchers.IO)
.memoryCache {
MemoryCache.Builder(context.androidContext)
.maxSizePercent(0.25)
.build()
}.components { add(VideoFrameDecoder.Factory()) }.build()
}
override fun onResumed() {
super.onResumed()
loadByFilter(currentFilter.value)
}
private fun loadByFilter(filter: MediaFilter) {
this.currentFilter.value = filter
synchronized(loadedDownloads) {
loadedDownloads.value = context.downloadTaskManager.queryFirstTasks(filter)
}
}
private fun lazyLoadFromIndex(lastIndex: Int) {
synchronized(loadedDownloads) {
loadedDownloads.value = loadedDownloads.value.toMutableMap().also {
val lastVisible = loadedDownloads.value.values.elementAt(lastIndex)
it += context.downloadTaskManager.queryTasks(
from = lastVisible.downloadId,
filter = currentFilter.value
)
}
}
}
@Composable
private fun FilterList() {
val coroutineScope = rememberCoroutineScope()
val showMenu = remember { mutableStateOf(false) }
IconButton(onClick = { showMenu.value = !showMenu.value}) {
Icon(
imageVector = Icons.Default.FilterList,
contentDescription = null
)
}
DropdownMenu(expanded = showMenu.value, onDismissRequest = { showMenu.value = false }) {
MediaFilter.values().toList().forEach { filter ->
DropdownMenuItem(
text = {
Row(
modifier = Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
modifier = Modifier.padding(end = 16.dp),
selected = (currentFilter.value == filter),
onClick = null
)
Text(filter.name, modifier = Modifier.weight(1f))
}
},
onClick = {
coroutineScope.launch {
loadByFilter(filter)
showMenu.value = false
}
}
)
}
}
}
@Composable
override fun TopBarActions(rowScope: RowScope) {
FilterList()
}
@Composable
private fun DownloadItem(download: DownloadObject) {
Card(
modifier = Modifier
.padding(6.dp)
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
) {
Box(modifier = Modifier.height(120.dp)) {
Image(
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context.androidContext)
.data(download.outputFile)
.memoryCacheKey(download.outputFile)
.build(),
imageLoader = imageLoader
),
modifier = Modifier
.matchParentSize()
.blur(5.dp),
contentDescription = null,
contentScale = ContentScale.FillWidth
)
Row(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp)
.fillMaxWidth()
.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
){
//info card
Row(
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.background,
shape = MaterialTheme.shapes.medium
)
.padding(15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context.androidContext)
.data(download.metadata.iconUrl)
.fallback(R.drawable.bitmoji_blank)
.memoryCacheKey(download.metadata.iconUrl)
.build(),
imageLoader = imageLoader
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredWidthIn(min = 0.dp, max = 48.dp)
.height(48.dp)
.clip(MaterialTheme.shapes.medium)
)
Column(
modifier = Modifier
.padding(start = 10.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
text = download.metadata.mediaDisplayType ?: "",
overflow = TextOverflow.Ellipsis,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
Text(
text = download.metadata.mediaDisplaySource ?: "",
overflow = TextOverflow.Ellipsis,
fontSize = 12.sp,
fontWeight = FontWeight.Light
)
}
}
Spacer(modifier = Modifier.weight(1f))
//action buttons
Row(
modifier = Modifier
.padding(5.dp),
verticalAlignment = Alignment.CenterVertically
) {
FilledIconButton(
onClick = {
},
colors = IconButtonDefaults.iconButtonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError
)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null
)
}
//open
FilledIconButton(onClick = {
val fileType = runCatching {
context.androidContext.contentResolver.openInputStream(Uri.parse(download.outputFile))?.use { input ->
FileType.fromInputStream(input)
}
}.getOrNull() ?: FileType.UNKNOWN
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(Uri.parse(download.outputFile), fileType.mimeType)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
context.androidContext.startActivity(intent)
}) {
Icon(
imageVector = Icons.Default.OpenInNew,
contentDescription = null
)
}
}
}
}
}
}
@Composable
override fun Content() {
val scrollState = rememberLazyListState()
LazyColumn(
state = scrollState,
modifier = Modifier.fillMaxSize()
) {
items(loadedDownloads.value.size) { index ->
DownloadItem(loadedDownloads.value.values.elementAt(index))
}
item {
Spacer(Modifier.height(20.dp))
if (loadedDownloads.value.isEmpty()) {
Text(text = "No downloads", fontSize = 20.sp, modifier = Modifier
.fillMaxWidth()
.padding(10.dp), textAlign = TextAlign.Center)
}
LaunchedEffect(true) {
val lastItemIndex = (loadedDownloads.value.size - 1).takeIf { it >= 0 } ?: return@LaunchedEffect
lazyLoadFromIndex(lastItemIndex)
scrollState.animateScrollToItem(lastItemIndex)
}
}
}
}
}

View File

@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.OpenInNew
@ -30,6 +29,7 @@ import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Switch
@ -46,7 +46,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navigation
@ -238,7 +237,7 @@ class FeaturesSection : Section() {
.height(50.dp)
.width(1.dp)
.background(
color = MaterialTheme.colors.onBackground.copy(alpha = 0.12f),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f),
shape = RoundedCornerShape(5.dp)
))
}
@ -329,8 +328,8 @@ class FeaturesSection : Section() {
}
},
modifier = Modifier.padding(10.dp),
containerColor = MaterialTheme.colors.primary,
contentColor = MaterialTheme.colors.onPrimary,
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
shape = RoundedCornerShape(16.dp),
) {
Icon(

View File

@ -5,9 +5,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -87,7 +87,7 @@ class MappingsScreen : SetupScreen() {
CircularProgressIndicator(
modifier = Modifier.padding().size(30.dp),
strokeWidth = 3.dp,
color = MaterialTheme.colors.onPrimary
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text(text = context.translation["setup.mappings.generate_button"])

View File

@ -10,10 +10,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -86,7 +86,6 @@ class PickLanguageScreen : SetupScreen(){
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
elevation = 8.dp,
shape = MaterialTheme.shapes.medium
) {
LazyColumn(

View File

@ -4,15 +4,17 @@ import android.annotation.SuppressLint
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import me.rhunk.snapenhance.download.data.DownloadMetadata
import me.rhunk.snapenhance.download.data.PendingDownload
import me.rhunk.snapenhance.download.data.DownloadObject
import me.rhunk.snapenhance.download.data.DownloadStage
import me.rhunk.snapenhance.ui.download.MediaFilter
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
import me.rhunk.snapenhance.util.getIntOrNull
import me.rhunk.snapenhance.util.getStringOrNull
class DownloadTaskManager {
private lateinit var taskDatabase: SQLiteDatabase
private val pendingTasks = mutableMapOf<Int, PendingDownload>()
private val cachedTasks = mutableMapOf<Int, PendingDownload>()
private val pendingTasks = mutableMapOf<Int, DownloadObject>()
private val cachedTasks = mutableMapOf<Int, DownloadObject>()
@SuppressLint("Range")
fun init(context: Context) {
@ -33,7 +35,7 @@ class DownloadTaskManager {
}
}
fun addTask(task: PendingDownload): Int {
fun addTask(task: DownloadObject): Int {
taskDatabase.execSQL("INSERT INTO tasks (hash, outputPath, outputFile, mediaDisplayType, mediaDisplaySource, iconUrl, downloadStage) VALUES (?, ?, ?, ?, ?, ?, ?)",
arrayOf(
task.metadata.mediaIdentifier,
@ -53,7 +55,7 @@ class DownloadTaskManager {
return task.downloadId
}
fun updateTask(task: PendingDownload) {
fun updateTask(task: DownloadObject) {
taskDatabase.execSQL("UPDATE tasks SET hash = ?, outputPath = ?, outputFile = ?, mediaDisplayType = ?, mediaDisplaySource = ?, iconUrl = ?, downloadStage = ? WHERE id = ?",
arrayOf(
task.metadata.mediaIdentifier,
@ -107,13 +109,13 @@ class DownloadTaskManager {
pendingTasks.remove(id)
}
fun removeTask(task: PendingDownload) {
fun removeTask(task: DownloadObject) {
removeTask(task.downloadId)
}
fun queryAllTasks(filter: MediaFilter): Map<Int, PendingDownload> {
fun queryFirstTasks(filter: MediaFilter): Map<Int, DownloadObject> {
val isPendingFilter = filter == MediaFilter.PENDING
val tasks = mutableMapOf<Int, PendingDownload>()
val tasks = mutableMapOf<Int, DownloadObject>()
tasks.putAll(pendingTasks.filter { isPendingFilter || filter.matches(it.value.metadata.mediaDisplayType) })
if (isPendingFilter) {
@ -130,7 +132,7 @@ class DownloadTaskManager {
}
@SuppressLint("Range")
fun queryTasks(from: Int, amount: Int = 20, filter: MediaFilter = MediaFilter.NONE): Map<Int, PendingDownload> {
fun queryTasks(from: Int, amount: Int = 30, filter: MediaFilter = MediaFilter.NONE): Map<Int, DownloadObject> {
if (filter == MediaFilter.PENDING) {
return emptyMap()
}
@ -139,27 +141,27 @@ class DownloadTaskManager {
"SELECT * FROM tasks WHERE id < ? AND mediaDisplayType LIKE ? ORDER BY id DESC LIMIT ?",
arrayOf(
from.toString(),
filter.mediaDisplayType.let { if (it == null) "%" else "%$it" },
if (filter.shouldIgnoreFilter) "%" else "%${filter.key}",
amount.toString()
)
)
val result = sortedMapOf<Int, PendingDownload>()
val result = sortedMapOf<Int, DownloadObject>()
while (cursor.moveToNext()) {
val task = PendingDownload(
downloadId = cursor.getInt(cursor.getColumnIndex("id")),
outputFile = cursor.getString(cursor.getColumnIndex("outputFile")),
val task = DownloadObject(
downloadId = cursor.getIntOrNull("id")!!,
outputFile = cursor.getStringOrNull("outputFile"),
metadata = DownloadMetadata(
outputPath = cursor.getString(cursor.getColumnIndex("outputPath")),
mediaIdentifier = cursor.getString(cursor.getColumnIndex("hash")),
mediaDisplayType = cursor.getString(cursor.getColumnIndex("mediaDisplayType")),
mediaDisplaySource = cursor.getString(cursor.getColumnIndex("mediaDisplaySource")),
iconUrl = cursor.getString(cursor.getColumnIndex("iconUrl"))
outputPath = cursor.getStringOrNull("outputPath")!!,
mediaIdentifier = cursor.getStringOrNull("hash"),
mediaDisplayType = cursor.getStringOrNull("mediaDisplayType"),
mediaDisplaySource = cursor.getStringOrNull("mediaDisplaySource"),
iconUrl = cursor.getStringOrNull("iconUrl")
)
).apply {
downloadTaskManager = this@DownloadTaskManager
downloadStage = DownloadStage.valueOf(cursor.getString(cursor.getColumnIndex("downloadStage")))
downloadStage = DownloadStage.valueOf(cursor.getStringOrNull("downloadStage")!!)
//if downloadStage is not saved, it means the app was killed before the download was finished
if (downloadStage != DownloadStage.SAVED) {
downloadStage = DownloadStage.FAILED

View File

@ -1,10 +1,9 @@
package me.rhunk.snapenhance.download.data
import kotlinx.coroutines.Job
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.download.DownloadTaskManager
data class PendingDownload(
data class DownloadObject(
var downloadId: Int = 0,
var outputFile: String? = null,
val metadata : DownloadMetadata

View File

@ -242,7 +242,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
pathSuffix = authorUsername,
mediaIdentifier = "$conversationId$senderId${conversationMessage.server_message_id}",
mediaDisplaySource = authorUsername,
mediaDisplayType = MediaFilter.CHAT_MEDIA.mediaDisplayType,
mediaDisplayType = MediaFilter.CHAT_MEDIA.key,
friendInfo = author
), mediaInfoMap)
@ -282,7 +282,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
pathSuffix = authorName,
mediaIdentifier = paramMap["MEDIA_ID"].toString(),
mediaDisplaySource = authorName,
mediaDisplayType = MediaFilter.STORY.mediaDisplayType,
mediaDisplayType = MediaFilter.STORY.key,
friendInfo = author
), mediaInfoMap)
return
@ -311,7 +311,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
downloadOperaMedia(provideDownloadManagerClient(
pathSuffix = "Spotlight",
mediaIdentifier = paramMap["SNAP_ID"].toString(),
mediaDisplayType = MediaFilter.SPOTLIGHT.mediaDisplayType,
mediaDisplayType = MediaFilter.SPOTLIGHT.key,
mediaDisplaySource = paramMap["TIME_STAMP"].toString()
), mediaInfoMap)
return
@ -476,7 +476,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
pathSuffix = authorName,
mediaIdentifier = "${message.client_conversation_id}${message.sender_id}${message.server_message_id}",
mediaDisplaySource = authorName,
mediaDisplayType = MediaFilter.CHAT_MEDIA.mediaDisplayType,
mediaDisplayType = MediaFilter.CHAT_MEDIA.key,
friendInfo = friendInfo
).downloadSingleMedia(
Base64.UrlSafe.encode(urlProto),

View File

@ -26,7 +26,7 @@ import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.download.data.PendingDownload
import me.rhunk.snapenhance.download.data.DownloadObject
import me.rhunk.snapenhance.download.data.DownloadStage
import me.rhunk.snapenhance.util.snap.PreviewUtils
import java.io.File
@ -37,7 +37,7 @@ import kotlin.coroutines.coroutineContext
class DownloadListAdapter(
private val activity: DownloadManagerActivity,
private val downloadList: MutableList<PendingDownload>
private val downloadList: MutableList<DownloadObject>
): Adapter<DownloadListAdapter.ViewHolder>() {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private val previewJobs = mutableMapOf<Int, Job>()
@ -68,7 +68,7 @@ class DownloadListAdapter(
}
@SuppressLint("Recycle")
private suspend fun handlePreview(download: PendingDownload, holder: ViewHolder) {
private suspend fun handlePreview(download: DownloadObject, holder: ViewHolder) {
download.outputFile?.let {
val uri = Uri.parse(it)
runCatching {
@ -124,7 +124,7 @@ class DownloadListAdapter(
}
}
private fun updateViewHolder(download: PendingDownload, holder: ViewHolder) {
private fun updateViewHolder(download: DownloadObject, holder: ViewHolder) {
holder.status.text = download.downloadStage.toString()
holder.view.background = holder.view.context.getDrawable(R.drawable.download_manager_item_background)
@ -163,7 +163,7 @@ class DownloadListAdapter(
}
}
holder.bitmojiIcon.setImageResource(R.drawable.bitmoji_blank)
// holder.bitmojiIcon.setImageResource(R.drawable.bitmoji_blank)
pendingDownload.metadata.iconUrl?.let { url ->
thread(start = true) {

View File

@ -19,13 +19,13 @@ import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.download.data.PendingDownload
import me.rhunk.snapenhance.download.data.DownloadObject
class DownloadManagerActivity : Activity() {
lateinit var translation: LocaleWrapper
private val backCallbacks = mutableListOf<() -> Unit>()
private val fetchedDownloadTasks = mutableListOf<PendingDownload>()
private val fetchedDownloadTasks = mutableListOf<DownloadObject>()
private var listFilter = MediaFilter.NONE
private val preferences by lazy {
@ -42,7 +42,7 @@ class DownloadManagerActivity : Activity() {
@SuppressLint("NotifyDataSetChanged")
private fun updateListContent() {
fetchedDownloadTasks.clear()
fetchedDownloadTasks.addAll(SharedContext.downloadTaskManager.queryAllTasks(filter = listFilter).values)
fetchedDownloadTasks.addAll(SharedContext.downloadTaskManager.queryFirstTasks(filter = listFilter).values)
with(findViewById<RecyclerView>(R.id.download_list)) {
adapter?.notifyDataSetChanged()

View File

@ -1,17 +1,17 @@
package me.rhunk.snapenhance.ui.download
enum class MediaFilter(
val mediaDisplayType: String? = null
val key: String,
val shouldIgnoreFilter: Boolean = false
) {
NONE,
PENDING,
CHAT_MEDIA("Chat Media"),
STORY("Story"),
SPOTLIGHT("Spotlight");
NONE("none", true),
PENDING("pending", true),
CHAT_MEDIA("chat_media"),
STORY("story"),
SPOTLIGHT("spotlight");
fun matches(source: String?): Boolean {
if (mediaDisplayType == null) return true
if (source == null) return false
return source.contains(mediaDisplayType, ignoreCase = true)
return source.contains(key, ignoreCase = true)
}
}

View File

@ -48,9 +48,7 @@ object PreviewUtils {
setDataSource(file.absolutePath)
}.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
} else {
BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply {
inSampleSize = 1
})
BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options())
}
}

View File

@ -1,11 +1,12 @@
[versions]
agp = "8.2.0-alpha14"
androidx-material = "1.6.0-alpha02"
coil-compose = "2.4.0"
junit = "4.13.2"
kotlin = "1.8.22"
kotlinx-coroutines-android = "1.7.2"
kotlin-reflect = "1.8.22"
material-icons-extended = "1.6.0-alpha03"
material-icons-core = "1.4.3"
material-icons-extended = "1.6.0-alpha02"
navigation-compose = "2.7.0-rc01"
recyclerview = "1.3.1"
gson = "2.10.1"
@ -19,9 +20,12 @@ material3 = "1.1.1"
[libraries]
androidx-material = { module = "androidx.compose.material:material", version.ref = "androidx-material" }
androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "material-icons-core" }
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material-icons-extended" }
androidx-material-ripple = { module = "androidx.compose.material:material-ripple", version.ref = "material-icons-core" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" }
coil-video = { module = "io.coil-kt:coil-video", version.ref = "coil-compose" }
coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
junit = { module = "junit:junit", version.ref = "junit" }
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin-reflect" }