perf: share save file
Some checks failed
Build-Apk / build (push) Has been cancelled

This commit is contained in:
lisonge 2024-08-02 23:07:25 +08:00
parent 36621c31b3
commit 8c12ee172f
9 changed files with 141 additions and 31 deletions

View File

@ -23,7 +23,7 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- save image to album -->
<!-- save image to album, save file to Downloads -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

View File

@ -1,6 +1,5 @@
package li.songe.gkd.data
import android.content.Context
import android.net.Uri
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.UriUtils
@ -17,11 +16,11 @@ import li.songe.gkd.util.exportZipDir
import li.songe.gkd.util.importZipDir
import li.songe.gkd.util.json
import li.songe.gkd.util.resetDirectory
import li.songe.gkd.util.shareFile
import li.songe.gkd.util.subsIdToRawFlow
import li.songe.gkd.util.subsItemsFlow
import li.songe.gkd.util.toast
import li.songe.gkd.util.updateSubscription
import java.io.File
@Serializable
private data class TransferData(
@ -52,8 +51,7 @@ private suspend fun importTransferData(transferData: TransferData): Boolean {
return hasNewSubsItem
}
suspend fun exportData(context: Context, subsIds: Collection<Long>) {
if (subsIds.isEmpty()) return
suspend fun exportData(subsIds: Collection<Long>):File {
exportZipDir.resetDirectory()
val dataFile = exportZipDir.resolve("${TransferData.TYPE}.json")
dataFile.writeText(
@ -74,7 +72,7 @@ suspend fun exportData(context: Context, subsIds: Collection<Long>) {
ZipUtils.zipFiles(listOf(dataFile, files), file)
dataFile.delete()
files.deleteRecursively()
context.shareFile(file, "分享数据文件")
return file
}
suspend fun importData(uri: Uri) {

View File

@ -125,7 +125,7 @@ val canDrawOverlaysState by lazy {
)
}
val canSaveToAlbumState by lazy {
val canWriteExternalStorage by lazy {
PermissionState(
check = {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@ -170,7 +170,7 @@ suspend fun updatePermissionState() {
arrayOf(
notificationState,
canDrawOverlaysState,
canSaveToAlbumState,
canWriteExternalStorage,
shizukuOkState
).forEach { it.updateAndGet() }
if (canQueryPkgState.stateFlow.value != canQueryPkgState.updateAndGet()) {

View File

@ -55,7 +55,7 @@ import li.songe.gkd.MainActivity
import li.songe.gkd.data.Snapshot
import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt
import li.songe.gkd.permission.canSaveToAlbumState
import li.songe.gkd.permission.canWriteExternalStorage
import li.songe.gkd.permission.requiredPermission
import li.songe.gkd.ui.component.StartEllipsisText
import li.songe.gkd.ui.destinations.ImagePreviewPageDestination
@ -66,6 +66,7 @@ import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.LocalPickContentLauncher
import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.saveFileToDownloads
import li.songe.gkd.util.shareFile
import li.songe.gkd.util.snapshotZipDir
import li.songe.gkd.util.throttle
@ -195,17 +196,31 @@ fun SnapshotPage() {
)
HorizontalDivider()
Text(
text = "分享",
text = "分享数据",
modifier = Modifier
.clickable(onClick = vm.viewModelScope.launchAsFn {
val zipFile =
SnapshotExt.getSnapshotZipFile(
snapshotVal.id,
snapshotVal.appId,
snapshotVal.activityId
)
context.shareFile(zipFile, "分享快照文件")
selectedSnapshot = null
val zipFile = SnapshotExt.getSnapshotZipFile(
snapshotVal.id,
snapshotVal.appId,
snapshotVal.activityId
)
context.shareFile(zipFile, "分享快照文件")
})
.then(modifier)
)
HorizontalDivider()
Text(
text = "保存到下载",
modifier = Modifier
.clickable(onClick = vm.viewModelScope.launchAsFn {
selectedSnapshot = null
val zipFile = SnapshotExt.getSnapshotZipFile(
snapshotVal.id,
snapshotVal.appId,
snapshotVal.activityId
)
context.saveFileToDownloads(zipFile)
})
.then(modifier)
)
@ -236,7 +251,7 @@ fun SnapshotPage() {
text = "保存截图到相册",
modifier = Modifier
.clickable(onClick = vm.viewModelScope.launchAsFn {
requiredPermission(context, canSaveToAlbumState)
requiredPermission(context, canWriteExternalStorage)
ImageUtils.save2Album(
ImageUtils.getBitmap(snapshotVal.screenshotFile),
Bitmap.CompressFormat.PNG,

View File

@ -28,7 +28,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ClipboardUtils
import com.ramcosta.composedestinations.navigation.navigate
@ -36,10 +35,10 @@ import kotlinx.coroutines.Dispatchers
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.deleteSubscription
import li.songe.gkd.data.exportData
import li.songe.gkd.ui.destinations.CategoryPageDestination
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
import li.songe.gkd.ui.destinations.SubsPageDestination
import li.songe.gkd.ui.home.HomeVm
import li.songe.gkd.util.LOCAL_SUBS_ID
import li.songe.gkd.util.LocalMainViewModel
import li.songe.gkd.util.LocalNavController
@ -61,7 +60,7 @@ fun SubsItemCard(
subsItem: SubsItem,
subscription: RawSubscription?,
index: Int,
vm: ViewModel,
vm: HomeVm,
isSelectedMode: Boolean,
isSelected: Boolean,
onCheckedChange: ((Boolean) -> Unit)? = null,
@ -192,7 +191,7 @@ private fun SubsMenuItem(
onExpandedChange: ((Boolean) -> Unit),
subItem: SubsItem,
subscription: RawSubscription?,
vm: ViewModel
vm: HomeVm
) {
val navController = LocalNavController.current
val context = LocalContext.current
@ -243,7 +242,7 @@ private fun SubsMenuItem(
onClick = {
onExpandedChange(false)
vm.viewModelScope.launchTry(Dispatchers.IO) {
exportData(context, listOf(subItem.id))
vm.showShareDataIdsFlow.value = setOf(subItem.id)
}
}
)

View File

@ -218,4 +218,6 @@ class HomeVm @Inject constructor() : ViewModel() {
val clickLogCountFlow =
DbSet.clickLogDao.count().stateIn(viewModelScope, SharingStarted.Eagerly, 0)
val showShareDataIdsFlow = MutableStateFlow<Set<Long>?>(null)
}

View File

@ -66,6 +66,7 @@ import li.songe.gkd.util.checkUpdate
import li.songe.gkd.util.findOption
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.saveFileToDownloads
import li.songe.gkd.util.shareFile
import li.songe.gkd.util.storeFlow
import li.songe.gkd.util.throttle
@ -149,7 +150,7 @@ fun useSettingsPage(): ScaffoldExt {
.fillMaxWidth()
.padding(16.dp)
Text(
text = "调用系统分享", modifier = Modifier
text = "分享数据", modifier = Modifier
.clickable(onClick = throttle {
showShareLogDlg = false
vm.viewModelScope.launchTry(Dispatchers.IO) {
@ -159,6 +160,17 @@ fun useSettingsPage(): ScaffoldExt {
})
.then(modifier)
)
Text(
text = "保存到下载", modifier = Modifier
.clickable(onClick = throttle {
showShareLogDlg = false
vm.viewModelScope.launchTry(Dispatchers.IO) {
val logZipFile = buildLogFile()
context.saveFileToDownloads(logZipFile)
}
})
.then(modifier)
)
Text(
text = "生成链接(需科学上网)",
modifier = Modifier

View File

@ -3,6 +3,7 @@ package li.songe.gkd.ui.home
import android.content.Intent
import android.webkit.URLUtil
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
import androidx.compose.material.icons.filled.Add
@ -24,6 +26,7 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton
@ -51,19 +54,20 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.dylanc.activityresult.launcher.launchForResult
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import li.songe.gkd.MainActivity
import li.songe.gkd.data.Value
import li.songe.gkd.data.deleteSubscription
import li.songe.gkd.data.exportData
import li.songe.gkd.data.importData
import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.SubsItemCard
import li.songe.gkd.ui.component.getResult
import li.songe.gkd.ui.component.waitResult
import li.songe.gkd.util.LOCAL_SUBS_ID
import li.songe.gkd.util.LocalLauncher
@ -72,9 +76,12 @@ import li.songe.gkd.util.checkSubsUpdate
import li.songe.gkd.util.isSafeUrl
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.saveFileToDownloads
import li.songe.gkd.util.shareFile
import li.songe.gkd.util.subsIdToRawFlow
import li.songe.gkd.util.subsItemsFlow
import li.songe.gkd.util.subsRefreshingFlow
import li.songe.gkd.util.throttle
import li.songe.gkd.util.toast
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@ -86,7 +93,6 @@ val subsNav = BottomNavItem(
@Composable
fun useSubsManagePage(): ScaffoldExt {
val launcher = LocalLauncher.current
val context = LocalContext.current
val mainVm = LocalMainViewModel.current
val vm = hiltViewModel<HomeVm>()
@ -171,6 +177,8 @@ fun useSubsManagePage(): ScaffoldExt {
})
}
ShareDataDialog(vm)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
return ScaffoldExt(
navItem = subsNav,
@ -205,11 +213,10 @@ fun useSubsManagePage(): ScaffoldExt {
}
if (canDeleteIds.isNotEmpty()) {
IconButton(onClick = vm.viewModelScope.launchAsFn {
val result = mainVm.dialogFlow.getResult(
mainVm.dialogFlow.waitResult(
title = "删除订阅",
text = "是否删除所选 ${canDeleteIds.size} 个订阅?\n\n注: 不包含本地订阅",
)
if (!result) return@launchAsFn
deleteSubscription(*canDeleteIds.toLongArray())
selectedIds = selectedIds - canDeleteIds
if (selectedIds.size == canDeleteIds.size) {
@ -223,7 +230,7 @@ fun useSubsManagePage(): ScaffoldExt {
}
}
IconButton(onClick = vm.viewModelScope.launchAsFn(Dispatchers.IO) {
exportData(context, selectedIds)
vm.showShareDataIdsFlow.value = selectedIds
}) {
Icon(
imageVector = Icons.Default.Share,
@ -425,4 +432,47 @@ fun useSubsManagePage(): ScaffoldExt {
)
}
}
}
@Composable
private fun ShareDataDialog(vm: HomeVm) {
val context = LocalContext.current as MainActivity
val showShareDataIds = vm.showShareDataIdsFlow.collectAsState().value
if (showShareDataIds != null) {
Dialog(onDismissRequest = { vm.showShareDataIdsFlow.value = null }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
val modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
Text(
text = "分享数据", modifier = Modifier
.clickable(onClick = throttle {
vm.showShareDataIdsFlow.value = null
vm.viewModelScope.launchTry(Dispatchers.IO) {
val file = exportData(showShareDataIds)
context.shareFile(file, "分享数据文件")
}
})
.then(modifier)
)
Text(
text = "保存到下载",
modifier = Modifier
.clickable(onClick = throttle {
vm.showShareDataIdsFlow.value = null
vm.viewModelScope.launchTry(Dispatchers.IO) {
val file = exportData(showShareDataIds)
context.saveFileToDownloads(file)
}
})
.then(modifier)
)
}
}
}
}

View File

@ -1,14 +1,23 @@
package li.songe.gkd.util
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import li.songe.gkd.MainActivity
import li.songe.gkd.permission.canWriteExternalStorage
import li.songe.gkd.permission.requiredPermission
import java.io.File
fun Context.shareFile(file: File, tile: String) {
fun Context.shareFile(file: File, title: String) {
val uri = FileProvider.getUriForFile(
this, "${packageName}.provider", file
)
@ -21,11 +30,36 @@ fun Context.shareFile(file: File, tile: String) {
}
tryStartActivity(
Intent.createChooser(
intent, tile
intent, title
)
)
}
suspend fun MainActivity.saveFileToDownloads(file: File) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
requiredPermission(this, canWriteExternalStorage)
val targetFile = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
file.name
)
targetFile.writeBytes(file.readBytes())
} else {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, file.name)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
withContext(Dispatchers.IO) {
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
?: error("创建URI失败")
contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(file.readBytes())
outputStream.flush()
}
}
}
toast("已保存 ${file.name} 到下载")
}
fun Context.tryStartActivity(intent: Intent) {
try {
startActivity(intent)