LoggedStories.kt (12324B) - raw


      1 package me.rhunk.snapenhance.ui.manager.pages.social
      2 
      3 import android.content.Intent
      4 import androidx.compose.foundation.Image
      5 import androidx.compose.foundation.clickable
      6 import androidx.compose.foundation.layout.*
      7 import androidx.compose.foundation.lazy.grid.GridCells
      8 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
      9 import androidx.compose.foundation.lazy.grid.items
     10 import androidx.compose.material3.Button
     11 import androidx.compose.material3.Card
     12 import androidx.compose.material3.MaterialTheme
     13 import androidx.compose.material3.Text
     14 import androidx.compose.runtime.*
     15 import androidx.compose.ui.Alignment
     16 import androidx.compose.ui.Modifier
     17 import androidx.compose.ui.draw.clip
     18 import androidx.compose.ui.layout.ContentScale
     19 import androidx.compose.ui.text.style.TextAlign
     20 import androidx.compose.ui.unit.dp
     21 import androidx.compose.ui.unit.sp
     22 import androidx.core.content.FileProvider
     23 import androidx.navigation.NavBackStackEntry
     24 import coil.annotation.ExperimentalCoilApi
     25 import coil.compose.rememberAsyncImagePainter
     26 import me.rhunk.snapenhance.bridge.DownloadCallback
     27 import me.rhunk.snapenhance.common.data.FileType
     28 import me.rhunk.snapenhance.common.data.StoryData
     29 import me.rhunk.snapenhance.common.data.download.*
     30 import me.rhunk.snapenhance.common.util.ktx.longHashCode
     31 import me.rhunk.snapenhance.download.DownloadProcessor
     32 import me.rhunk.snapenhance.storage.getFriendInfo
     33 import me.rhunk.snapenhance.ui.manager.Routes
     34 import me.rhunk.snapenhance.ui.util.Dialog
     35 import me.rhunk.snapenhance.ui.util.coil.ImageRequestHelper
     36 import java.io.File
     37 import java.text.DateFormat
     38 import java.util.Date
     39 import java.util.UUID
     40 import kotlin.math.absoluteValue
     41 
     42 class LoggedStories : Routes.Route() {
     43     @OptIn(ExperimentalCoilApi::class, ExperimentalLayoutApi::class)
     44     override val content: @Composable (NavBackStackEntry) -> Unit = content@{ navBackStackEntry ->
     45         val userId = navBackStackEntry.arguments?.getString("id") ?: return@content
     46 
     47         val stories = remember { mutableStateListOf<StoryData>() }
     48         val friendInfo = remember { context.database.getFriendInfo(userId) }
     49         var lastStoryTimestamp by remember { mutableLongStateOf(Long.MAX_VALUE) }
     50 
     51         var selectedStory by remember { mutableStateOf<StoryData?>(null) }
     52 
     53         selectedStory?.let { story ->
     54             fun downloadSelectedStory(
     55                 inputMedia: InputMedia,
     56             ) {
     57                 val mediaAuthor = friendInfo?.mutableUsername ?: userId
     58                 val uniqueHash = UUID.randomUUID().toString().longHashCode().absoluteValue.toString(16)
     59 
     60                 DownloadProcessor(
     61                     remoteSideContext = context,
     62                     callback = object: DownloadCallback.Default() {
     63                         override fun onSuccess(outputPath: String?) {
     64                             context.shortToast("Downloaded to $outputPath")
     65                         }
     66 
     67                         override fun onFailure(message: String?, throwable: String?) {
     68                             context.shortToast("Failed to download $message")
     69                         }
     70                     }
     71                 ).enqueue(DownloadRequest(
     72                     inputMedias = arrayOf(inputMedia)
     73                 ), DownloadMetadata(
     74                     mediaIdentifier = uniqueHash,
     75                     outputPath = createNewFilePath(
     76                         context.config.root,
     77                         uniqueHash,
     78                         MediaDownloadSource.STORY_LOGGER,
     79                         mediaAuthor,
     80                         story.createdAt
     81                     ),
     82                     iconUrl = null,
     83                     mediaAuthor = friendInfo?.mutableUsername ?: userId,
     84                     downloadSource = MediaDownloadSource.STORY_LOGGER.translate(context.translation),
     85                 ))
     86             }
     87 
     88             Dialog(onDismissRequest = {
     89                 selectedStory = null
     90             }) {
     91                 Card(
     92                     modifier = Modifier
     93                         .padding(4.dp)
     94                 ) {
     95                     Column(
     96                         modifier = Modifier
     97                             .padding(16.dp)
     98                             .fillMaxWidth(),
     99                         verticalArrangement = Arrangement.spacedBy(8.dp),
    100                     ) {
    101                         remember {
    102                             story.postedAt.takeIf { it >= 0L }?.let {
    103                                 DateFormat.getDateTimeInstance().format(Date(it))
    104                             }
    105                         }?.let {
    106                             Text(text = "Posted at $it")
    107                         }
    108                         remember {
    109                             story.createdAt.takeIf { it >= 0L }?.let {
    110                                 DateFormat.getDateTimeInstance().format(Date(it))
    111                             }
    112                         }?.let {
    113                             Text(text = "Created at $it")
    114                         }
    115 
    116                         FlowRow(
    117                             modifier = Modifier.fillMaxWidth(),
    118                             horizontalArrangement = Arrangement.SpaceEvenly,
    119                         ) {
    120                             Button(onClick = {
    121                                 context.androidContext.externalCacheDir?.let { cacheDir ->
    122                                     context.imageLoader.diskCache?.openSnapshot(story.url)?.use { diskCacheSnapshot ->
    123                                         val cacheFile = diskCacheSnapshot.data.toFile()
    124                                         val targetFile = File(cacheDir, cacheFile.name).also {
    125                                             it.deleteOnExit()
    126                                         }
    127 
    128                                         runCatching {
    129                                             cacheFile.inputStream().let {
    130                                                 story.getEncryptionKeyPair()?.decryptInputStream(it) ?: it
    131                                             }.use { inputStream ->
    132                                                 targetFile.outputStream().use { outputStream ->
    133                                                     inputStream.copyTo(outputStream)
    134                                                 }
    135                                             }
    136 
    137                                             context.androidContext.startActivity(Intent().apply {
    138                                                 action = Intent.ACTION_VIEW
    139                                                 setDataAndType(
    140                                                     FileProvider.getUriForFile(
    141                                                         context.androidContext,
    142                                                         "me.rhunk.snapenhance.fileprovider",
    143                                                         targetFile
    144                                                     ),
    145                                                     FileType.fromFile(targetFile).mimeType
    146                                                 )
    147                                                 addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
    148                                             })
    149                                         }.onFailure {
    150                                             context.shortToast("Failed to open file. Check logs for more info")
    151                                             context.log.error("Failed to open file", it)
    152                                         }
    153                                     } ?: run {
    154                                         context.shortToast("Failed to get file")
    155                                         return@Button
    156                                     }
    157                                 }
    158                             }) {
    159                                 Text(text = context.translation["button.open"])
    160                             }
    161 
    162                             Button(onClick = {
    163                                 downloadSelectedStory(
    164                                     InputMedia(
    165                                         content = story.url,
    166                                         type = DownloadMediaType.REMOTE_MEDIA,
    167                                         encryption = story.getEncryptionKeyPair()
    168                                     )
    169                                 )
    170                             }) {
    171                                 Text(text = context.translation["button.download"])
    172                             }
    173 
    174                             if (remember {
    175                                 context.imageLoader.diskCache?.openSnapshot(story.url)?.also { it.close() } != null
    176                             }) {
    177                                 Button(onClick = {
    178                                     downloadSelectedStory(
    179                                         InputMedia(
    180                                             content = context.imageLoader.diskCache?.openSnapshot(story.url)?.use {
    181                                                 it.data.toFile().absolutePath
    182                                             } ?: run {
    183                                                 context.shortToast("Failed to get file")
    184                                                 return@Button
    185                                             },
    186                                             type = DownloadMediaType.LOCAL_MEDIA,
    187                                             encryption = story.getEncryptionKeyPair()
    188                                         )
    189                                     )
    190                                 }) {
    191                                     Text(text = translation["save_from_cache_button"])
    192                                 }
    193                             }
    194                         }
    195                     }
    196                 }
    197             }
    198         }
    199 
    200         if (stories.isEmpty()) {
    201             Text(text = translation["no_stories"], Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
    202         }
    203 
    204         LazyVerticalGrid(
    205             columns = GridCells.Adaptive(100.dp),
    206             contentPadding = PaddingValues(8.dp),
    207         ) {
    208             items(stories, key = { it.url }) { story ->
    209                 var hasFailed by remember(story.url) { mutableStateOf(false) }
    210 
    211                 Column(
    212                     modifier = Modifier
    213                         .padding(8.dp)
    214                         .clickable {
    215                             selectedStory = story
    216                         }
    217                         .clip(MaterialTheme.shapes.medium)
    218                         .heightIn(min = 128.dp),
    219                     horizontalAlignment = Alignment.CenterHorizontally,
    220                     verticalArrangement = Arrangement.Center,
    221                 ) {
    222                     if (hasFailed) {
    223                         Text(text = translation["story_failed_to_load"], Modifier.padding(8.dp), fontSize = 10.sp)
    224                     } else {
    225                         Image(
    226                             painter = rememberAsyncImagePainter(
    227                                 model = ImageRequestHelper.newPreviewImageRequest(
    228                                     context.androidContext,
    229                                     story.url,
    230                                     story.getEncryptionKeyPair(),
    231                                 ),
    232                                 imageLoader = context.imageLoader,
    233                                 onError = {
    234                                     hasFailed = true
    235                                 }
    236                             ),
    237                             contentDescription = null,
    238                             contentScale = ContentScale.FillWidth,
    239                             modifier = Modifier
    240                                 .fillMaxSize()
    241                                 .height(128.dp)
    242                         )
    243                     }
    244                 }
    245             }
    246             item {
    247                 LaunchedEffect(Unit) {
    248                     context.messageLogger.getStories(userId, lastStoryTimestamp, 20).also { result ->
    249                         stories.addAll(result.values.reversed())
    250                         result.keys.minOrNull()?.let {
    251                             lastStoryTimestamp = it
    252                         }
    253                     }
    254                 }
    255             }
    256         }
    257     }
    258 }