mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
This commit is contained in:
parent
36621c31b3
commit
8c12ee172f
|
@ -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" />
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user