mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
feat: download section
This commit is contained in:
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -71,6 +71,8 @@ class Navigation(
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}, actions = {
|
||||
currentSection.TopBarActions(this)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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"])
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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),
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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" }
|
||||
|
Reference in New Issue
Block a user