mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
Render committed branch changes as virtualized SectionList rows for clean and dirty working trees, keeping all files accessible without delaying Back. Share the existing sleek scrollbar rules with mobile web pages and retain explicit hidden-indicator settings. Fixes #30170. Review reference: #30171; thanks to @zhangxaochen for the diagnosis and initial proposed fix. Co-authored-by: zhangxaochen <zhangxaochen@163.com>
250 lines
8.4 KiB
TypeScript
250 lines
8.4 KiB
TypeScript
import { ActivityIndicator, Pressable, Text, View } from 'react-native'
|
|
import { ChevronRight, FileText, Minus, Plus, Trash2 } from 'lucide-react-native'
|
|
import { colors } from '../theme/mobile-theme'
|
|
import { MOBILE_GIT_STATUS_LABELS } from './mobile-git-status'
|
|
import { formatMobileBranchEntryMeta } from './mobile-branch-entry-format'
|
|
import {
|
|
statusColor,
|
|
type MobileBranchEntryView,
|
|
type MobileGitStatusEntryView
|
|
} from './mobile-source-control-screen-state'
|
|
import type { MobileSourceControlState } from './use-mobile-source-control-state'
|
|
import { styles } from './mobile-source-control-styles'
|
|
|
|
type RowState = Pick<
|
|
MobileSourceControlState,
|
|
| 'busyAction'
|
|
| 'openingPath'
|
|
| 'openingBranchPath'
|
|
| 'openFile'
|
|
| 'runGitAction'
|
|
| 'setDiscardTarget'
|
|
>
|
|
|
|
export function makeRenderFileRow(state: RowState) {
|
|
const { busyAction, openingPath, openingBranchPath, openFile, runGitAction, setDiscardTarget } =
|
|
state
|
|
return function FileRow({ item }: { item: MobileGitStatusEntryView }) {
|
|
const rowBusy =
|
|
busyAction === item.stageActionId ||
|
|
busyAction === item.unstageActionId ||
|
|
busyAction === item.discardActionId ||
|
|
openingPath === item.path
|
|
const rowDisabled =
|
|
!item.canOpen || busyAction !== null || openingPath !== null || openingBranchPath !== null
|
|
const ioBusy = busyAction !== null || openingPath !== null || openingBranchPath !== null
|
|
return (
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.fileRow,
|
|
pressed && item.canOpen && styles.fileRowPressed,
|
|
rowDisabled && styles.fileRowDisabled,
|
|
!item.canOpen && styles.fileRowUnavailable
|
|
]}
|
|
onPress={() => void openFile(item)}
|
|
disabled={rowDisabled}
|
|
accessibilityLabel={`Open changed file ${item.path}`}
|
|
>
|
|
<View style={styles.statusBadge}>
|
|
<Text style={[styles.statusBadgeText, { color: statusColor(item.status) }]}>
|
|
{MOBILE_GIT_STATUS_LABELS[item.status]}
|
|
</Text>
|
|
</View>
|
|
<FileText
|
|
size={16}
|
|
color={item.canOpen ? colors.textSecondary : colors.textMuted}
|
|
strokeWidth={2.1}
|
|
/>
|
|
<View style={styles.fileTextBlock}>
|
|
<Text
|
|
style={[styles.filePath, !item.canOpen && styles.filePathDisabled]}
|
|
numberOfLines={1}
|
|
>
|
|
{item.path}
|
|
</Text>
|
|
{item.oldPath ? (
|
|
<Text style={styles.fileMeta} numberOfLines={1}>
|
|
from {item.oldPath}
|
|
</Text>
|
|
) : item.conflictStatus === 'unresolved' ? (
|
|
<Text style={styles.fileMeta} numberOfLines={1}>
|
|
Unresolved conflict
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
{rowBusy ? (
|
|
<ActivityIndicator size="small" color={colors.textSecondary} />
|
|
) : item.area === 'staged' ? (
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.iconButton,
|
|
ioBusy && styles.iconButtonDisabled,
|
|
pressed && styles.iconButtonPressed
|
|
]}
|
|
disabled={ioBusy}
|
|
onPress={() =>
|
|
void runGitAction(item.unstageActionId, 'git.unstage', { filePath: item.path })
|
|
}
|
|
hitSlop={8}
|
|
accessibilityLabel={`Unstage ${item.path}`}
|
|
>
|
|
<Minus size={16} color={colors.textSecondary} strokeWidth={2.2} />
|
|
</Pressable>
|
|
) : item.canStage || item.canDiscard ? (
|
|
<View style={styles.rowActions}>
|
|
{item.canStage ? (
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.iconButton,
|
|
ioBusy && styles.iconButtonDisabled,
|
|
pressed && styles.iconButtonPressed
|
|
]}
|
|
disabled={ioBusy}
|
|
onPress={() =>
|
|
void runGitAction(item.stageActionId, 'git.stage', { filePath: item.path })
|
|
}
|
|
hitSlop={8}
|
|
accessibilityLabel={`Stage ${item.path}`}
|
|
>
|
|
<Plus size={16} color={colors.textSecondary} strokeWidth={2.2} />
|
|
</Pressable>
|
|
) : null}
|
|
{item.canDiscard ? (
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.iconButton,
|
|
ioBusy && styles.iconButtonDisabled,
|
|
pressed && styles.iconButtonPressed
|
|
]}
|
|
disabled={ioBusy}
|
|
onPress={() => setDiscardTarget(item)}
|
|
hitSlop={8}
|
|
accessibilityLabel={`Discard ${item.path}`}
|
|
>
|
|
<Trash2 size={16} color={colors.statusRed} strokeWidth={2.1} />
|
|
</Pressable>
|
|
) : null}
|
|
</View>
|
|
) : null}
|
|
{!rowBusy && item.canOpen ? (
|
|
<ChevronRight size={16} color={colors.textMuted} strokeWidth={2.1} />
|
|
) : null}
|
|
</Pressable>
|
|
)
|
|
}
|
|
}
|
|
|
|
type BranchState = Pick<
|
|
MobileSourceControlState,
|
|
| 'shouldShowBranchCompareSection'
|
|
| 'branchCompareSummaryText'
|
|
| 'branchEntries'
|
|
| 'branchCompareState'
|
|
| 'branchCompareResult'
|
|
| 'busyAction'
|
|
| 'openBranchDiff'
|
|
| 'openingBranchPath'
|
|
| 'openingPath'
|
|
>
|
|
|
|
export function BranchCompareHeader({ state }: { state: BranchState }) {
|
|
const {
|
|
shouldShowBranchCompareSection,
|
|
branchCompareSummaryText,
|
|
branchEntries,
|
|
branchCompareState,
|
|
branchCompareResult
|
|
} = state
|
|
if (!shouldShowBranchCompareSection) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<View style={styles.branchCompareBlock}>
|
|
<View style={styles.sectionHeader}>
|
|
<View style={styles.branchSectionTitleBlock}>
|
|
<Text style={styles.sectionTitle}>Committed on Branch</Text>
|
|
{branchCompareSummaryText ? (
|
|
<Text style={styles.branchSectionSubtitle} numberOfLines={1}>
|
|
{branchCompareSummaryText}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
<Text style={styles.sectionCount}>{branchEntries.length}</Text>
|
|
</View>
|
|
{branchCompareState.kind === 'loading' ? (
|
|
<View style={styles.branchStateRow}>
|
|
<ActivityIndicator size="small" color={colors.textSecondary} />
|
|
<Text style={styles.branchStateText}>Loading committed changes...</Text>
|
|
</View>
|
|
) : branchCompareState.kind === 'error' ? (
|
|
<View style={styles.branchStateRow}>
|
|
<Text style={styles.branchStateText}>{branchCompareState.message}</Text>
|
|
</View>
|
|
) : branchCompareResult && branchCompareResult.summary.status !== 'ready' ? (
|
|
<View style={styles.branchStateRow}>
|
|
<Text style={styles.branchStateText}>
|
|
{branchCompareResult.summary.errorMessage ?? 'Committed changes unavailable.'}
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
export function BranchFileRow({
|
|
entry,
|
|
state
|
|
}: {
|
|
entry: MobileBranchEntryView
|
|
state: BranchState
|
|
}) {
|
|
const { busyAction, openBranchDiff, openingBranchPath, openingPath } = state
|
|
const rowBusy = openingBranchPath === entry.path
|
|
const rowDisabled =
|
|
!entry.canOpen || busyAction !== null || openingPath !== null || openingBranchPath !== null
|
|
const meta = formatMobileBranchEntryMeta(entry)
|
|
return (
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.fileRow,
|
|
pressed && entry.canOpen && styles.fileRowPressed,
|
|
rowDisabled && styles.fileRowDisabled,
|
|
!entry.canOpen && styles.fileRowUnavailable
|
|
]}
|
|
onPress={() => void openBranchDiff(entry)}
|
|
disabled={rowDisabled}
|
|
accessibilityLabel={`Open committed change ${entry.path}`}
|
|
>
|
|
<View style={styles.statusBadge}>
|
|
<Text style={[styles.statusBadgeText, { color: statusColor(entry.status) }]}>
|
|
{MOBILE_GIT_STATUS_LABELS[entry.status]}
|
|
</Text>
|
|
</View>
|
|
<FileText
|
|
size={16}
|
|
color={entry.canOpen ? colors.textSecondary : colors.textMuted}
|
|
strokeWidth={2.1}
|
|
/>
|
|
<View style={styles.fileTextBlock}>
|
|
<Text
|
|
style={[styles.filePath, !entry.canOpen && styles.filePathDisabled]}
|
|
numberOfLines={1}
|
|
>
|
|
{entry.path}
|
|
</Text>
|
|
{meta ? (
|
|
<Text style={styles.fileMeta} numberOfLines={1}>
|
|
{meta}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
{rowBusy ? (
|
|
<ActivityIndicator size="small" color={colors.textSecondary} />
|
|
) : entry.canOpen ? (
|
|
<ChevronRight size={16} color={colors.textMuted} strokeWidth={2.1} />
|
|
) : null}
|
|
</Pressable>
|
|
)
|
|
}
|