mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
This commit is contained in:
parent
a57fe608f9
commit
58f367b72b
|
@ -1,19 +1,33 @@
|
|||
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
|
||||
import com.blankj.utilcode.util.ZipUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.util.LOCAL_SUBS_IDS
|
||||
import li.songe.gkd.util.checkSubsUpdate
|
||||
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
|
||||
|
||||
@Serializable
|
||||
data class TransferData(
|
||||
private data class TransferData(
|
||||
val type: String = TYPE,
|
||||
val ctime: Long = System.currentTimeMillis(),
|
||||
|
||||
val subsItems: List<SubsItem> = emptyList(),
|
||||
val subscriptions: List<RawSubscription> = emptyList(),
|
||||
val subsConfigs: List<SubsConfig> = emptyList(),
|
||||
val categoryConfigs: List<CategoryConfig> = emptyList(),
|
||||
) {
|
||||
|
@ -22,16 +36,7 @@ data class TransferData(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun exportTransferData(subsItemIds: Collection<Long>): TransferData {
|
||||
return TransferData(
|
||||
subsItems = subsItemsFlow.value.filter { subsItemIds.contains(it.id) },
|
||||
subscriptions = subsIdToRawFlow.value.values.filter { it.id < 0 && subsItemIds.contains(it.id) },
|
||||
subsConfigs = DbSet.subsConfigDao.querySubsItemConfig(subsItemIds.toList()),
|
||||
categoryConfigs = DbSet.categoryConfigDao.querySubsItemConfig(subsItemIds.toList()),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun importTransferData(transferData: TransferData): Boolean {
|
||||
private suspend fun importTransferData(transferData: TransferData): Boolean {
|
||||
// TODO transaction
|
||||
val maxOrder = (subsItemsFlow.value.maxOfOrNull { it.order } ?: -1) + 1
|
||||
val subsItems =
|
||||
|
@ -44,10 +49,68 @@ suspend fun importTransferData(transferData: TransferData): Boolean {
|
|||
DbSet.subsItemDao.insertOrIgnore(*subsItems.toTypedArray())
|
||||
DbSet.subsConfigDao.insertOrIgnore(*transferData.subsConfigs.toTypedArray())
|
||||
DbSet.categoryConfigDao.insertOrIgnore(*transferData.categoryConfigs.toTypedArray())
|
||||
transferData.subscriptions.forEach { subscription ->
|
||||
return hasNewSubsItem
|
||||
}
|
||||
|
||||
suspend fun exportData(context: Context, subsIds: Collection<Long>) {
|
||||
if (subsIds.isEmpty()) return
|
||||
exportZipDir.resetDirectory()
|
||||
val dataFile = exportZipDir.resolve("${TransferData.TYPE}.json")
|
||||
dataFile.writeText(
|
||||
json.encodeToString(
|
||||
TransferData(
|
||||
subsItems = subsItemsFlow.value.filter { subsIds.contains(it.id) },
|
||||
subsConfigs = DbSet.subsConfigDao.querySubsItemConfig(subsIds.toList()),
|
||||
categoryConfigs = DbSet.categoryConfigDao.querySubsItemConfig(subsIds.toList()),
|
||||
)
|
||||
)
|
||||
)
|
||||
val files = exportZipDir.resolve("files").apply { mkdir() }
|
||||
subsIdToRawFlow.value.values.filter { it.id < 0 && subsIds.contains(it.id) }.forEach {
|
||||
val file = files.resolve("${it.id}.json")
|
||||
file.writeText(json.encodeToString(it))
|
||||
}
|
||||
val file = exportZipDir.resolve("backup-${System.currentTimeMillis()}.zip")
|
||||
ZipUtils.zipFiles(listOf(dataFile, files), file)
|
||||
dataFile.delete()
|
||||
files.deleteRecursively()
|
||||
context.shareFile(file, "分享数据文件")
|
||||
}
|
||||
|
||||
suspend fun importData(uri: Uri) {
|
||||
importZipDir.resetDirectory()
|
||||
val zipFile = importZipDir.resolve("import.zip")
|
||||
zipFile.writeBytes(UriUtils.uri2Bytes(uri))
|
||||
val unZipImportFile = importZipDir.resolve("unzipImport")
|
||||
ZipUtils.unzipFile(zipFile, unZipImportFile)
|
||||
val transferFile = unZipImportFile.resolve("${TransferData.TYPE}.json")
|
||||
if (!transferFile.exists() || !transferFile.isFile) {
|
||||
toast("导入无数据")
|
||||
return
|
||||
}
|
||||
val data = withContext(Dispatchers.Default) {
|
||||
json.decodeFromString<TransferData>(transferFile.readText())
|
||||
}
|
||||
val hasNewSubsItem = importTransferData(data)
|
||||
val files = unZipImportFile.resolve("files")
|
||||
val subscriptions = (files.listFiles { f -> f.isFile && f.name.endsWith(".json") }
|
||||
?: emptyArray()).mapNotNull { f ->
|
||||
try {
|
||||
RawSubscription.parse(f.readText())
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
subscriptions.forEach { subscription ->
|
||||
if (LOCAL_SUBS_IDS.contains(subscription.id)) {
|
||||
updateSubscription(subscription)
|
||||
}
|
||||
}
|
||||
return hasNewSubsItem
|
||||
toast("导入成功")
|
||||
importZipDir.resetDirectory()
|
||||
if (hasNewSubsItem) {
|
||||
delay(1000)
|
||||
checkSubsUpdate(true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package li.songe.gkd.ui.component
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
@ -33,27 +32,21 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.blankj.utilcode.util.ClipboardUtils
|
||||
import com.blankj.utilcode.util.ZipUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.TransferData
|
||||
import li.songe.gkd.data.deleteSubscription
|
||||
import li.songe.gkd.data.exportTransferData
|
||||
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.util.LOCAL_SUBS_ID
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.exportZipDir
|
||||
import li.songe.gkd.util.formatTimeAgo
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.openUri
|
||||
import li.songe.gkd.util.shareFile
|
||||
import li.songe.gkd.util.subsLoadErrorsFlow
|
||||
import li.songe.gkd.util.subsRefreshErrorsFlow
|
||||
import li.songe.gkd.util.subsRefreshingFlow
|
||||
|
@ -249,7 +242,7 @@ private fun SubsMenuItem(
|
|||
onClick = {
|
||||
onExpandedChange(false)
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
context.shareSubs(subItem.id)
|
||||
exportData(context, listOf(subItem.id))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -296,14 +289,3 @@ private fun SubsMenuItem(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Context.shareSubs(vararg subsIds: Long) {
|
||||
val transferDataFile = exportZipDir.resolve("${TransferData.TYPE}.json")
|
||||
transferDataFile.writeText(
|
||||
json.encodeToString(exportTransferData(subsIds.toList()))
|
||||
)
|
||||
val file = exportZipDir.resolve("backup-${System.currentTimeMillis()}.zip")
|
||||
ZipUtils.zipFiles(listOf(transferDataFile), file)
|
||||
transferDataFile.delete()
|
||||
this.shareFile(file, "分享数据文件")
|
||||
}
|
||||
|
|
|
@ -15,22 +15,15 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.UriUtils
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.MainActivity
|
||||
import li.songe.gkd.OpenFileActivity
|
||||
import li.songe.gkd.OpenSchemeActivity
|
||||
import li.songe.gkd.data.TransferData
|
||||
import li.songe.gkd.data.importTransferData
|
||||
import li.songe.gkd.data.importData
|
||||
import li.songe.gkd.util.ProfileTransitions
|
||||
import li.songe.gkd.util.checkSubsUpdate
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.readFileZipByteArray
|
||||
import li.songe.gkd.util.toast
|
||||
|
||||
data class BottomNavItem(
|
||||
|
@ -53,8 +46,7 @@ fun HomePage() {
|
|||
|
||||
val pages = arrayOf(controlPage, subsPage, appListPage, settingsPage)
|
||||
|
||||
val currentPage = pages.find { p -> p.navItem.label == tab.label }
|
||||
?: controlPage
|
||||
val currentPage = pages.find { p -> p.navItem.label == tab.label } ?: controlPage
|
||||
|
||||
val intent = context.intent
|
||||
LaunchedEffect(key1 = intent, block = {
|
||||
|
@ -67,23 +59,7 @@ fun HomePage() {
|
|||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
toast("加载导入...")
|
||||
vm.tabFlow.value = subsPage.navItem
|
||||
val string = readFileZipByteArray(
|
||||
UriUtils.uri2Bytes(uri),
|
||||
"${TransferData.TYPE}.json"
|
||||
)
|
||||
if (string != null) {
|
||||
val transferData = withContext(Dispatchers.Default) {
|
||||
json.decodeFromString<TransferData>(string)
|
||||
}
|
||||
val hasNewSubsItem = importTransferData(transferData)
|
||||
toast("导入成功")
|
||||
if (hasNewSubsItem) {
|
||||
delay(1000)
|
||||
checkSubsUpdate(true)
|
||||
}
|
||||
} else {
|
||||
toast("导入失败")
|
||||
}
|
||||
importData(uri)
|
||||
}
|
||||
} else if (source == OpenSchemeActivity::class.qualifiedName) {
|
||||
LogUtils.d(uri)
|
||||
|
@ -97,8 +73,7 @@ fun HomePage() {
|
|||
bottomBar = {
|
||||
NavigationBar {
|
||||
pages.forEach { page ->
|
||||
NavigationBarItem(
|
||||
selected = tab.label == page.navItem.label,
|
||||
NavigationBarItem(selected = tab.label == page.navItem.label,
|
||||
modifier = Modifier,
|
||||
onClick = {
|
||||
vm.tabFlow.value = page.navItem
|
||||
|
@ -111,8 +86,7 @@ fun HomePage() {
|
|||
},
|
||||
label = {
|
||||
Text(text = page.navItem.label)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -53,27 +53,23 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.blankj.utilcode.util.UriUtils
|
||||
import com.dylanc.activityresult.launcher.launchForResult
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.data.TransferData
|
||||
import li.songe.gkd.data.Value
|
||||
import li.songe.gkd.data.deleteSubscription
|
||||
import li.songe.gkd.data.importTransferData
|
||||
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.getDialogResult
|
||||
import li.songe.gkd.ui.component.shareSubs
|
||||
import li.songe.gkd.util.LOCAL_SUBS_ID
|
||||
import li.songe.gkd.util.LocalLauncher
|
||||
import li.songe.gkd.util.checkSubsUpdate
|
||||
import li.songe.gkd.util.isSafeUrl
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.readFileZipByteArray
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
import li.songe.gkd.util.subsItemsFlow
|
||||
import li.songe.gkd.util.subsRefreshingFlow
|
||||
|
@ -225,7 +221,7 @@ fun useSubsManagePage(): ScaffoldExt {
|
|||
}
|
||||
}
|
||||
IconButton(onClick = vm.viewModelScope.launchAsFn(Dispatchers.IO) {
|
||||
context.shareSubs(*selectedIds.toLongArray())
|
||||
exportData(context, selectedIds)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
|
@ -305,17 +301,7 @@ fun useSubsManagePage(): ScaffoldExt {
|
|||
toast("未选择文件")
|
||||
return@launchAsFn
|
||||
}
|
||||
val string = readFileZipByteArray(
|
||||
UriUtils.uri2Bytes(uri), "${TransferData.TYPE}.json"
|
||||
)
|
||||
if (string != null) {
|
||||
val transferData =
|
||||
json.decodeFromString<TransferData>(string)
|
||||
importTransferData(transferData)
|
||||
toast("导入成功")
|
||||
} else {
|
||||
toast("导入文件无数据")
|
||||
}
|
||||
importData(uri)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ val newVersionApkDir by lazy { cacheDir.resolve("newVersionApk") }
|
|||
val logZipDir by lazy { cacheDir.resolve("logZip") }
|
||||
val imageCacheDir by lazy { cacheDir.resolve("imageCache") }
|
||||
val exportZipDir by lazy { cacheDir.resolve("exportZip") }
|
||||
val importZipDir by lazy { cacheDir.resolve("exportZip") }
|
||||
|
||||
fun initFolder() {
|
||||
listOf(
|
||||
|
@ -31,7 +32,8 @@ fun initFolder() {
|
|||
newVersionApkDir,
|
||||
logZipDir,
|
||||
imageCacheDir,
|
||||
exportZipDir
|
||||
exportZipDir,
|
||||
importZipDir
|
||||
).forEach { f ->
|
||||
if (!f.exists()) {
|
||||
// TODO 在某些机型上无法创建目录 用户反馈重启手机后解决 是否存在其它解决方式?
|
||||
|
@ -46,18 +48,27 @@ fun clearCache() {
|
|||
newVersionApkDir,
|
||||
logZipDir,
|
||||
imageCacheDir,
|
||||
exportZipDir
|
||||
exportZipDir,
|
||||
importZipDir
|
||||
).forEach { dir ->
|
||||
if (dir.isDirectory && dir.exists()) {
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.isFile) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
dir.deleteRecursively()
|
||||
dir.mkdir()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun File.resetDirectory() {
|
||||
if (isFile) {
|
||||
delete()
|
||||
} else if (isDirectory) {
|
||||
deleteRecursively()
|
||||
}
|
||||
if (!exists()) {
|
||||
mkdir()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun buildLogFile(): File {
|
||||
val files = mutableListOf(dbFolder, subsFolder)
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
fun readFileZipByteArray(zipByteArray: ByteArray, fileName: String): String? {
|
||||
val byteArrayInputStream = ByteArrayInputStream(zipByteArray)
|
||||
val zipInputStream = ZipInputStream(byteArrayInputStream)
|
||||
zipInputStream.use {
|
||||
var zipEntry: ZipEntry? = zipInputStream.nextEntry
|
||||
while (zipEntry != null) {
|
||||
if (zipEntry.name == fileName) {
|
||||
val reader = BufferedReader(InputStreamReader(zipInputStream))
|
||||
val content = reader.use { it.readText() }
|
||||
return content
|
||||
}
|
||||
zipEntry = zipInputStream.nextEntry
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
Loading…
Reference in New Issue
Block a user