feat: 上传快照生成链接

This commit is contained in:
lisonge 2023-10-08 11:48:21 +08:00
parent 3b67d31a3e
commit 6e5d6b3e26
5 changed files with 212 additions and 3 deletions

View File

@ -0,0 +1,9 @@
package li.songe.gkd.data
import kotlinx.serialization.Serializable
@Serializable
data class GithubPoliciesAsset(
val id: Int,
val href: String,
)

View File

@ -49,7 +49,7 @@ object SnapshotExt {
suspend fun getSnapshotZipFile(snapshotId: Long): File {
val file = File(snapshotZipDir, "${snapshotId}.zip")
if (file.exists()) {
file.delete()
return file
}
withContext(Dispatchers.IO) {
ZipUtils.zipFiles(

View File

@ -21,8 +21,11 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.AlertDialog
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -30,21 +33,25 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.core.content.FileProvider
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ClipboardUtils
import com.blankj.utilcode.util.ImageUtils
import com.blankj.utilcode.util.ToastUtils
import com.blankj.utilcode.util.UriUtils
import com.dylanc.activityresult.launcher.launchForResult
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import li.songe.gkd.data.Snapshot
@ -52,6 +59,7 @@ import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt
import li.songe.gkd.ui.component.SimpleTopAppBar
import li.songe.gkd.ui.destinations.ImagePreviewPageDestination
import li.songe.gkd.util.LoadStatus
import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.LocalPickContentLauncher
import li.songe.gkd.util.LocalRequestPermissionLauncher
@ -59,6 +67,10 @@ import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.format
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.navigate
import li.songe.gkd.util.recordStoreFlow
import li.songe.gkd.util.snapshotZipDir
import li.songe.gkd.util.updateStorage
import java.io.File
@RootNavGraph
@Destination(style = ProfileTransitions::class)
@ -73,6 +85,8 @@ fun SnapshotPage() {
val vm = hiltViewModel<SnapshotVm>()
val snapshots by vm.snapshotsState.collectAsState()
val uploadStatus by vm.uploadStatusFlow.collectAsState()
val recordStore by recordStoreFlow.collectAsState()
var selectedSnapshot by remember {
mutableStateOf<Snapshot?>(null)
@ -143,7 +157,7 @@ fun SnapshotPage() {
.then(modifier)
)
Text(
text = "分享(注意隐私信息)",
text = "分享",
modifier = Modifier
.clickable(onClick = vm.viewModelScope.launchAsFn {
val zipFile = SnapshotExt.getSnapshotZipFile(snapshotVal.id)
@ -162,6 +176,27 @@ fun SnapshotPage() {
})
.then(modifier)
)
if (recordStore.snapshotIdMap.containsKey(snapshotVal.id)) {
Text(
text = "复制链接", modifier = Modifier
.clickable(onClick = {
selectedSnapshot = null
ClipboardUtils.copyText("https://gkd-kit.gitee.io/import/" + recordStore.snapshotIdMap[snapshotVal.id])
ToastUtils.showShort("复制成功")
})
.then(modifier)
)
} else {
Text(
text = "生成链接(需科学上网)", modifier = Modifier
.clickable(onClick = {
selectedSnapshot = null
vm.uploadZip(snapshotVal)
})
.then(modifier)
)
}
Text(
text = "保存截图到相册",
modifier = Modifier
@ -185,7 +220,7 @@ fun SnapshotPage() {
.then(modifier)
)
Text(
text = "从相册替换截图",
text = "替换截图(去除隐私)",
modifier = Modifier
.clickable(onClick = vm.viewModelScope.launchAsFn {
val uri = pickContentLauncher.launchForImageResult()
@ -195,6 +230,19 @@ fun SnapshotPage() {
val newBitmap = ImageUtils.getBitmap(newBytes, 0)
if (oldBitmap.width == newBitmap.width && oldBitmap.height == newBitmap.height) {
snapshotVal.screenshotFile.writeBytes(newBytes)
File(snapshotZipDir, "${snapshotVal.id}.zip").apply {
if (exists()) delete()
}
if (recordStore.snapshotIdMap.containsKey(snapshotVal.id)) {
updateStorage(
recordStoreFlow,
recordStore.copy(snapshotIdMap = recordStore.snapshotIdMap
.toMutableMap()
.apply {
remove(snapshotVal.id)
})
)
}
} else {
ToastUtils.showShort("截图尺寸不一致,无法替换")
return@withContext
@ -211,6 +259,16 @@ fun SnapshotPage() {
DbSet.snapshotDao.delete(snapshotVal)
withContext(IO) {
SnapshotExt.remove(snapshotVal.id)
if (recordStore.snapshotIdMap.containsKey(snapshotVal.id)) {
updateStorage(
recordStoreFlow,
recordStore.copy(snapshotIdMap = recordStore.snapshotIdMap
.toMutableMap()
.apply {
remove(snapshotVal.id)
})
)
}
}
selectedSnapshot = null
})
@ -220,6 +278,81 @@ fun SnapshotPage() {
}
}
}
when (val uploadStatusVal = uploadStatus) {
is LoadStatus.Failure -> {
AlertDialog(
title = { Text(text = "上传失败") },
text = {
Text(text = uploadStatusVal.exception.let {
it.message ?: it.toString()
})
},
onDismissRequest = { vm.uploadStatusFlow.value = null },
confirmButton = {
TextButton(onClick = {
vm.uploadStatusFlow.value = null
}) {
Text(text = "关闭")
}
},
)
}
is LoadStatus.Loading -> {
Dialog(onDismissRequest = { }) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "上传文件中,请稍等",
fontSize = 16.sp,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
)
Spacer(modifier = Modifier.height(15.dp))
LinearProgressIndicator(progress = uploadStatusVal.progress)
Spacer(modifier = Modifier.height(5.dp))
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
) {
TextButton(onClick = {
vm.uploadJob?.cancel(CancellationException("终止上传"))
vm.uploadJob = null
}) {
Text(text = "终止上传", color = Color.Red)
}
}
}
}
}
is LoadStatus.Success -> {
AlertDialog(title = { Text(text = "上传完成") }, text = {
Text(text = "https://gkd-kit.gitee.io/import/" + uploadStatusVal.result.id)
}, onDismissRequest = {}, dismissButton = {
TextButton(onClick = {
vm.uploadStatusFlow.value = null
}) {
Text(text = "关闭")
}
}, confirmButton = {
TextButton(onClick = {
ClipboardUtils.copyText("https://gkd-kit.gitee.io/import/" + uploadStatusVal.result.id)
ToastUtils.showShort("复制成功")
vm.uploadStatusFlow.value = null
}) {
Text(text = "复制")
}
})
}
else -> {}
}
}

View File

@ -3,14 +3,78 @@ package li.songe.gkd.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.ktor.client.call.body
import io.ktor.client.plugins.onUpload
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData
import io.ktor.client.statement.bodyAsText
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import li.songe.gkd.data.GithubPoliciesAsset
import li.songe.gkd.data.RpcError
import li.songe.gkd.data.Snapshot
import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt.getSnapshotZipFile
import li.songe.gkd.util.LoadStatus
import li.songe.gkd.util.Singleton
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.recordStoreFlow
import li.songe.gkd.util.updateStorage
import javax.inject.Inject
const val FILE_UPLOAD_URL = "https://github-upload-assets.lisonge.workers.dev/"
@HiltViewModel
class SnapshotVm @Inject constructor() : ViewModel() {
val snapshotsState = DbSet.snapshotDao.query().map { it.reversed() }
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val uploadStatusFlow = MutableStateFlow<LoadStatus<GithubPoliciesAsset>?>(null)
var uploadJob: Job? = null
fun uploadZip(snapshot: Snapshot) {
uploadJob = viewModelScope.launchTry(Dispatchers.IO) {
val zipFile = getSnapshotZipFile(snapshot.id)
uploadStatusFlow.value = LoadStatus.Loading()
try {
val response = Singleton.client.submitFormWithBinaryData(url = FILE_UPLOAD_URL,
formData = formData {
append("\"file\"", zipFile.readBytes(), Headers.build {
append(HttpHeaders.ContentType, "application/x-zip-compressed")
append(HttpHeaders.ContentDisposition, "filename=\"file.zip\"")
})
}) {
onUpload { bytesSentTotal, contentLength ->
if (uploadStatusFlow.value is LoadStatus.Loading) {
uploadStatusFlow.value =
LoadStatus.Loading(bytesSentTotal / contentLength.toFloat())
}
}
}
if (response.headers["X_RPC_OK"] == "true") {
val policiesAsset = response.body<GithubPoliciesAsset>()
uploadStatusFlow.value = LoadStatus.Success(policiesAsset)
updateStorage(
recordStoreFlow,
recordStoreFlow.value.copy(snapshotIdMap = recordStoreFlow.value.snapshotIdMap.toMutableMap()
.apply {
set(snapshot.id, policiesAsset.id)
})
)
} else if (response.headers["X_RPC_OK"] == "false") {
uploadStatusFlow.value = LoadStatus.Failure(response.body<RpcError>())
} else {
uploadStatusFlow.value = LoadStatus.Failure(Exception(response.bodyAsText()))
}
} catch (e: Exception) {
uploadStatusFlow.value = LoadStatus.Failure(e)
}
}
}
}

View File

@ -0,0 +1,3 @@
package li.songe.gkd.util
const val VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION"