@@ -2,6 +2,7 @@ package com.xayah.feature.main.restore
22
33import android.util.Log
44import androidx.compose.animation.ExperimentalAnimationApi
5+ import androidx.compose.foundation.ExperimentalFoundationApi // 新增
56import androidx.compose.foundation.layout.Arrangement
67import androidx.compose.foundation.layout.Column
78import androidx.compose.foundation.layout.Row
@@ -10,51 +11,46 @@ import androidx.compose.foundation.layout.fillMaxSize
1011import androidx.compose.foundation.layout.fillMaxWidth
1112import androidx.compose.foundation.layout.height
1213import androidx.compose.foundation.layout.padding
13- import androidx.compose.foundation.layout.size
14+ import androidx.compose.foundation.rememberScrollState
15+ import androidx.compose.foundation.verticalScroll // 新增
16+ import androidx.compose.material3.Button
17+ import androidx.compose.material3.ButtonDefaults
1418import androidx.compose.material3.ExperimentalMaterial3Api
1519import androidx.compose.material3.HorizontalDivider
16- import androidx.compose.material3.Icon
1720import androidx.compose.material3.MaterialTheme
1821import androidx.compose.material3.Text
1922import androidx.compose.material3.TopAppBarDefaults
2023import androidx.compose.material3.rememberTopAppBarState
2124import androidx.compose.runtime.Composable
25+ import androidx.compose.runtime.LaunchedEffect
2226import androidx.compose.runtime.getValue
27+ import androidx.compose.runtime.rememberCoroutineScope
2328import androidx.compose.ui.Alignment
2429import androidx.compose.ui.Modifier
2530import androidx.compose.ui.platform.LocalContext
26- import androidx.compose.ui.unit.dp
2731import androidx.hilt.navigation.compose.hiltViewModel
28- import androidx.navigation.NavController
29- import androidx.compose.foundation.ExperimentalFoundationApi
30- import androidx.compose.foundation.rememberScrollState
31- import androidx.compose.foundation.verticalScroll
3232import androidx.lifecycle.compose.collectAsStateWithLifecycle
33- import androidx.compose.runtime.rememberCoroutineScope
34- import androidx.compose.material.icons.Icons
35- import androidx.compose.material.icons.filled.Folder
36- import androidx.compose.runtime.LaunchedEffect
37- import com.xayah.core.ui.route.MainRoutes
38- import com.xayah.core.model.OpType
39- import com.xayah.core.util.navigateSingle
40- import com.xayah.core.util.localBackupSaveDir
41- import com.xayah.core.model.Target
33+ import androidx.navigation.NavController
34+ import com.xayah.core.datastore.readBackupDirectory
35+ import com.xayah.core.model.DataType
36+ import com.xayah.core.ui.component.confirm
4237import com.xayah.core.ui.component.BodyMediumText
38+ import com.xayah.core.ui.component.LocalSlotScope
4339import com.xayah.core.ui.component.PackageIconImage
44- import com.xayah.core.datastore.readBackupDirectory
4540import com.xayah.core.ui.component.ProgressButton
4641import com.xayah.core.ui.component.Title
4742import com.xayah.core.ui.component.TitleLargeText
43+ import com.xayah.core.ui.route.MainRoutes
4844import com.xayah.core.ui.theme.ThemedColorSchemeKeyTokens
4945import com.xayah.core.ui.theme.value
5046import com.xayah.core.ui.token.SizeTokens
5147import com.xayah.core.util.DateUtil
52- import com.xayah.core.model.DataType
53- import com.xayah.core.model.ResticProgressState
48+ import com.xayah.core.util.localBackupSaveDir
49+ import com.xayah.core.util.navigateSingle
5450import kotlinx.coroutines.launch
5551import java.net.URLEncoder
5652
57- @OptIn(ExperimentalMaterial3Api ::class , ExperimentalFoundationApi ::class , ExperimentalAnimationApi ::class )
53+ @OptIn(ExperimentalMaterial3Api ::class , ExperimentalAnimationApi ::class , ExperimentalFoundationApi ::class ) // 修改: 添加 ExperimentalFoundationApi
5854@Composable
5955fun CloudBackupDetailPage (
6056 navController : NavController ,
@@ -63,17 +59,23 @@ fun CloudBackupDetailPage(
6359 viewModel : CloudRestoreViewModel = hiltViewModel()
6460) {
6561 val resticProgress by viewModel.resticProgress.collectAsStateWithLifecycle()
62+ val dialogState = LocalSlotScope .current!! .dialogSlot // 修改: 从 LocalSlotScope 获取
63+ val coroutineScope = rememberCoroutineScope()
64+ val context = LocalContext .current
65+
6666 LaunchedEffect (accountName) {
6767 viewModel.setCloudEntity(accountName)
6868 }
69+
70+ val isDeleting = resticProgress.isDeleting
6971 val isRestoring = resticProgress.totalDataTypes > 0 &&
70- resticProgress.currentDataTypeIndex < resticProgress.totalDataTypes
71- val isCompleted = resticProgress.isCompleted
72- val buttonEnabled = ! isRestoring && ! isCompleted
73- val coroutineScope = rememberCoroutineScope()
74- val context = LocalContext .current
72+ resticProgress.currentDataTypeIndex < resticProgress.totalDataTypes &&
73+ ! isDeleting // 排除删除状态
74+
75+ val isCompleted = resticProgress.isCompleted && ! isDeleting
76+ val deleteButtonEnabled = ! isRestoring && ! isCompleted && ! isDeleting
77+ val restoreButtonEnabled = ! isRestoring && ! isCompleted && ! isDeleting
7578
76- // 计算进度信息
7779 val currentProgress = if (resticProgress.bytesTotal > 0 ) {
7880 resticProgress.bytesWritten.toFloat() / resticProgress.bytesTotal
7981 } else 0f
@@ -98,76 +100,119 @@ fun CloudBackupDetailPage(
98100 }
99101 return if (index < sortedBackups.size) {
100102 sortedBackups[index].dataType.type.uppercase()
101- } else {
102- " "
103- }
103+ } else " "
104104 }
105105
106+ val totalSnapshots = group.backups.size
107+ val totalSteps = totalSnapshots + 1 // 快照数量 + 1 (prune)
108+ val currentStep = if (isDeleting) {
109+ resticProgress.currentDataTypeIndex + 1
110+ } else {
111+ 0
112+ }
106113 RestoreScaffold (
107114 scrollBehavior = TopAppBarDefaults .exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
108115 title = " 云端备份详情" ,
109116 actions = {
110- ProgressButton (
117+ Column (
111118 modifier = Modifier .fillMaxWidth(),
112- progress = currentProgress,
113- currentIndex = currentIndex,
114- totalCount = totalCount,
115- speed = speed,
116- progressSize = progressSize,
117- enabled = buttonEnabled,
118- text = when {
119- isRestoring -> {
120- val currentDataType = getCurrentDataTypeName(group, currentIndex)
121- " 正在恢复${currentDataType} 快照"
119+ verticalArrangement = Arrangement .spacedBy(SizeTokens .Level8 )
120+ ) {
121+ // 删除按钮
122+ ProgressButton (
123+ modifier = Modifier .fillMaxWidth(),
124+ progress = 0f ,
125+ currentIndex = if (isDeleting) currentStep else 0 ,
126+ totalCount = if (isDeleting) totalSteps else 0 ,
127+ speed = " " ,
128+ progressSize = " " ,
129+ enabled = deleteButtonEnabled,
130+ text = if (isDeleting) {
131+ if (currentStep <= totalSnapshots) {
132+ val currentDataType = getCurrentDataTypeName(group, currentIndex)
133+ " 正在删除${currentDataType} 快照 ($currentStep /$totalSteps )"
134+ } else {
135+ " 正在清理存储空间 ($totalSteps /$totalSteps )"
136+ }
137+ } else {
138+ " 删除云端快照"
139+ },
140+ onClick = {
141+ if (! isDeleting) {
142+ coroutineScope.launch {
143+ if (dialogState.confirm(
144+ title = " 提示" ,
145+ text = " 确认删除该应用的所有云端快照?\n 共计 ${group.backups.size} 个快照"
146+ )) {
147+ val success = viewModel.deleteCloudSnapshots(group)
148+ if (success) {
149+ navController.popBackStack()
150+ }
151+ }
152+ }
153+ }
122154 }
123- isCompleted -> " 云端恢复已完成"
124- else -> " 恢复云端快照"
125- },
126- onClick = {
127- if (! isRestoring && ! isCompleted) {
128- coroutineScope.launch {
129- try {
130- Log .d(" CloudRestore" , " 用户点击恢复按钮,开始云端恢复流程" )
131- val success = viewModel.restoreFromCloudSnapshots(group)
132- Log .d(" CloudRestore" , " 云端恢复结果: $success " )
133-
134- if (success) {
135- Log .d(" CloudRestore" , " 云端恢复成功,准备读取备份目录" )
136- val backupDir = " ${context.localBackupSaveDir()} /restore/"
137- Log .d(" CloudRestore" , " 导航到恢复页面,备份目录: $backupDir " )
138- viewModel.refreshLocalDatabase(backupDir)
139-
140- // 在数据库刷新完成后计算应用大小
141- viewModel.calculateSizesForActivatedApps()
142- val route = MainRoutes .PackagesRestoreProcessingGraph .getRoute(
143- cloudName = URLEncoder .encode(" " , " UTF-8" ), // 改为空字符串,与本地恢复一致
144- backupDir = URLEncoder .encode(backupDir, " UTF-8" ),
145- packageName = group.packageName
146- )
147- Log .d(" Navigation" , " 构建路由: $route " )
148- navController.navigateSingle(route)
149- Log .d(" Navigation" , " 导航完成: CloudBackupDetailPage → PackagesRestoreProcessingGraph" )
150- } else {
151- Log .e(" CloudRestore" , " 云端恢复失败" )
155+ )
156+
157+ // 恢复按钮
158+ ProgressButton (
159+ modifier = Modifier .fillMaxWidth(),
160+ progress = currentProgress,
161+ currentIndex = currentIndex,
162+ totalCount = totalCount,
163+ speed = speed,
164+ progressSize = progressSize,
165+ enabled = restoreButtonEnabled,
166+ text = when {
167+ isRestoring -> {
168+ val currentDataType = getCurrentDataTypeName(group, currentIndex)
169+ " 正在恢复${currentDataType} 快照"
170+ }
171+ isCompleted -> " 云端恢复已完成"
172+ else -> " 恢复云端快照"
173+ },
174+ onClick = {
175+ if (! isRestoring && ! isCompleted && ! isDeleting) {
176+ coroutineScope.launch {
177+ try {
178+ Log .d(" CloudRestore" , " 用户点击恢复按钮,开始云端恢复流程" )
179+ val success = viewModel.restoreFromCloudSnapshots(group)
180+ Log .d(" CloudRestore" , " 云端恢复结果: $success " )
181+
182+ if (success) {
183+ Log .d(" CloudRestore" , " 云端恢复成功,准备读取备份目录" )
184+ val backupDir = " ${context.localBackupSaveDir()} /restore/"
185+ Log .d(" CloudRestore" , " 导航到恢复页面,备份目录: $backupDir " )
186+ viewModel.refreshLocalDatabase(backupDir)
187+ viewModel.calculateSizesForActivatedApps()
188+
189+ val route = MainRoutes .PackagesRestoreProcessingGraph .getRoute(
190+ cloudName = URLEncoder .encode(" " , " UTF-8" ),
191+ backupDir = URLEncoder .encode(backupDir, " UTF-8" ),
192+ packageName = group.packageName
193+ )
194+ Log .d(" Navigation" , " 构建路由: $route " )
195+ navController.navigateSingle(route)
196+ Log .d(" Navigation" , " 导航完成: CloudBackupDetailPage → PackagesRestoreProcessingGraph" )
197+ } else {
198+ Log .e(" CloudRestore" , " 云端恢复失败" )
199+ }
200+ } catch (e: Exception ) {
201+ Log .e(" CloudRestore" , " 云端恢复流程异常: ${e.message} " , e)
152202 }
153- } catch (e: Exception ) {
154- Log .e(" CloudRestore" , " 云端恢复流程异常: ${e.message} " , e)
155203 }
156204 }
157- } else {
158- Log .d(" CloudRestore" , " 云端恢复正在进行中,忽略点击" )
159205 }
160- }
161- )
206+ )
207+ }
162208 }
163209 ) {
164- Column (
210+ Column (
165211 modifier = Modifier
166212 .fillMaxSize()
167213 .padding(SizeTokens .Level16 )
168214 .verticalScroll(rememberScrollState())
169215 ) {
170- // APP图标和基本信息
171216 Row (
172217 modifier = Modifier .fillMaxWidth(),
173218 verticalAlignment = Alignment .CenterVertically ,
@@ -186,23 +231,17 @@ fun CloudBackupDetailPage(
186231 color = ThemedColorSchemeKeyTokens .Outline .value
187232 )
188233 BodyMediumText (
189- text = DateUtil .formatTimestamp(
190- group.timestamp,
191- DateUtil .PATTERN_YMD_HMS
192- ),
234+ text = DateUtil .formatTimestamp(group.timestamp, DateUtil .PATTERN_YMD_HMS ),
193235 color = ThemedColorSchemeKeyTokens .Outline .value
194236 )
195237 }
196238 }
197239
198240 Spacer (modifier = Modifier .height(SizeTokens .Level24 ))
199241
200- // 备份类型详情
201242 Title (title = " 备份类型详情" ) {
202243 group.backups.forEach { backup ->
203- Column (
204- modifier = Modifier .padding(vertical = SizeTokens .Level8 )
205- ) {
244+ Column (modifier = Modifier .padding(vertical = SizeTokens .Level8 )) {
206245 Row (
207246 modifier = Modifier .fillMaxWidth(),
208247 horizontalArrangement = Arrangement .SpaceBetween ,
@@ -225,10 +264,7 @@ fun CloudBackupDetailPage(
225264 )
226265 }
227266 }
228-
229- HorizontalDivider (
230- modifier = Modifier .padding(vertical = SizeTokens .Level4 )
231- )
267+ HorizontalDivider (modifier = Modifier .padding(vertical = SizeTokens .Level4 ))
232268 }
233269 }
234270 }
0 commit comments