| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; |
| 1 | import { NativeStackScreenOptions } from "../../native/StackHeader"; |
| 2 | 2 | import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; |
| 3 | | import type { MenuAction } from "@react-native-menu/menu"; |
| 4 | 3 | import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; |
| 5 | 4 | import { Platform, View } from "react-native"; |
| 6 | 5 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| ... | ... | @@ -14,10 +13,8 @@ import { |
| 14 | 13 | } from "@t3tools/shared/filePreview"; |
| 15 | 14 | import { mediaFileReference } from "@t3tools/client-runtime/media-reference"; |
| 16 | 15 | |
| 17 | | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 18 | 16 | import { MaterialScreenContent } from "../../components/MaterialScreenContent"; |
| 19 | 17 | import { AudioFilePreview } from "../../components/AudioFilePreview"; |
| 20 | | import { ControlPillMenu } from "../../components/ControlPill"; |
| 21 | 18 | import { EmptyState } from "../../components/EmptyState"; |
| 22 | 19 | import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; |
| 23 | 20 | import { LoadingScreen } from "../../components/LoadingScreen"; |
| ... | ... | @@ -38,14 +35,6 @@ import { |
| 38 | 35 | useAdaptiveWorkspacePaneRole, |
| 39 | 36 | useRegisterWorkspaceInspector, |
| 40 | 37 | } from "../layout/AdaptiveWorkspaceLayout"; |
| 41 | | import { |
| 42 | | createNativeMailSearchToolbarItem, |
| 43 | | NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, |
| 44 | | } from "../layout/native-mail-search-toolbar"; |
| 45 | | import { |
| 46 | | AndroidWorkspaceSidebarButton, |
| 47 | | WorkspaceSidebarToolbar, |
| 48 | | } from "../layout/workspace-sidebar-toolbar"; |
| 49 | 38 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| 50 | 39 | import { ThreadRouteScreen } from "../threads/ThreadRouteScreen"; |
| 51 | 40 | import { FilePreviewLoading, FilePreviewNotice } from "./FilePreviewFeedback"; |
| ... | ... | @@ -55,7 +44,7 @@ import { useFileTreeEntries } from "./useFileTreeEntries"; |
| 55 | 44 | import { preloadWorkspaceFileContents } from "./preload-workspace-file"; |
| 56 | 45 | import { SourceFileSurface } from "./SourceFileSurface"; |
| 57 | 46 | import { ThreadFileNavigatorPane } from "./thread-file-navigator-pane"; |
| 58 | | import { MaterialFilesHeader } from "./MaterialFilesHeader"; |
| 47 | import { ScreenHeader } from "../../components/ScreenHeader"; |
| 59 | 48 | import { WorkspaceFileImagePreview } from "./WorkspaceFileImagePreview"; |
| 60 | 49 | import { WorkspaceFilePreviewError } from "./WorkspaceFilePreviewError"; |
| 61 | 50 | import { WorkspaceFileVideoPreview } from "./WorkspaceFileVideoPreview"; |
| ... | ... | @@ -70,6 +59,107 @@ import { |
| 70 | 59 | } from "./filePath"; |
| 71 | 60 | import { useWorkspaceFileAssetUrlState } from "./workspaceFileAssetUrl"; |
| 72 | 61 | |
| 62 | function FilesBrowserHeader(props: { |
| 63 | readonly projectName: string; |
| 64 | readonly searchQuery: string; |
| 65 | readonly onSearchQueryChange: (query: string) => void; |
| 66 | readonly onRefresh: () => void; |
| 67 | readonly onBack: () => void; |
| 68 | }) { |
| 69 | return ( |
| 70 | <ScreenHeader |
| 71 | title="Files" |
| 72 | subtitle={props.projectName} |
| 73 | onBack={props.onBack} |
| 74 | hideBottomBorder |
| 75 | matchSearchSurface |
| 76 | search={{ |
| 77 | value: props.searchQuery, |
| 78 | onChangeText: props.onSearchQueryChange, |
| 79 | placeholder: "Search files", |
| 80 | onRefresh: props.onRefresh, |
| 81 | refreshAccessibilityLabel: "Refresh files", |
| 82 | closeAccessibilityLabel: "Close file search", |
| 83 | clearAccessibilityLabel: "Clear file search", |
| 84 | }} |
| 85 | /> |
| 86 | ); |
| 87 | } |
| 88 | |
| 89 | function FileHeader(props: { |
| 90 | readonly title: string; |
| 91 | readonly subtitle: string; |
| 92 | readonly iconColor: string; |
| 93 | readonly activeMode: string; |
| 94 | readonly fileInspectorSupported: boolean; |
| 95 | readonly onBack: () => void; |
| 96 | readonly onReturnToThread: () => void; |
| 97 | readonly actions: ReadonlyArray<{ |
| 98 | readonly id: string; |
| 99 | readonly title: string; |
| 100 | readonly icon: string; |
| 101 | readonly inline: boolean; |
| 102 | readonly onPress: () => unknown; |
| 103 | }>; |
| 104 | }) { |
| 105 | const { panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); |
| 106 | const modes = props.actions.filter(({ inline }) => inline); |
| 107 | return ( |
| 108 | <ScreenHeader |
| 109 | title={props.title} |
| 110 | subtitle={props.subtitle} |
| 111 | onBack={props.onBack} |
| 112 | hideBottomBorder |
| 113 | options={{ headerTintColor: props.iconColor, headerTitle: props.title }} |
| 114 | backInSplitView={ |
| 115 | props.fileInspectorSupported |
| 116 | ? { |
| 117 | accessibilityLabel: "Return to chat", |
| 118 | icon: "chevron.left", |
| 119 | onPress: props.onReturnToThread, |
| 120 | } |
| 121 | : undefined |
| 122 | } |
| 123 | actions={ |
| 124 | props.fileInspectorSupported |
| 125 | ? [ |
| 126 | { |
| 127 | accessibilityLabel: panes.auxiliaryPaneVisible |
| 128 | ? "Hide file navigator" |
| 129 | : "Show file navigator", |
| 130 | icon: "sidebar.right", |
| 131 | selected: panes.auxiliaryPaneVisible, |
| 132 | onPress: toggleAuxiliaryPane, |
| 133 | }, |
| 134 | ] |
| 135 | : undefined |
| 136 | } |
| 137 | menus={[ |
| 138 | { |
| 139 | title: "File actions", |
| 140 | icon: "ellipsis", |
| 141 | separateBackground: false, |
| 142 | items: [ |
| 143 | ...(modes.length > 0 |
| 144 | ? [ |
| 145 | { |
| 146 | id: "modes", |
| 147 | inline: true, |
| 148 | items: modes.map((action) => ({ |
| 149 | ...action, |
| 150 | selected: action.id === props.activeMode, |
| 151 | })), |
| 152 | }, |
| 153 | ] |
| 154 | : []), |
| 155 | ...props.actions.filter(({ inline }) => !inline), |
| 156 | ], |
| 157 | }, |
| 158 | ]} |
| 159 | /> |
| 160 | ); |
| 161 | } |
| 162 | |
| 73 | 163 | type FileViewMode = "preview" | "source"; |
| 74 | 164 | |
| 75 | 165 | function firstRouteParam(value: string | string[] | undefined): string | null { |
| ... | ... | @@ -315,14 +405,10 @@ function FilesToolbarBottomFade() { |
| 315 | 405 | export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { |
| 316 | 406 | useAdaptiveWorkspacePaneRole("inspector"); |
| 317 | 407 | const navigation = useNavigation(); |
| 318 | | const { fileInspector, layout, panes, showAuxiliaryPane, togglePrimarySidebar } = |
| 319 | | useAdaptiveWorkspaceLayout(); |
| 408 | const { fileInspector, layout, showAuxiliaryPane } = useAdaptiveWorkspaceLayout(); |
| 320 | 409 | const [searchQuery, setSearchQuery] = useState(""); |
| 321 | | const isAndroid = Platform.OS === "android"; |
| 322 | 410 | const { themeAppearance: highlightTheme } = useAppearancePreferences(); |
| 323 | | const theme = useUniwindTheme(); |
| 324 | | const headerColor = theme["--color-header"]; |
| 325 | | const sheetSurfaceColor = theme["--color-sheet-solid"]; |
| 411 | const headerColor = useUniwindTheme()["--color-header"]; |
| 326 | 412 | const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace( |
| 327 | 413 | props.route.params, |
| 328 | 414 | ); |
| ... | ... | @@ -430,84 +516,15 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { |
| 430 | 516 | ); |
| 431 | 517 | } |
| 432 | 518 | |
| 433 | | const usesCompactMailToolbar = |
| 434 | | Platform.OS === "ios" && !layout.usesSplitView && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; |
| 435 | | |
| 436 | 519 | const content = ( |
| 437 | 520 | <> |
| 438 | | {/* Static header config (glass preset and title) lives in Stack.tsx. The |
| 439 | | live sheet color stays dynamic here so the FlatList can remain the |
| 440 | | direct scene child for native scroll-edge sampling. */} |
| 441 | | <NativeStackScreenOptions |
| 442 | | options={{ |
| 443 | | contentStyle: { |
| 444 | | backgroundColor: Platform.OS === "android" ? headerColor : sheetSurfaceColor, |
| 445 | | }, |
| 446 | | headerShown: !isAndroid, |
| 447 | | unstable_headerSubtitle: |
| 448 | | Platform.OS === "ios" && projectName.length > 0 ? projectName : undefined, |
| 449 | | // No refresh button: the list already supports pull-to-refresh. |
| 450 | | unstable_headerToolbarItems: usesCompactMailToolbar |
| 451 | | ? () => [ |
| 452 | | createNativeMailSearchToolbarItem({ |
| 453 | | onSearchTextChange: setSearchQuery, |
| 454 | | placeholder: "Search files", |
| 455 | | searchTextChangeId: "files-search-text", |
| 456 | | }), |
| 457 | | ] |
| 458 | | : undefined, |
| 459 | | headerSearchBarOptions: usesCompactMailToolbar |
| 460 | | ? undefined |
| 461 | | : { |
| 462 | | allowToolbarIntegration: true, |
| 463 | | autoCapitalize: "none", |
| 464 | | hideNavigationBar: false, |
| 465 | | placeholder: "Search files", |
| 466 | | onChangeText: (event) => { |
| 467 | | setSearchQuery(event.nativeEvent.text); |
| 468 | | }, |
| 469 | | onCancelButtonPress: () => { |
| 470 | | setSearchQuery(""); |
| 471 | | }, |
| 472 | | }, |
| 473 | | }} |
| 521 | <FilesBrowserHeader |
| 522 | projectName={projectName} |
| 523 | searchQuery={searchQuery} |
| 524 | onSearchQueryChange={setSearchQuery} |
| 525 | onRefresh={entriesQuery.refresh} |
| 526 | onBack={handleReturnToThread} |
| 474 | 527 | /> |
| 475 | | {isAndroid ? ( |
| 476 | | <> |
| 477 | | { |
| 478 | | <MaterialFilesHeader |
| 479 | | projectName={projectName} |
| 480 | | leading={<AndroidWorkspaceSidebarButton />} |
| 481 | | searchQuery={searchQuery} |
| 482 | | onSearchQueryChange={setSearchQuery} |
| 483 | | onRefresh={entriesQuery.refresh} |
| 484 | | onBack={handleReturnToThread} |
| 485 | | /> |
| 486 | | } |
| 487 | | </> |
| 488 | | ) : ( |
| 489 | | <> |
| 490 | | {layout.usesSplitView ? ( |
| 491 | | <NativeHeaderToolbar placement="left"> |
| 492 | | <NativeHeaderToolbar.Button |
| 493 | | accessibilityLabel={panes.primarySidebarVisible ? "Maximize files" : "Show threads"} |
| 494 | | icon={ |
| 495 | | panes.primarySidebarVisible |
| 496 | | ? "arrow.up.left.and.arrow.down.right" |
| 497 | | : "sidebar.left" |
| 498 | | } |
| 499 | | onPress={togglePrimarySidebar} |
| 500 | | separateBackground |
| 501 | | /> |
| 502 | | </NativeHeaderToolbar> |
| 503 | | ) : null} |
| 504 | | {usesCompactMailToolbar ? null : ( |
| 505 | | <NativeHeaderToolbar placement="bottom"> |
| 506 | | <NativeHeaderToolbar.SearchBarSlot /> |
| 507 | | </NativeHeaderToolbar> |
| 508 | | )} |
| 509 | | </> |
| 510 | | )} |
| 511 | 528 | <MaterialScreenContent insetHorizontal={layout.usesSplitView}> |
| 512 | 529 | <FileTreeBrowser |
| 513 | 530 | key={JSON.stringify([environmentId, cwd])} |
| ... | ... | @@ -540,10 +557,9 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { |
| 540 | 557 | export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { |
| 541 | 558 | useAdaptiveWorkspacePaneRole("inspector"); |
| 542 | 559 | const navigation = useNavigation(); |
| 543 | | const { fileInspector, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); |
| 560 | const { fileInspector } = useAdaptiveWorkspaceLayout(); |
| 544 | 561 | const { appearance, setCodeWordBreak } = useAppearancePreferences(); |
| 545 | 562 | const iconColor = useUniwindTheme()["--color-icon"]; |
| 546 | | const isAndroid = Platform.OS === "android"; |
| 547 | 563 | const params = props.route.params; |
| 548 | 564 | const relativePath = normalizeRoutePath(params.path); |
| 549 | 565 | const targetLine = normalizeRouteLine(firstRouteParam(params.line)); |
| ... | ... | @@ -819,23 +835,6 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { |
| 819 | 835 | fileData?.truncated, |
| 820 | 836 | ]); |
| 821 | 837 | |
| 822 | | const androidFileMenuActions = useMemo<MenuAction[]>( |
| 823 | | () => |
| 824 | | fileMenuActions.map((action) => ({ |
| 825 | | id: action.id, |
| 826 | | title: action.title, |
| 827 | | image: action.icon, |
| 828 | | state: action.id === resolvedActiveMode ? "on" : undefined, |
| 829 | | })), |
| 830 | | [fileMenuActions, resolvedActiveMode], |
| 831 | | ); |
| 832 | | const handleAndroidFileMenuAction = useCallback( |
| 833 | | (event: { nativeEvent: { event: string } }) => { |
| 834 | | const action = fileMenuActions.find(({ id }) => id === event.nativeEvent.event); |
| 835 | | void action?.onPress(); |
| 836 | | }, |
| 837 | | [fileMenuActions], |
| 838 | | ); |
| 839 | 838 | const handleReturnToThread = useCallback(() => { |
| 840 | 839 | if (environmentId !== null && threadId !== null) { |
| 841 | 840 | navigation.dispatch( |
| ... | ... | @@ -878,100 +877,16 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { |
| 878 | 877 | |
| 879 | 878 | return ( |
| 880 | 879 | <View className="flex-1 bg-sheet"> |
| 881 | | <NativeStackScreenOptions |
| 882 | | options={{ |
| 883 | | // Static header config lives in Stack.tsx (SOLID_HEADER_OPTIONS: solid |
| 884 | | // sheet-colored header — this route's content scrolls internally, so |
| 885 | | // there is nothing for glass to sample). Only dynamic values here. |
| 886 | | headerShown: !isAndroid, |
| 887 | | headerTintColor: iconColor, |
| 888 | | headerTitle: basename(relativePath), |
| 889 | | title: basename(relativePath), |
| 890 | | unstable_headerSubtitle: |
| 891 | | Platform.OS === "ios" && headerSubtitle.length > 0 ? headerSubtitle : undefined, |
| 892 | | }} |
| 880 | <FileHeader |
| 881 | title={basename(relativePath)} |
| 882 | subtitle={headerSubtitle} |
| 883 | iconColor={iconColor} |
| 884 | activeMode={resolvedActiveMode} |
| 885 | fileInspectorSupported={fileInspector.supported} |
| 886 | onBack={handleBack} |
| 887 | onReturnToThread={handleReturnToThread} |
| 888 | actions={fileMenuActions} |
| 893 | 889 | /> |
| 894 | | {isAndroid ? ( |
| 895 | | <AndroidScreenHeader |
| 896 | | title={basename(relativePath)} |
| 897 | | subtitle={headerSubtitle} |
| 898 | | leading={<AndroidWorkspaceSidebarButton />} |
| 899 | | hideBottomBorder |
| 900 | | onBack={handleBack} |
| 901 | | trailing={ |
| 902 | | <> |
| 903 | | {fileInspector.supported ? ( |
| 904 | | <AndroidHeaderIconButton |
| 905 | | accessibilityLabel={ |
| 906 | | panes.auxiliaryPaneVisible ? "Hide file navigator" : "Show file navigator" |
| 907 | | } |
| 908 | | icon="sidebar.right" |
| 909 | | selected={panes.auxiliaryPaneVisible} |
| 910 | | onPress={toggleAuxiliaryPane} |
| 911 | | /> |
| 912 | | ) : null} |
| 913 | | <ControlPillMenu |
| 914 | | actions={androidFileMenuActions} |
| 915 | | isAnchoredToRight |
| 916 | | title="File actions" |
| 917 | | onPressAction={handleAndroidFileMenuAction} |
| 918 | | > |
| 919 | | <AndroidHeaderIconButton accessibilityLabel="File actions" icon="ellipsis" /> |
| 920 | | </ControlPillMenu> |
| 921 | | </> |
| 922 | | } |
| 923 | | /> |
| 924 | | ) : null} |
| 925 | | <WorkspaceSidebarToolbar> |
| 926 | | {fileInspector.supported ? ( |
| 927 | | <NativeHeaderToolbar.Button |
| 928 | | accessibilityLabel="Return to chat" |
| 929 | | icon="chevron.left" |
| 930 | | onPress={handleReturnToThread} |
| 931 | | /> |
| 932 | | ) : null} |
| 933 | | </WorkspaceSidebarToolbar> |
| 934 | | <NativeHeaderToolbar placement="right"> |
| 935 | | {fileInspector.supported ? ( |
| 936 | | <NativeHeaderToolbar.Button |
| 937 | | accessibilityLabel={ |
| 938 | | panes.auxiliaryPaneVisible ? "Hide file navigator" : "Show file navigator" |
| 939 | | } |
| 940 | | icon="sidebar.right" |
| 941 | | onPress={toggleAuxiliaryPane} |
| 942 | | separateBackground |
| 943 | | /> |
| 944 | | ) : null} |
| 945 | | <NativeHeaderToolbar.Menu accessibilityLabel="File actions" icon="ellipsis"> |
| 946 | | {fileMenuActions.some(({ inline }) => inline) ? ( |
| 947 | | <NativeHeaderToolbar.Menu inline> |
| 948 | | {fileMenuActions |
| 949 | | .filter(({ inline }) => inline) |
| 950 | | .map((action) => ( |
| 951 | | <NativeHeaderToolbar.MenuAction |
| 952 | | key={action.id} |
| 953 | | icon={action.icon} |
| 954 | | isOn={action.id === resolvedActiveMode} |
| 955 | | onPress={action.onPress} |
| 956 | | > |
| 957 | | {action.title} |
| 958 | | </NativeHeaderToolbar.MenuAction> |
| 959 | | ))} |
| 960 | | </NativeHeaderToolbar.Menu> |
| 961 | | ) : null} |
| 962 | | {fileMenuActions |
| 963 | | .filter(({ inline }) => !inline) |
| 964 | | .map((action) => ( |
| 965 | | <NativeHeaderToolbar.MenuAction |
| 966 | | key={action.id} |
| 967 | | icon={action.icon} |
| 968 | | onPress={action.onPress} |
| 969 | | > |
| 970 | | {action.title} |
| 971 | | </NativeHeaderToolbar.MenuAction> |
| 972 | | ))} |
| 973 | | </NativeHeaderToolbar.Menu> |
| 974 | | </NativeHeaderToolbar> |
| 975 | 890 | <MaterialScreenContent> |
| 976 | 891 | <FileContent |
| 977 | 892 | key={previewKey} |