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 }