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 {
|
dependencies {
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
|
implementation(libs.androidx.material.icons.core)
|
||||||
|
implementation(libs.androidx.material.ripple)
|
||||||
implementation(libs.androidx.material.icons.extended)
|
implementation(libs.androidx.material.icons.extended)
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.material)
|
|
||||||
implementation(libs.androidx.activity.ktx)
|
implementation(libs.androidx.activity.ktx)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.documentfile)
|
implementation(libs.androidx.documentfile)
|
||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
implementation(libs.coil.video)
|
||||||
|
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
|
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview: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.Constants
|
||||||
import me.rhunk.snapenhance.Logger
|
import me.rhunk.snapenhance.Logger
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.SharedContext
|
|
||||||
import me.rhunk.snapenhance.bridge.DownloadCallback
|
import me.rhunk.snapenhance.bridge.DownloadCallback
|
||||||
import me.rhunk.snapenhance.data.FileType
|
import me.rhunk.snapenhance.data.FileType
|
||||||
import me.rhunk.snapenhance.download.data.DownloadMediaType
|
import me.rhunk.snapenhance.download.data.DownloadMediaType
|
||||||
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
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.DownloadRequest
|
||||||
import me.rhunk.snapenhance.download.data.DownloadStage
|
import me.rhunk.snapenhance.download.data.DownloadStage
|
||||||
import me.rhunk.snapenhance.download.data.InputMedia
|
import me.rhunk.snapenhance.download.data.InputMedia
|
||||||
import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
|
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.download.RemoteMediaResolver
|
||||||
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -60,7 +59,7 @@ class DownloadProcessor (
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val translation by lazy {
|
private val translation by lazy {
|
||||||
SharedContext.translation.getCategory("download_processor")
|
remoteSideContext.translation.getCategory("download_processor")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val gson by lazy {
|
private val gson by lazy {
|
||||||
@ -118,7 +117,7 @@ class DownloadProcessor (
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) {
|
private suspend fun saveMediaToGallery(inputFile: File, downloadObject: DownloadObject) {
|
||||||
if (coroutineContext.job.isCancelled) return
|
if (coroutineContext.job.isCancelled) return
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
@ -128,12 +127,12 @@ class DownloadProcessor (
|
|||||||
return
|
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()))
|
val outputFolder = DocumentFile.fromTreeUri(remoteSideContext.androidContext, Uri.parse(remoteSideContext.config.root.downloader.saveFolder.get()))
|
||||||
?: throw Exception("Failed to open output folder")
|
?: throw Exception("Failed to open output folder")
|
||||||
|
|
||||||
val outputFileFolder = pendingDownload.metadata.outputPath.let {
|
val outputFileFolder = downloadObject.metadata.outputPath.let {
|
||||||
if (it.contains("/")) {
|
if (it.contains("/")) {
|
||||||
it.substringBeforeLast("/").split("/").fold(outputFolder) { folder, name ->
|
it.substringBeforeLast("/").split("/").fold(outputFolder) { folder, name ->
|
||||||
folder.findFile(name) ?: folder.createDirectory(name)!!
|
folder.findFile(name) ?: folder.createDirectory(name)!!
|
||||||
@ -150,8 +149,8 @@ class DownloadProcessor (
|
|||||||
inputStream.copyTo(outputStream)
|
inputStream.copyTo(outputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingDownload.outputFile = outputFile.uri.toString()
|
downloadObject.outputFile = outputFile.uri.toString()
|
||||||
pendingDownload.downloadStage = DownloadStage.SAVED
|
downloadObject.downloadStage = DownloadStage.SAVED
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE")
|
val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE")
|
||||||
@ -167,7 +166,7 @@ class DownloadProcessor (
|
|||||||
}.onFailure { exception ->
|
}.onFailure { exception ->
|
||||||
Logger.error(exception)
|
Logger.error(exception)
|
||||||
callbackOnFailure(translation.format("failed_gallery_toast", "error" to exception.toString()), exception.message)
|
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
|
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 ->
|
downloadRequest.inputMedias.first().let { inputMedia ->
|
||||||
val mediaType = inputMedia.type
|
val mediaType = inputMedia.type
|
||||||
val media = downloadedMedias[inputMedia]!!
|
val media = downloadedMedias[inputMedia]!!
|
||||||
|
|
||||||
if (!downloadRequest.isDashPlaylist) {
|
if (!downloadRequest.isDashPlaylist) {
|
||||||
saveMediaToGallery(media.file, pendingDownloadObject)
|
saveMediaToGallery(media.file, downloadObjectObject)
|
||||||
media.file.delete()
|
media.file.delete()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -261,12 +260,12 @@ class DownloadProcessor (
|
|||||||
output = outputFile,
|
output = outputFile,
|
||||||
startTime = dashOptions.offsetTime,
|
startTime = dashOptions.offsetTime,
|
||||||
duration = dashOptions.duration)
|
duration = dashOptions.duration)
|
||||||
saveMediaToGallery(outputFile, pendingDownloadObject)
|
saveMediaToGallery(outputFile, downloadObjectObject)
|
||||||
}.onFailure { exception ->
|
}.onFailure { exception ->
|
||||||
if (coroutineContext.job.isCancelled) return@onFailure
|
if (coroutineContext.job.isCancelled) return@onFailure
|
||||||
Logger.error(exception)
|
Logger.error(exception)
|
||||||
callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message)
|
callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message)
|
||||||
pendingDownloadObject.downloadStage = DownloadStage.FAILED
|
downloadObjectObject.downloadStage = DownloadStage.FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
dashPlaylistFile.delete()
|
dashPlaylistFile.delete()
|
||||||
@ -297,11 +296,11 @@ class DownloadProcessor (
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingDownloadObject = PendingDownload(
|
val downloadObjectObject = DownloadObject(
|
||||||
metadata = downloadMetadata
|
metadata = downloadMetadata
|
||||||
).apply { downloadTaskManager = remoteSideContext.downloadTaskManager }
|
).apply { downloadTaskManager = remoteSideContext.downloadTaskManager }
|
||||||
|
|
||||||
pendingDownloadObject.also {
|
downloadObjectObject.also {
|
||||||
remoteSideContext.downloadTaskManager.addTask(it)
|
remoteSideContext.downloadTaskManager.addTask(it)
|
||||||
}.apply {
|
}.apply {
|
||||||
job = coroutineContext.job
|
job = coroutineContext.job
|
||||||
@ -345,7 +344,7 @@ class DownloadProcessor (
|
|||||||
val mergedOverlay: File = File.createTempFile("merged", "." + media.fileType.fileExtension)
|
val mergedOverlay: File = File.createTempFile("merged", "." + media.fileType.fileExtension)
|
||||||
runCatching {
|
runCatching {
|
||||||
callbackOnProgress(translation.format("download_toast", "path" to media.file.nameWithoutExtension))
|
callbackOnProgress(translation.format("download_toast", "path" to media.file.nameWithoutExtension))
|
||||||
pendingDownloadObject.downloadStage = DownloadStage.MERGING
|
downloadObjectObject.downloadStage = DownloadStage.MERGING
|
||||||
|
|
||||||
MediaDownloaderHelper.mergeOverlayFile(
|
MediaDownloaderHelper.mergeOverlayFile(
|
||||||
media = renamedMedia,
|
media = renamedMedia,
|
||||||
@ -353,12 +352,12 @@ class DownloadProcessor (
|
|||||||
output = mergedOverlay
|
output = mergedOverlay
|
||||||
)
|
)
|
||||||
|
|
||||||
saveMediaToGallery(mergedOverlay, pendingDownloadObject)
|
saveMediaToGallery(mergedOverlay, downloadObjectObject)
|
||||||
}.onFailure { exception ->
|
}.onFailure { exception ->
|
||||||
if (coroutineContext.job.isCancelled) return@onFailure
|
if (coroutineContext.job.isCancelled) return@onFailure
|
||||||
Logger.error(exception)
|
Logger.error(exception)
|
||||||
callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message)
|
callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message)
|
||||||
pendingDownloadObject.downloadStage = DownloadStage.MERGE_FAILED
|
downloadObjectObject.downloadStage = DownloadStage.MERGE_FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
mergedOverlay.delete()
|
mergedOverlay.delete()
|
||||||
@ -367,9 +366,9 @@ class DownloadProcessor (
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadRemoteMedia(pendingDownloadObject, downloadedMedias, downloadRequest)
|
downloadRemoteMedia(downloadObjectObject, downloadedMedias, downloadRequest)
|
||||||
}.onFailure { exception ->
|
}.onFailure { exception ->
|
||||||
pendingDownloadObject.downloadStage = DownloadStage.FAILED
|
downloadObjectObject.downloadStage = DownloadStage.FAILED
|
||||||
Logger.error(exception)
|
Logger.error(exception)
|
||||||
callbackOnFailure(translation["failed_generic_toast"], exception.message)
|
callbackOnFailure(translation["failed_generic_toast"], exception.message)
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,8 @@ class Navigation(
|
|||||||
Icon(Icons.Filled.ArrowBack, contentDescription = null)
|
Icon(Icons.Filled.ArrowBack, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, actions = {
|
||||||
|
currentSection.TopBarActions(this)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager
|
package me.rhunk.snapenhance.ui.manager
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.BugReport
|
import androidx.compose.material.icons.filled.BugReport
|
||||||
import androidx.compose.material.icons.filled.Download
|
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.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.HomeSection
|
import me.rhunk.snapenhance.ui.manager.sections.HomeSection
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.NotImplemented
|
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 me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@ -26,7 +27,7 @@ enum class EnumSection(
|
|||||||
DOWNLOADS(
|
DOWNLOADS(
|
||||||
route = "downloads",
|
route = "downloads",
|
||||||
icon = Icons.Filled.Download,
|
icon = Icons.Filled.Download,
|
||||||
section = DownloadSection::class
|
section = DownloadsSection::class
|
||||||
),
|
),
|
||||||
FEATURES(
|
FEATURES(
|
||||||
route = "features",
|
route = "features",
|
||||||
@ -70,6 +71,9 @@ open class Section {
|
|||||||
@Composable
|
@Composable
|
||||||
open fun Content() { NotImplemented() }
|
open fun Content() { NotImplemented() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
open fun TopBarActions(rowScope: RowScope) {}
|
||||||
|
|
||||||
open fun build(navGraphBuilder: NavGraphBuilder) {
|
open fun build(navGraphBuilder: NavGraphBuilder) {
|
||||||
navGraphBuilder.composable(enumSection.route) {
|
navGraphBuilder.composable(enumSection.route) {
|
||||||
Content()
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.FolderOpen
|
import androidx.compose.material.icons.filled.FolderOpen
|
||||||
import androidx.compose.material.icons.filled.OpenInNew
|
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.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.Switch
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navigation
|
import androidx.navigation.navigation
|
||||||
@ -238,7 +237,7 @@ class FeaturesSection : Section() {
|
|||||||
.height(50.dp)
|
.height(50.dp)
|
||||||
.width(1.dp)
|
.width(1.dp)
|
||||||
.background(
|
.background(
|
||||||
color = MaterialTheme.colors.onBackground.copy(alpha = 0.12f),
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f),
|
||||||
shape = RoundedCornerShape(5.dp)
|
shape = RoundedCornerShape(5.dp)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -329,8 +328,8 @@ class FeaturesSection : Section() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(10.dp),
|
modifier = Modifier.padding(10.dp),
|
||||||
containerColor = MaterialTheme.colors.primary,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
contentColor = MaterialTheme.colors.onPrimary,
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -5,9 +5,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -87,7 +87,7 @@ class MappingsScreen : SetupScreen() {
|
|||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.padding().size(30.dp),
|
modifier = Modifier.padding().size(30.dp),
|
||||||
strokeWidth = 3.dp,
|
strokeWidth = 3.dp,
|
||||||
color = MaterialTheme.colors.onPrimary
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(text = context.translation["setup.mappings.generate_button"])
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -86,7 +86,6 @@ class PickLanguageScreen : SetupScreen(){
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(10.dp)
|
.padding(10.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
elevation = 8.dp,
|
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
@ -4,15 +4,17 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
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.download.data.DownloadStage
|
||||||
import me.rhunk.snapenhance.ui.download.MediaFilter
|
import me.rhunk.snapenhance.ui.download.MediaFilter
|
||||||
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
|
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
|
||||||
|
import me.rhunk.snapenhance.util.getIntOrNull
|
||||||
|
import me.rhunk.snapenhance.util.getStringOrNull
|
||||||
|
|
||||||
class DownloadTaskManager {
|
class DownloadTaskManager {
|
||||||
private lateinit var taskDatabase: SQLiteDatabase
|
private lateinit var taskDatabase: SQLiteDatabase
|
||||||
private val pendingTasks = mutableMapOf<Int, PendingDownload>()
|
private val pendingTasks = mutableMapOf<Int, DownloadObject>()
|
||||||
private val cachedTasks = mutableMapOf<Int, PendingDownload>()
|
private val cachedTasks = mutableMapOf<Int, DownloadObject>()
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
fun init(context: Context) {
|
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 (?, ?, ?, ?, ?, ?, ?)",
|
taskDatabase.execSQL("INSERT INTO tasks (hash, outputPath, outputFile, mediaDisplayType, mediaDisplaySource, iconUrl, downloadStage) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
task.metadata.mediaIdentifier,
|
task.metadata.mediaIdentifier,
|
||||||
@ -53,7 +55,7 @@ class DownloadTaskManager {
|
|||||||
return task.downloadId
|
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 = ?",
|
taskDatabase.execSQL("UPDATE tasks SET hash = ?, outputPath = ?, outputFile = ?, mediaDisplayType = ?, mediaDisplaySource = ?, iconUrl = ?, downloadStage = ? WHERE id = ?",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
task.metadata.mediaIdentifier,
|
task.metadata.mediaIdentifier,
|
||||||
@ -107,13 +109,13 @@ class DownloadTaskManager {
|
|||||||
pendingTasks.remove(id)
|
pendingTasks.remove(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeTask(task: PendingDownload) {
|
fun removeTask(task: DownloadObject) {
|
||||||
removeTask(task.downloadId)
|
removeTask(task.downloadId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun queryAllTasks(filter: MediaFilter): Map<Int, PendingDownload> {
|
fun queryFirstTasks(filter: MediaFilter): Map<Int, DownloadObject> {
|
||||||
val isPendingFilter = filter == MediaFilter.PENDING
|
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) })
|
tasks.putAll(pendingTasks.filter { isPendingFilter || filter.matches(it.value.metadata.mediaDisplayType) })
|
||||||
if (isPendingFilter) {
|
if (isPendingFilter) {
|
||||||
@ -130,7 +132,7 @@ class DownloadTaskManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@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) {
|
if (filter == MediaFilter.PENDING) {
|
||||||
return emptyMap()
|
return emptyMap()
|
||||||
}
|
}
|
||||||
@ -139,27 +141,27 @@ class DownloadTaskManager {
|
|||||||
"SELECT * FROM tasks WHERE id < ? AND mediaDisplayType LIKE ? ORDER BY id DESC LIMIT ?",
|
"SELECT * FROM tasks WHERE id < ? AND mediaDisplayType LIKE ? ORDER BY id DESC LIMIT ?",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
from.toString(),
|
from.toString(),
|
||||||
filter.mediaDisplayType.let { if (it == null) "%" else "%$it" },
|
if (filter.shouldIgnoreFilter) "%" else "%${filter.key}",
|
||||||
amount.toString()
|
amount.toString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = sortedMapOf<Int, PendingDownload>()
|
val result = sortedMapOf<Int, DownloadObject>()
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val task = PendingDownload(
|
val task = DownloadObject(
|
||||||
downloadId = cursor.getInt(cursor.getColumnIndex("id")),
|
downloadId = cursor.getIntOrNull("id")!!,
|
||||||
outputFile = cursor.getString(cursor.getColumnIndex("outputFile")),
|
outputFile = cursor.getStringOrNull("outputFile"),
|
||||||
metadata = DownloadMetadata(
|
metadata = DownloadMetadata(
|
||||||
outputPath = cursor.getString(cursor.getColumnIndex("outputPath")),
|
outputPath = cursor.getStringOrNull("outputPath")!!,
|
||||||
mediaIdentifier = cursor.getString(cursor.getColumnIndex("hash")),
|
mediaIdentifier = cursor.getStringOrNull("hash"),
|
||||||
mediaDisplayType = cursor.getString(cursor.getColumnIndex("mediaDisplayType")),
|
mediaDisplayType = cursor.getStringOrNull("mediaDisplayType"),
|
||||||
mediaDisplaySource = cursor.getString(cursor.getColumnIndex("mediaDisplaySource")),
|
mediaDisplaySource = cursor.getStringOrNull("mediaDisplaySource"),
|
||||||
iconUrl = cursor.getString(cursor.getColumnIndex("iconUrl"))
|
iconUrl = cursor.getStringOrNull("iconUrl")
|
||||||
)
|
)
|
||||||
).apply {
|
).apply {
|
||||||
downloadTaskManager = this@DownloadTaskManager
|
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 is not saved, it means the app was killed before the download was finished
|
||||||
if (downloadStage != DownloadStage.SAVED) {
|
if (downloadStage != DownloadStage.SAVED) {
|
||||||
downloadStage = DownloadStage.FAILED
|
downloadStage = DownloadStage.FAILED
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
package me.rhunk.snapenhance.download.data
|
package me.rhunk.snapenhance.download.data
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import me.rhunk.snapenhance.SharedContext
|
|
||||||
import me.rhunk.snapenhance.download.DownloadTaskManager
|
import me.rhunk.snapenhance.download.DownloadTaskManager
|
||||||
|
|
||||||
data class PendingDownload(
|
data class DownloadObject(
|
||||||
var downloadId: Int = 0,
|
var downloadId: Int = 0,
|
||||||
var outputFile: String? = null,
|
var outputFile: String? = null,
|
||||||
val metadata : DownloadMetadata
|
val metadata : DownloadMetadata
|
@ -242,7 +242,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
pathSuffix = authorUsername,
|
pathSuffix = authorUsername,
|
||||||
mediaIdentifier = "$conversationId$senderId${conversationMessage.server_message_id}",
|
mediaIdentifier = "$conversationId$senderId${conversationMessage.server_message_id}",
|
||||||
mediaDisplaySource = authorUsername,
|
mediaDisplaySource = authorUsername,
|
||||||
mediaDisplayType = MediaFilter.CHAT_MEDIA.mediaDisplayType,
|
mediaDisplayType = MediaFilter.CHAT_MEDIA.key,
|
||||||
friendInfo = author
|
friendInfo = author
|
||||||
), mediaInfoMap)
|
), mediaInfoMap)
|
||||||
|
|
||||||
@ -282,7 +282,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
pathSuffix = authorName,
|
pathSuffix = authorName,
|
||||||
mediaIdentifier = paramMap["MEDIA_ID"].toString(),
|
mediaIdentifier = paramMap["MEDIA_ID"].toString(),
|
||||||
mediaDisplaySource = authorName,
|
mediaDisplaySource = authorName,
|
||||||
mediaDisplayType = MediaFilter.STORY.mediaDisplayType,
|
mediaDisplayType = MediaFilter.STORY.key,
|
||||||
friendInfo = author
|
friendInfo = author
|
||||||
), mediaInfoMap)
|
), mediaInfoMap)
|
||||||
return
|
return
|
||||||
@ -311,7 +311,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
downloadOperaMedia(provideDownloadManagerClient(
|
downloadOperaMedia(provideDownloadManagerClient(
|
||||||
pathSuffix = "Spotlight",
|
pathSuffix = "Spotlight",
|
||||||
mediaIdentifier = paramMap["SNAP_ID"].toString(),
|
mediaIdentifier = paramMap["SNAP_ID"].toString(),
|
||||||
mediaDisplayType = MediaFilter.SPOTLIGHT.mediaDisplayType,
|
mediaDisplayType = MediaFilter.SPOTLIGHT.key,
|
||||||
mediaDisplaySource = paramMap["TIME_STAMP"].toString()
|
mediaDisplaySource = paramMap["TIME_STAMP"].toString()
|
||||||
), mediaInfoMap)
|
), mediaInfoMap)
|
||||||
return
|
return
|
||||||
@ -476,7 +476,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
pathSuffix = authorName,
|
pathSuffix = authorName,
|
||||||
mediaIdentifier = "${message.client_conversation_id}${message.sender_id}${message.server_message_id}",
|
mediaIdentifier = "${message.client_conversation_id}${message.sender_id}${message.server_message_id}",
|
||||||
mediaDisplaySource = authorName,
|
mediaDisplaySource = authorName,
|
||||||
mediaDisplayType = MediaFilter.CHAT_MEDIA.mediaDisplayType,
|
mediaDisplayType = MediaFilter.CHAT_MEDIA.key,
|
||||||
friendInfo = friendInfo
|
friendInfo = friendInfo
|
||||||
).downloadSingleMedia(
|
).downloadSingleMedia(
|
||||||
Base64.UrlSafe.encode(urlProto),
|
Base64.UrlSafe.encode(urlProto),
|
||||||
|
@ -26,7 +26,7 @@ import me.rhunk.snapenhance.Logger
|
|||||||
import me.rhunk.snapenhance.SharedContext
|
import me.rhunk.snapenhance.SharedContext
|
||||||
import me.rhunk.snapenhance.core.R
|
import me.rhunk.snapenhance.core.R
|
||||||
import me.rhunk.snapenhance.data.FileType
|
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.download.data.DownloadStage
|
||||||
import me.rhunk.snapenhance.util.snap.PreviewUtils
|
import me.rhunk.snapenhance.util.snap.PreviewUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -37,7 +37,7 @@ import kotlin.coroutines.coroutineContext
|
|||||||
|
|
||||||
class DownloadListAdapter(
|
class DownloadListAdapter(
|
||||||
private val activity: DownloadManagerActivity,
|
private val activity: DownloadManagerActivity,
|
||||||
private val downloadList: MutableList<PendingDownload>
|
private val downloadList: MutableList<DownloadObject>
|
||||||
): Adapter<DownloadListAdapter.ViewHolder>() {
|
): Adapter<DownloadListAdapter.ViewHolder>() {
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||||
private val previewJobs = mutableMapOf<Int, Job>()
|
private val previewJobs = mutableMapOf<Int, Job>()
|
||||||
@ -68,7 +68,7 @@ class DownloadListAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Recycle")
|
@SuppressLint("Recycle")
|
||||||
private suspend fun handlePreview(download: PendingDownload, holder: ViewHolder) {
|
private suspend fun handlePreview(download: DownloadObject, holder: ViewHolder) {
|
||||||
download.outputFile?.let {
|
download.outputFile?.let {
|
||||||
val uri = Uri.parse(it)
|
val uri = Uri.parse(it)
|
||||||
runCatching {
|
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.status.text = download.downloadStage.toString()
|
||||||
holder.view.background = holder.view.context.getDrawable(R.drawable.download_manager_item_background)
|
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 ->
|
pendingDownload.metadata.iconUrl?.let { url ->
|
||||||
thread(start = true) {
|
thread(start = true) {
|
||||||
|
@ -19,13 +19,13 @@ import me.rhunk.snapenhance.SharedContext
|
|||||||
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
|
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
|
||||||
import me.rhunk.snapenhance.core.BuildConfig
|
import me.rhunk.snapenhance.core.BuildConfig
|
||||||
import me.rhunk.snapenhance.core.R
|
import me.rhunk.snapenhance.core.R
|
||||||
import me.rhunk.snapenhance.download.data.PendingDownload
|
import me.rhunk.snapenhance.download.data.DownloadObject
|
||||||
|
|
||||||
class DownloadManagerActivity : Activity() {
|
class DownloadManagerActivity : Activity() {
|
||||||
lateinit var translation: LocaleWrapper
|
lateinit var translation: LocaleWrapper
|
||||||
|
|
||||||
private val backCallbacks = mutableListOf<() -> Unit>()
|
private val backCallbacks = mutableListOf<() -> Unit>()
|
||||||
private val fetchedDownloadTasks = mutableListOf<PendingDownload>()
|
private val fetchedDownloadTasks = mutableListOf<DownloadObject>()
|
||||||
private var listFilter = MediaFilter.NONE
|
private var listFilter = MediaFilter.NONE
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by lazy {
|
||||||
@ -42,7 +42,7 @@ class DownloadManagerActivity : Activity() {
|
|||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun updateListContent() {
|
private fun updateListContent() {
|
||||||
fetchedDownloadTasks.clear()
|
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)) {
|
with(findViewById<RecyclerView>(R.id.download_list)) {
|
||||||
adapter?.notifyDataSetChanged()
|
adapter?.notifyDataSetChanged()
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
package me.rhunk.snapenhance.ui.download
|
package me.rhunk.snapenhance.ui.download
|
||||||
|
|
||||||
enum class MediaFilter(
|
enum class MediaFilter(
|
||||||
val mediaDisplayType: String? = null
|
val key: String,
|
||||||
|
val shouldIgnoreFilter: Boolean = false
|
||||||
) {
|
) {
|
||||||
NONE,
|
NONE("none", true),
|
||||||
PENDING,
|
PENDING("pending", true),
|
||||||
CHAT_MEDIA("Chat Media"),
|
CHAT_MEDIA("chat_media"),
|
||||||
STORY("Story"),
|
STORY("story"),
|
||||||
SPOTLIGHT("Spotlight");
|
SPOTLIGHT("spotlight");
|
||||||
|
|
||||||
fun matches(source: String?): Boolean {
|
fun matches(source: String?): Boolean {
|
||||||
if (mediaDisplayType == null) return true
|
|
||||||
if (source == null) return false
|
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)
|
setDataSource(file.absolutePath)
|
||||||
}.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
}.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
||||||
} else {
|
} else {
|
||||||
BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply {
|
BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options())
|
||||||
inSampleSize = 1
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.2.0-alpha14"
|
agp = "8.2.0-alpha14"
|
||||||
androidx-material = "1.6.0-alpha02"
|
coil-compose = "2.4.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "1.8.22"
|
kotlin = "1.8.22"
|
||||||
kotlinx-coroutines-android = "1.7.2"
|
kotlinx-coroutines-android = "1.7.2"
|
||||||
kotlin-reflect = "1.8.22"
|
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"
|
navigation-compose = "2.7.0-rc01"
|
||||||
recyclerview = "1.3.1"
|
recyclerview = "1.3.1"
|
||||||
gson = "2.10.1"
|
gson = "2.10.1"
|
||||||
@ -19,9 +20,12 @@ material3 = "1.1.1"
|
|||||||
|
|
||||||
|
|
||||||
[libraries]
|
[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-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" }
|
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" }
|
coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
|
||||||
junit = { module = "junit:junit", version.ref = "junit" }
|
junit = { module = "junit:junit", version.ref = "junit" }
|
||||||
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin-reflect" }
|
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin-reflect" }
|
||||||
|
Reference in New Issue
Block a user