@@ -7,55 +7,61 @@ import androidx.compose.animation.ExperimentalAnimationApi
77import androidx.compose.foundation.layout.Arrangement
88import androidx.compose.foundation.layout.Column
99import androidx.compose.foundation.layout.ExperimentalLayoutApi
10+ import androidx.compose.foundation.layout.Spacer
1011import androidx.compose.foundation.layout.fillMaxSize
12+ import androidx.compose.foundation.layout.height
13+ import androidx.compose.foundation.layout.padding
14+ import androidx.compose.foundation.layout.fillMaxWidth
1115import androidx.compose.foundation.rememberScrollState
1216import androidx.compose.foundation.verticalScroll
1317import androidx.compose.material.icons.Icons
1418import androidx.compose.material.icons.outlined.Block
19+ import androidx.compose.material3.AlertDialog
20+ import androidx.compose.material3.Button
1521import androidx.compose.material3.ExperimentalMaterial3Api
22+ import androidx.compose.material3.LinearProgressIndicator
23+ import androidx.compose.material3.MaterialTheme
24+ import androidx.compose.material3.OutlinedTextField
25+ import androidx.compose.material3.Scaffold
26+ import androidx.compose.material3.Text
27+ import androidx.compose.material3.TextButton
28+ import androidx.compose.material3.TopAppBar
1629import androidx.compose.material3.TopAppBarDefaults
1730import androidx.compose.material3.rememberTopAppBarState
1831import androidx.compose.runtime.Composable
32+ import androidx.compose.runtime.LaunchedEffect
1933import androidx.compose.runtime.getValue
34+ import androidx.compose.runtime.mutableStateOf
35+ import androidx.compose.runtime.remember
36+ import androidx.compose.runtime.rememberCoroutineScope
37+ import androidx.compose.runtime.setValue
2038import androidx.compose.ui.Modifier
2139import androidx.compose.ui.graphics.vector.ImageVector
2240import androidx.compose.ui.platform.LocalContext
2341import androidx.compose.ui.res.stringResource
2442import androidx.compose.ui.res.vectorResource
43+ import androidx.compose.ui.unit.dp
2544import androidx.hilt.navigation.compose.hiltViewModel
2645import androidx.lifecycle.compose.collectAsStateWithLifecycle
27- import androidx.compose.material3.Text
28- import androidx.compose.foundation.layout.padding
29- // DataStore Keys
3046import com.xayah.core.datastore.KeyAutoScreenOff
3147import com.xayah.core.datastore.KeyMonet
32- import com.xayah.core.datastore.KeyResticEnableCompression // 引入 Restic 压缩 Key
33- // UI Components
48+ import com.xayah.core.datastore.KeyResticEnableCompression
3449import com.xayah.core.ui.component.Clickable
3550import com.xayah.core.ui.component.InnerBottomSpacer
3651import com.xayah.core.ui.component.Switchable
3752import com.xayah.core.ui.component.Title
38- import androidx.compose.material3.Scaffold
39- import androidx.compose.material3.TopAppBar // 补充导入
40- import androidx.compose.runtime.LaunchedEffect
41- // Navigation and Routing
4253import com.xayah.core.ui.route.MainRoutes
43- import com.xayah.core.ui.util.LocalNavController // 补充导入
44- // Tokens and Utils
4554import com.xayah.core.ui.token.SizeTokens
55+ import com.xayah.core.ui.util.LocalNavController
4656import com.xayah.core.util.LanguageUtil
4757import com.xayah.core.util.getActivity
4858import com.xayah.core.util.navigateSingle
4959import com.xayah.core.util.readMappedLanguage
50- // Activities and ViewModels
51- import com.xayah.feature.setup.MainActivity as SetupActivity
52- import com.xayah.feature.main.settings.R
53- import com.xayah.feature.main.settings.IndexViewModel // 补充导入
5460import com.xayah.feature.main.settings.restic.ResticViewModel
61+ import com.xayah.feature.setup.MainActivity as SetupActivity
62+ import kotlinx.coroutines.launch
5563
56- @ExperimentalLayoutApi
57- @ExperimentalAnimationApi
58- @ExperimentalMaterial3Api
64+ @OptIn(ExperimentalLayoutApi ::class , ExperimentalAnimationApi ::class , ExperimentalMaterial3Api ::class )
5965@Composable
6066fun PageSettings () {
6167 val context = LocalContext .current
@@ -64,21 +70,28 @@ fun PageSettings() {
6470 val resticViewModel = hiltViewModel<ResticViewModel >()
6571 val directoryState by viewModel.directoryState.collectAsStateWithLifecycle()
6672 val scrollBehavior = TopAppBarDefaults .exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
73+ val scope = rememberCoroutineScope() // 用于在 UI 事件中启动协程
6774
68- // 收集状态
75+ // 状态收集
6976 val resticVersion by resticViewModel.resticVersionState.collectAsStateWithLifecycle()
7077 val resticInitialized by resticViewModel.resticInitializedState.collectAsStateWithLifecycle(initialValue = false )
7178 val snapshotCount by resticViewModel.resticSnapshotCountState.collectAsStateWithLifecycle(initialValue = 0 )
7279 val repoPath by resticViewModel.repoPathState.collectAsStateWithLifecycle()
7380 val resticError by resticViewModel.resticErrorState.collectAsStateWithLifecycle()
74- // 触发 Restic 状态检查
75- LaunchedEffect (Unit ) {
76- resticViewModel.checkResticStatus()
81+ val downloadState by resticViewModel.downloadState.collectAsStateWithLifecycle()
82+
83+ var showDownloadDialog by remember { mutableStateOf(false ) }
84+
85+ // 逻辑:自动弹出下载对话框
86+ LaunchedEffect (resticVersion, downloadState) {
87+ if (resticVersion == null && downloadState is ResticViewModel .DownloadState .Idle ) {
88+ showDownloadDialog = true
89+ }
7790 }
7891
79- // 监听状态变化
80- LaunchedEffect (resticVersion, resticInitialized, snapshotCount ) {
81- Log .d( " Settings " , " Restic状态: Version= $resticVersion , Initialized= $resticInitialized , Snapshots= $snapshotCount " )
92+ // 逻辑:进入页面检查状态
93+ LaunchedEffect (Unit ) {
94+ resticViewModel.checkResticStatus( )
8295 }
8396
8497 Scaffold (
@@ -88,15 +101,14 @@ fun PageSettings() {
88101 scrollBehavior = scrollBehavior
89102 )
90103 }
91- ) { paddingValues -> // 使用明确的参数名
104+ ) { paddingValues ->
92105 Column (
93106 modifier = Modifier
94107 .verticalScroll(rememberScrollState())
95108 .fillMaxSize()
96- .padding(paddingValues), // 使用 paddingValues 而不是 it
109+ .padding(paddingValues),
97110 verticalArrangement = Arrangement .spacedBy(SizeTokens .Level24 )
98111 ) {
99-
100112 // --- 备份和恢复设置 ---
101113 Column {
102114 Clickable (
@@ -115,8 +127,7 @@ fun PageSettings() {
115127 title = stringResource(id = R .string.setup),
116128 value = stringResource(id = R .string.enter_the_setup_page_again),
117129 ) {
118- // 重新进入设置页面(通常用于重新配置应用)
119- context.getActivity().finish()
130+ context.getActivity()?.finish()
120131 context.startActivity(Intent (context, SetupActivity ::class .java))
121132 }
122133 }
@@ -161,19 +172,11 @@ fun PageSettings() {
161172
162173 // --- Restic 配置 ---
163174 Title (title = stringResource(id = R .string.restic_configuration)) {
164- // 状态显示部分
165175 Clickable (
166176 title = stringResource(id = R .string.restic_version),
167- value = if (resticVersion != null ) {
168- resticVersion
169- } else {
170- stringResource(id = R .string.restic_not_detected)
171- }
172- ) {
173- // 版本信息不可点击,仅用于显示
174- }
177+ value = resticVersion ? : stringResource(id = R .string.restic_not_detected)
178+ ) {}
175179
176- // 始终显示初始化状态
177180 Clickable (
178181 title = stringResource(id = R .string.restic_initialization_status),
179182 value = when {
@@ -187,7 +190,6 @@ fun PageSettings() {
187190 }
188191 }
189192
190- // 始终显示快照数量
191193 Clickable (
192194 title = stringResource(id = R .string.restic_snapshot_count),
193195 value = when {
@@ -196,11 +198,8 @@ fun PageSettings() {
196198 snapshotCount > 0 -> stringResource(id = R .string.restic_snapshots_count, snapshotCount)
197199 else -> stringResource(id = R .string.restic_no_snapshots)
198200 }
199- ) {
200- // 快照信息不可点击,仅用于显示
201- }
201+ ) {}
202202
203- // 配置选项 - 始终显示
204203 Clickable (
205204 title = stringResource(id = R .string.restic_password),
206205 value = stringResource(id = R .string.restic_password_desc),
@@ -224,7 +223,6 @@ fun PageSettings() {
224223 checkedText = stringResource(id = R .string.auto_screen_off_desc),
225224 )
226225
227- // 添加缓存管理入口
228226 Clickable (
229227 title = stringResource(id = R .string.cache_management),
230228 value = stringResource(id = R .string.cache_management_desc),
@@ -247,4 +245,86 @@ fun PageSettings() {
247245 InnerBottomSpacer (innerPadding = paddingValues)
248246 }
249247 }
248+
249+ if (showDownloadDialog) {
250+ ResticDownloadDialog (
251+ viewModel = resticViewModel,
252+ onDismiss = { showDownloadDialog = false },
253+ onDownloadComplete = {
254+ showDownloadDialog = false
255+ // 修复点:使用 Composable 作用域中的 scope
256+ scope.launch {
257+ resticViewModel.checkResticStatus()
258+ }
259+ }
260+ )
261+ }
262+ }
263+
264+ @OptIn(ExperimentalMaterial3Api ::class )
265+ @Composable
266+ fun ResticDownloadDialog (
267+ viewModel : ResticViewModel ,
268+ onDismiss : () -> Unit ,
269+ onDownloadComplete : () -> Unit
270+ ) {
271+ val downloadState by viewModel.downloadState.collectAsStateWithLifecycle()
272+ var urlInput by remember { mutableStateOf(" " ) }
273+ val scope = rememberCoroutineScope()
274+
275+ AlertDialog (
276+ onDismissRequest = onDismiss,
277+ title = { Text (" 下载Restic二进制文件" ) },
278+ text = {
279+ Column {
280+ Text (" Restic二进制文件不存在,请输入下载URL:" )
281+ Spacer (modifier = Modifier .height(8 .dp))
282+ OutlinedTextField (
283+ value = urlInput,
284+ onValueChange = { urlInput = it },
285+ label = { Text (" 下载URL" ) },
286+ modifier = Modifier .fillMaxWidth()
287+ )
288+
289+ val state = downloadState
290+ when (state) {
291+ is ResticViewModel .DownloadState .Downloading -> {
292+ Spacer (modifier = Modifier .height(8 .dp))
293+ LinearProgressIndicator (modifier = Modifier .fillMaxWidth())
294+ Text (" 正在下载..." )
295+ }
296+ is ResticViewModel .DownloadState .Error -> {
297+ Text (" 下载失败: ${state.message} " , color = MaterialTheme .colorScheme.error)
298+ }
299+ is ResticViewModel .DownloadState .Success -> {
300+ Text (" 下载成功!" , color = MaterialTheme .colorScheme.primary)
301+ }
302+ else -> {}
303+ }
304+ }
305+ },
306+ confirmButton = {
307+ val state = downloadState
308+ if (state is ResticViewModel .DownloadState .Success ) {
309+ Button (onClick = onDownloadComplete) { Text (" 完成" ) }
310+ } else {
311+ Button (
312+ onClick = {
313+ viewModel.setDownloadUrl(urlInput)
314+ scope.launch {
315+ viewModel.downloadResticBinary(urlInput)
316+ }
317+ },
318+ enabled = urlInput.isNotBlank() && state !is ResticViewModel .DownloadState .Downloading
319+ ) {
320+ Text (if (state is ResticViewModel .DownloadState .Downloading ) " 下载中..." else " 下载" )
321+ }
322+ }
323+ },
324+ dismissButton = {
325+ if (downloadState !is ResticViewModel .DownloadState .Downloading ) {
326+ TextButton (onClick = onDismiss) { Text (" 取消" ) }
327+ }
328+ }
329+ )
250330}
0 commit comments