@@ -101,9 +101,32 @@ fun GitScreen(
101101 var showCommitDialog by remember { mutableStateOf(false ) }
102102 var showCredentials by remember { mutableStateOf(false ) }
103103 // editingCredential != null -> 编辑现有;editingCredential == null && isAddingCredential -> 新增;否则列表态。
104+ // 编辑/新增态直接在 [Scaffold] 之外独立渲染全屏 [CredentialEditorScreen](它自带 Scaffold/TopAppBar/BackHandler),
105+ // 避免与本页 Scaffold 嵌套产生双层顶栏,返回由其自身 BackHandler 接管。
104106 var editingCredential by remember { mutableStateOf<GitCredential ?>(null ) }
105107 var isAddingCredential by remember { mutableStateOf(false ) }
106108
109+ // 编辑/新增凭据:独立全屏页,不进入下方 GitScreen 的 Scaffold,避免双层顶栏。
110+ if (editingCredential != null ) {
111+ val editing = editingCredential!!
112+ CredentialEditorScreen (
113+ initial = editing,
114+ onBack = { editingCredential = null },
115+ onSave = { credentialViewModel.saveCredential(it); editingCredential = null },
116+ onDelete = { credentialViewModel.deleteCredential(it); editingCredential = null }
117+ )
118+ return
119+ }
120+ if (isAddingCredential) {
121+ CredentialEditorScreen (
122+ initial = null ,
123+ onBack = { isAddingCredential = false },
124+ onSave = { credentialViewModel.saveCredential(it); isAddingCredential = false },
125+ onDelete = { /* 新增态无删除 */ }
126+ )
127+ return
128+ }
129+
107130 Scaffold (
108131 containerColor = MaterialTheme .colorScheme.background,
109132 topBar = {
@@ -115,13 +138,8 @@ fun GitScreen(
115138 ),
116139 navigationIcon = {
117140 IconButton (onClick = {
118- // 子页优先回上一级:编辑/新增 -> 凭据列表 -> 退出 Git 页
119- when {
120- editingCredential != null -> editingCredential = null
121- isAddingCredential -> isAddingCredential = false
122- showCredentials -> showCredentials = false
123- else -> onNavigateBack()
124- }
141+ // 凭据列表态回 Git 页,否则退出 Git 页。编辑/新增态由 [CredentialEditorScreen] 自身 BackHandler 处理,不走此顶栏。
142+ if (showCredentials) showCredentials = false else onNavigateBack()
125143 }) {
126144 Icon (FeatherIcons .ArrowLeft , contentDescription = " 返回" )
127145 }
@@ -134,7 +152,8 @@ fun GitScreen(
134152 IconButton (onClick = { viewModel.refresh() }, enabled = ! state.busy) {
135153 Icon (FeatherIcons .RefreshCw , contentDescription = " 刷新" )
136154 }
137- } else if (editingCredential == null && ! isAddingCredential) {
155+ } else {
156+ // showCredentials 列表态:显示添加凭据。编辑/新增态已 return,渲染顶栏时不会落到此分支。
138157 IconButton (onClick = { isAddingCredential = true }) {
139158 Icon (FeatherIcons .Plus , contentDescription = " 添加凭据" )
140159 }
@@ -145,26 +164,6 @@ fun GitScreen(
145164 snackbarHost = { SnackbarHost (snackbarHostState) }
146165 ) { padding ->
147166 Column (modifier = Modifier .fillMaxSize().padding(padding)) {
148- // 凭据编辑/新增子页(全屏覆盖 Git tab 内容)
149- val editing = editingCredential
150- if (editing != null ) {
151- CredentialEditorScreen (
152- initial = editing,
153- onBack = { editingCredential = null },
154- onSave = { credentialViewModel.saveCredential(it) },
155- onDelete = { credentialViewModel.deleteCredential(it) }
156- )
157- return @Column
158- }
159- if (isAddingCredential) {
160- CredentialEditorScreen (
161- initial = null ,
162- onBack = { isAddingCredential = false },
163- onSave = { credentialViewModel.saveCredential(it); isAddingCredential = false },
164- onDelete = { /* 新增态无删除 */ }
165- )
166- return @Column
167- }
168167 if (showCredentials) {
169168 // 每次进入凭据页重新读署名:用户可能在终端改过项目级/全局署名,避免回显陈旧空值。
170169 LaunchedEffect (Unit ) { credentialViewModel.refreshIdentity() }
@@ -201,7 +200,7 @@ fun GitScreen(
201200 state.loading -> Box (Modifier .fillMaxSize(), contentAlignment = Alignment .Center ) {
202201 CircularProgressIndicator ()
203202 }
204- state.notARepo -> EmptyState ( " 当前工作区不是 Git 仓库 " )
203+ state.notARepo -> NotARepoState (onInit = viewModel::initRepo )
205204 else -> when (state.tab) {
206205 GitTab .STATUS -> StatusTab (
207206 status = state.status,
@@ -237,88 +236,6 @@ fun GitScreen(
237236 }
238237 )
239238 }
240-
241- // push/pull 缺凭据时:VM 置位 pendingCredentialHost,此处弹登录框,填完保存落盘 + 自动重试原操作。
242- state.pendingCredentialHost?.let { host ->
243- CredentialPromptDialog (
244- host = host,
245- onConfirm = { username, token ->
246- viewModel.saveCredentialAndRetry(host, username, token)
247- },
248- onDismiss = { viewModel.cancelPendingCredential() }
249- )
250- }
251- }
252-
253- /* *
254- * 拉取/推送缺 https 凭据时的登录弹窗。host 只读预填(来自当前 remote),用户填 username/token;
255- * 填完确认后 VM 存 Room + 落盘到容器持久挂载 + 自动重试原操作。仿 Win git 无凭据弹窗登录体验。
256- * 新存的凭据自动设为该 host 默认(与 findForHost 的 isDefault 优先语义一致)。
257- */
258- @OptIn(ExperimentalMaterial3Api ::class )
259- @Composable
260- private fun CredentialPromptDialog (
261- host : String ,
262- onConfirm : (username: String , token: String ) -> Unit ,
263- onDismiss : () -> Unit
264- ) {
265- var username by remember { mutableStateOf(" " ) }
266- var token by remember { mutableStateOf(" " ) }
267- var tokenVisible by remember { mutableStateOf(false ) }
268- val canSave = username.trim().isNotBlank() && token.isNotBlank()
269-
270- AlertDialog (
271- onDismissRequest = onDismiss,
272- title = { Text (" 需要 $host 的登录凭据" ) },
273- text = {
274- Column (verticalArrangement = Arrangement .spacedBy(Spacing .md)) {
275- OutlinedTextField (
276- value = host,
277- onValueChange = { /* host 来自 remote,只读 */ },
278- label = { Text (" 远程主机" ) },
279- singleLine = true ,
280- readOnly = true ,
281- modifier = Modifier .fillMaxWidth()
282- )
283- OutlinedTextField (
284- value = username,
285- onValueChange = { username = it },
286- label = { Text (" 用户名" ) },
287- singleLine = true ,
288- modifier = Modifier .fillMaxWidth()
289- )
290- OutlinedTextField (
291- value = token,
292- onValueChange = { token = it },
293- label = { Text (" 访问令牌 Token / PAT" ) },
294- singleLine = true ,
295- visualTransformation = if (tokenVisible) VisualTransformation .None else PasswordVisualTransformation (),
296- keyboardOptions = KeyboardOptions (keyboardType = KeyboardType .Password ),
297- trailingIcon = {
298- IconButton (onClick = { tokenVisible = ! tokenVisible }) {
299- Icon (
300- if (tokenVisible) FeatherIcons .EyeOff else FeatherIcons .Eye ,
301- contentDescription = if (tokenVisible) " 隐藏" else " 显示"
302- )
303- }
304- },
305- modifier = Modifier .fillMaxWidth()
306- )
307- Text (
308- text = " 填完即保存并自动重试。该凭据同时会保存到容器内 git,终端与 AI 执行裸 git 命令时也可直接使用。" ,
309- style = MaterialTheme .typography.bodySmall,
310- color = MaterialTheme .colorScheme.onSurfaceVariant
311- )
312- }
313- },
314- confirmButton = {
315- TextButton (
316- onClick = { if (canSave) onConfirm(username.trim(), token) },
317- enabled = canSave
318- ) { Text (" 保存并重试" ) }
319- },
320- dismissButton = { TextButton (onClick = onDismiss) { Text (" 取消" ) } }
321- )
322239}
323240
324241@Composable
@@ -1190,6 +1107,33 @@ private fun EmptyState(text: String) {
11901107 }
11911108}
11921109
1110+ /* * 非仓库态:文案 + 「初始化 Git 仓库」按钮(跑 `git init`,成功后自动刷新进仓库态)。 */
1111+ @Composable
1112+ private fun NotARepoState (onInit : () -> Unit ) {
1113+ Box (Modifier .fillMaxSize(), contentAlignment = Alignment .Center ) {
1114+ Column (
1115+ horizontalAlignment = Alignment .CenterHorizontally ,
1116+ verticalArrangement = Arrangement .spacedBy(Spacing .md)
1117+ ) {
1118+ Text (
1119+ " 当前工作区不是 Git 仓库" ,
1120+ style = MaterialTheme .typography.bodyMedium,
1121+ color = MaterialTheme .colorScheme.onSurfaceVariant
1122+ )
1123+ Text (
1124+ " 初始化后会在此创建 .git,之后可暂存、提交、关联远程。" ,
1125+ style = MaterialTheme .typography.bodySmall,
1126+ color = MaterialTheme .colorScheme.onSurfaceVariant
1127+ )
1128+ FilledTonalButton (onClick = onInit) {
1129+ Icon (FeatherIcons .GitBranch , contentDescription = null )
1130+ Spacer (Modifier .width(Spacing .sm))
1131+ Text (" 初始化 Git 仓库" )
1132+ }
1133+ }
1134+ }
1135+ }
1136+
11931137@Composable
11941138private fun CommitDialog (onDismiss : () -> Unit , onConfirm : (String ) -> Unit ) {
11951139 var message by remember { mutableStateOf(" " ) }
0 commit comments