Merge main into pr1-large-file-preview
The floating editor toolbar this branch was based on was removed on main (the toolbar is always docked now); the file-view region resolves to main's structure with this branch's virtualizer-bound ScrollableOverlay.
This commit is contained in:
@@ -51,7 +51,7 @@ import { shouldAllowFileDraftSave, shouldScheduleFileAutosave } from '@/lib/file
|
||||
import { getRuntimeUrlResolver } from '@/lib/runtime-url';
|
||||
import { acquireRuntimeUrlAuthToken, refreshRuntimeUrlAuthToken, subscribeRuntimeUrlAuthToken } from '@/lib/runtime-auth';
|
||||
import { getRuntimeApiBaseUrl, getRuntimeKey } from '@/lib/runtime-switch';
|
||||
import { getOutsideFileGrant } from '@/lib/outsideFileGrants';
|
||||
import { getOutsideFileGrant, resolveOutsideFileReadOptions } from '@/lib/outsideFileGrants';
|
||||
import { subscribeToFileContentInvalidation } from '@/lib/fileContentInvalidation';
|
||||
import { DiagramEditor } from '@/components/diagram';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
@@ -62,7 +62,7 @@ import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
||||
import { useGitStatus } from '@/stores/useGitStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { buildCodeMirrorCommentWidgets, normalizeLineRange, useInlineCommentController } from '@/components/comments';
|
||||
import { buildCodeMirrorCommentWidgets, FilePreviewCommentMenu, normalizeLineRange, useInlineCommentController } from '@/components/comments';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { useDirectoryShowHidden } from '@/lib/directoryShowHidden';
|
||||
import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
|
||||
@@ -769,8 +769,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
const [wrapLines, setWrapLines] = React.useState(true);
|
||||
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
||||
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
||||
const [isFloatingToolbarOpen, setIsFloatingToolbarOpen] = React.useState(false);
|
||||
const floatingToolbarRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const toolbarDropdownOpenCountRef = React.useRef(0);
|
||||
|
||||
const handleToolbarDropdownOpenChange = React.useCallback((open: boolean) => {
|
||||
@@ -780,23 +778,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
);
|
||||
}, []);
|
||||
|
||||
const isClickInsidePortalledMenu = React.useCallback((target: EventTarget | null) => {
|
||||
if (!(target instanceof Element)) return false;
|
||||
return target.closest('[data-slot="dropdown-menu-content"], [data-slot="dropdown-menu-item"]') !== null;
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isFloatingToolbarOpen) return;
|
||||
const handler = (event: MouseEvent) => {
|
||||
if (toolbarDropdownOpenCountRef.current > 0) return;
|
||||
if (isClickInsidePortalledMenu(event.target)) return;
|
||||
if (floatingToolbarRef.current && !floatingToolbarRef.current.contains(event.target as Node)) {
|
||||
setIsFloatingToolbarOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [isClickInsidePortalledMenu, isFloatingToolbarOpen]);
|
||||
type TextViewMode = 'view' | 'edit';
|
||||
type PreviewViewMode = 'preview' | 'edit';
|
||||
|
||||
@@ -861,6 +842,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
}, [openPaths, selectedPath]);
|
||||
const selectedFile = React.useMemo(() => (effectiveSelectedPath ? toFileNode(effectiveSelectedPath) : null), [effectiveSelectedPath, toFileNode]);
|
||||
const selectedFilePath = selectedFile?.path ?? '';
|
||||
const [, setOutsideFileGrantRevision] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!root || !selectedPath) return;
|
||||
@@ -881,6 +863,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
}),
|
||||
[mode, selectedFileIsOutsideWorkspace, selectedOutsideFileGrant, root],
|
||||
);
|
||||
const resolveFileReadOptions = React.useCallback(async (path: string) => {
|
||||
const previousGrant = getOutsideFileGrant(path);
|
||||
const readOptions = await resolveOutsideFileReadOptions(path, root, mode === 'editor-only');
|
||||
if (readOptions.outsideFileGrant && readOptions.outsideFileGrant !== previousGrant) {
|
||||
setOutsideFileGrantRevision((revision) => revision + 1);
|
||||
}
|
||||
return readOptions;
|
||||
}, [mode, root]);
|
||||
|
||||
// Editor tabs horizontal scroll fades
|
||||
const editorTabsScrollRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -949,7 +939,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
|
||||
const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false);
|
||||
const pendingSelectFileRef = React.useRef<FileNode | null>(null);
|
||||
const pendingTabRef = React.useRef<import('@/stores/useUIStore').MainTab | null>(null);
|
||||
const pendingClosePathRef = React.useRef<string | null>(null);
|
||||
const skipDirtyOnceRef = React.useRef(false);
|
||||
const copiedContentTimeoutRef = React.useRef<number | null>(null);
|
||||
@@ -1039,7 +1028,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
|
||||
// Session/config for sending comments
|
||||
const setMainTabGuard = useUIStore((state) => state.setMainTabGuard);
|
||||
const pendingFileNavigation = useUIStore((state) => state.pendingFileNavigation);
|
||||
const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation);
|
||||
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
|
||||
@@ -1048,7 +1036,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
const fileEditorKeymap = useUIStore((state) => state.fileEditorKeymap);
|
||||
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
|
||||
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
|
||||
const settingsExpandedEditorToolbar = useUIStore((state) => state.expandedEditorToolbar);
|
||||
|
||||
// Global mouseup to end drag selection
|
||||
React.useEffect(() => {
|
||||
@@ -1081,6 +1068,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return lines.slice(startLine - 1, endLine).join('\n');
|
||||
}, []);
|
||||
|
||||
const markdownPreviewRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const fileCommentController = useInlineCommentController<SelectedLineRange>({
|
||||
source: 'file',
|
||||
fileLabel: selectedFile?.path ?? null,
|
||||
@@ -1106,10 +1095,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
React.useEffect(() => {
|
||||
setLineSelection(null);
|
||||
reset();
|
||||
setMainTabGuard(null);
|
||||
setDraftContent('');
|
||||
setIsSaving(false);
|
||||
}, [selectedFile?.path, reset, setMainTabGuard]);
|
||||
}, [selectedFile?.path, reset]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setCommentSelection(lineSelection);
|
||||
@@ -1565,38 +1553,35 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
};
|
||||
}, [currentDirectory, debouncedSearchQuery, searchFiles, showHidden, showGitignored]);
|
||||
|
||||
const readFile = React.useCallback(async (path: string, options?: { allowOutsideWorkspace?: boolean; outsideFileGrant?: string; optional?: boolean }): Promise<string> => {
|
||||
const readFile = React.useCallback(async (path: string): Promise<string> => {
|
||||
const options = await resolveFileReadOptions(path);
|
||||
if (files.readFile) {
|
||||
const result = await files.readFile(path, { ...(options ?? {}), directory: root || undefined });
|
||||
const result = await files.readFile(path, { ...options, directory: root || undefined });
|
||||
return result.content ?? '';
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({ path });
|
||||
if (options?.allowOutsideWorkspace) {
|
||||
if (options.allowOutsideWorkspace) {
|
||||
params.set('allowOutsideWorkspace', 'true');
|
||||
}
|
||||
if (options?.outsideFileGrant) {
|
||||
if (options.outsideFileGrant) {
|
||||
params.set('outsideFileGrant', options.outsideFileGrant);
|
||||
}
|
||||
if (options?.optional) {
|
||||
params.set('optional', 'true');
|
||||
}
|
||||
if (root) {
|
||||
params.set('directory', root);
|
||||
}
|
||||
const response = await runtimeFetch(`/api/fs/read?${params.toString()}`, {
|
||||
cache: options?.optional ? 'no-store' : 'default',
|
||||
});
|
||||
const response = await runtimeFetch(`/api/fs/read?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error((error as { error?: string }).error || t('filesView.error.readFileFailed'));
|
||||
}
|
||||
return response.text();
|
||||
}, [files, root, t]);
|
||||
}, [files, resolveFileReadOptions, root, t]);
|
||||
|
||||
const readFileStat = React.useCallback(async (path: string, options?: { allowOutsideWorkspace?: boolean; outsideFileGrant?: string }): Promise<FileStatSnapshot | null> => {
|
||||
const readFileStat = React.useCallback(async (path: string): Promise<FileStatSnapshot | null> => {
|
||||
if (files.statFile) {
|
||||
const result = await files.statFile(path, { ...(options ?? {}), directory: root || undefined });
|
||||
const options = await resolveFileReadOptions(path);
|
||||
const result = await files.statFile(path, { ...options, directory: root || undefined });
|
||||
return {
|
||||
path: result.path,
|
||||
size: result.size,
|
||||
@@ -1604,7 +1589,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [files, root]);
|
||||
}, [files, resolveFileReadOptions, root]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!root || !files.statFile || openPaths.length === 0) {
|
||||
@@ -1616,7 +1601,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
|
||||
void Promise.all(paths.map(async (path) => {
|
||||
try {
|
||||
const stat = await files.statFile?.(path, { directory: root || undefined });
|
||||
const options = await resolveFileReadOptions(path);
|
||||
const stat = await files.statFile?.(path, { ...options, directory: root || undefined });
|
||||
if (!cancelled && stat && !stat.isFile) {
|
||||
removeOpenPathsByPrefix(root, path);
|
||||
}
|
||||
@@ -1630,7 +1616,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [files, openPaths, removeOpenPathsByPrefix, root]);
|
||||
}, [files, openPaths, removeOpenPathsByPrefix, resolveFileReadOptions, root]);
|
||||
|
||||
const displayedContent = React.useMemo(() =>
|
||||
fileContent.length > MAX_VIEW_CHARS
|
||||
@@ -1721,32 +1707,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
}
|
||||
}, [contentDetectedBinary, draftContent, fileContent, fileLoading, files, isDirty, loadedFileLineEnding, loadedFilePath, readFileStat, root, selectedFile, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isDirty) {
|
||||
setMainTabGuard(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const guard = (_nextTab: import('@/stores/useUIStore').MainTab) => {
|
||||
if (skipDirtyOnceRef.current) {
|
||||
skipDirtyOnceRef.current = false;
|
||||
return true;
|
||||
}
|
||||
setConfirmDiscardOpen(true);
|
||||
pendingTabRef.current = _nextTab;
|
||||
return false;
|
||||
};
|
||||
|
||||
setMainTabGuard(guard);
|
||||
|
||||
return () => {
|
||||
const currentGuard = useUIStore.getState().mainTabGuard;
|
||||
if (currentGuard === guard) {
|
||||
setMainTabGuard(null);
|
||||
}
|
||||
};
|
||||
}, [isDirty, setMainTabGuard]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (autoSaveEnabled) {
|
||||
return;
|
||||
@@ -1843,6 +1803,13 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
setDesktopImageSrc('');
|
||||
setLoadedFilePath(null);
|
||||
setContentDetectedBinary(false);
|
||||
setFileLoading(true);
|
||||
|
||||
// Prime asset URLs; read and stat resolve again immediately before their calls.
|
||||
await resolveFileReadOptions(node.path);
|
||||
if (!isCurrentLoad()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIsImage = isImageFile(node.path);
|
||||
const isSvg = isSvgFile(node.path);
|
||||
@@ -1857,7 +1824,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
if (runtime.isDesktop && selectedIsImage && !isSvg) {
|
||||
setFileContent('');
|
||||
setDraftContent('');
|
||||
setFileLoading(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1888,15 +1854,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
setFileLoading(true);
|
||||
|
||||
const outsideFileGrant = getOutsideFileGrant(node.path);
|
||||
const readOptions = {
|
||||
allowOutsideWorkspace: mode === 'editor-only' && Boolean(root) && !isPathWithinRoot(node.path, root),
|
||||
outsideFileGrant,
|
||||
};
|
||||
|
||||
await readFile(node.path, readOptions)
|
||||
await readFile(node.path)
|
||||
.then((content) => {
|
||||
if (!isCurrentLoad()) {
|
||||
return;
|
||||
@@ -1917,7 +1875,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
? `${editorContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …`
|
||||
: editorContent);
|
||||
setLoadedFilePath(node.path);
|
||||
void readFileStat(node.path, readOptions)
|
||||
void readFileStat(node.path)
|
||||
.then((stat) => {
|
||||
if (stat && isCurrentLoad()) {
|
||||
lastLoadedFileStatRef.current = stat;
|
||||
@@ -1982,7 +1940,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
setFileLoading(false);
|
||||
}
|
||||
});
|
||||
}, [expandPaths, isMobile, loadDirectory, mode, readFile, readFileStat, removeOpenPathsByPrefix, root, runtime.isDesktop, searchQuery, setSelectedPath, t]);
|
||||
}, [expandPaths, isMobile, loadDirectory, readFile, readFileStat, removeOpenPathsByPrefix, resolveFileReadOptions, root, runtime.isDesktop, searchQuery, setSelectedPath, t]);
|
||||
|
||||
const ensurePathVisible = React.useCallback(async (targetPath: string, includeTarget: boolean) => {
|
||||
if (!root) {
|
||||
@@ -2108,7 +2066,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
void readFileStat(selectedFile.path, selectedFileReadOptions)
|
||||
void readFileStat(selectedFile.path)
|
||||
.then((latestStat) => {
|
||||
if (cancelled || !latestStat) {
|
||||
return;
|
||||
@@ -2144,15 +2102,13 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
cancelled = true;
|
||||
window.clearInterval(interval);
|
||||
};
|
||||
}, [loadedFilePath, readFileStat, selectedFile?.path, selectedFileReadOptions]);
|
||||
}, [loadedFilePath, readFileStat, selectedFile?.path]);
|
||||
|
||||
const discardAndContinue = React.useCallback(() => {
|
||||
const nextFile = pendingSelectFileRef.current;
|
||||
const nextTab = pendingTabRef.current;
|
||||
const closePath = pendingClosePathRef.current;
|
||||
|
||||
pendingSelectFileRef.current = null;
|
||||
pendingTabRef.current = null;
|
||||
pendingClosePathRef.current = null;
|
||||
|
||||
// Allow one guarded navigation (tab/file) without re-opening dialog.
|
||||
@@ -2191,15 +2147,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextTab) {
|
||||
setMainTabGuard(null);
|
||||
useUIStore.getState().setActiveMainTab(nextTab);
|
||||
}
|
||||
}, [displayedContent, handleSelectFile, isMobile, removeOpenPath, root, selectedFile?.path, setMainTabGuard, setSelectedPath]);
|
||||
}, [displayedContent, handleSelectFile, isMobile, removeOpenPath, root, selectedFile?.path, setSelectedPath]);
|
||||
|
||||
const saveAndContinue = React.useCallback(async () => {
|
||||
const nextFile = pendingSelectFileRef.current;
|
||||
const nextTab = pendingTabRef.current;
|
||||
const closePath = pendingClosePathRef.current;
|
||||
|
||||
const saved = await saveDraft();
|
||||
@@ -2209,7 +2160,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
}
|
||||
|
||||
pendingSelectFileRef.current = null;
|
||||
pendingTabRef.current = null;
|
||||
pendingClosePathRef.current = null;
|
||||
|
||||
// We'll proceed after saving; suppress guard reopening.
|
||||
@@ -2245,11 +2195,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextTab) {
|
||||
setMainTabGuard(null);
|
||||
useUIStore.getState().setActiveMainTab(nextTab);
|
||||
}
|
||||
}, [handleSelectFile, isMobile, removeOpenPath, root, saveDraft, selectedFile?.path, setMainTabGuard, setSelectedPath]);
|
||||
}, [handleSelectFile, isMobile, removeOpenPath, root, saveDraft, selectedFile?.path, setSelectedPath]);
|
||||
|
||||
const handleCloseFile = React.useCallback((path: string) => {
|
||||
const isActive = selectedFile?.path === path;
|
||||
@@ -2614,12 +2560,12 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
diagramXmlRef.current = xml;
|
||||
diagramSavedXmlRef.current = xml;
|
||||
setDraftContent(xml);
|
||||
const stat = await readFileStat(path, selectedFileReadOptions).catch(() => null);
|
||||
const stat = await readFileStat(path).catch(() => null);
|
||||
if (stat) {
|
||||
lastLoadedFileStatRef.current = stat;
|
||||
}
|
||||
return true;
|
||||
}, [files, readFileStat, selectedFileReadOptions, t]);
|
||||
}, [files, readFileStat, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
@@ -3046,7 +2992,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
[lightTheme.metadata.id, darkTheme.metadata.id],
|
||||
);
|
||||
|
||||
const pdfAssetAuthKey = selectedFile?.path && isSelectedPdf
|
||||
const pdfAssetAuthKey = selectedFile?.path
|
||||
&& isSelectedPdf
|
||||
&& (!selectedFileReadOptions.allowOutsideWorkspace || selectedFileReadOptions.outsideFileGrant)
|
||||
? `${selectedFile.path}|${selectedFileReadOptions.allowOutsideWorkspace ? 'outside' : 'workspace'}|${selectedFileReadOptions.outsideFileGrant ?? ''}|${fileContentRevision}`
|
||||
: '';
|
||||
|
||||
@@ -3069,7 +3017,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
: desktopImageSrc)
|
||||
: '';
|
||||
|
||||
const pdfSrc = selectedFile?.path && isSelectedPdf && pdfAssetAuthReadyKey === pdfAssetAuthKey
|
||||
const pdfSrc = selectedFile?.path && isSelectedPdf && pdfAssetAuthKey && pdfAssetAuthReadyKey === pdfAssetAuthKey
|
||||
? getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', {
|
||||
path: selectedFile.path,
|
||||
allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined,
|
||||
@@ -3101,14 +3049,19 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
|
||||
setFileError(null);
|
||||
|
||||
const readOptions = await resolveFileReadOptions(selectedFile.path);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const srcPromise = files.readFileBinary
|
||||
? files.readFileBinary(selectedFile.path, selectedFileReadOptions).then((result) => result.dataUrl)
|
||||
? files.readFileBinary(selectedFile.path, readOptions).then((result) => result.dataUrl)
|
||||
: (async () => {
|
||||
const response = await runtimeFetch('/api/fs/raw', {
|
||||
query: {
|
||||
path: selectedFile.path,
|
||||
allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined,
|
||||
outsideFileGrant: selectedFileReadOptions.outsideFileGrant,
|
||||
allowOutsideWorkspace: readOptions.allowOutsideWorkspace ? 'true' : undefined,
|
||||
outsideFileGrant: readOptions.outsideFileGrant,
|
||||
directory: root || undefined,
|
||||
},
|
||||
});
|
||||
@@ -3154,7 +3107,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
}, [fileContentRevision, files, isSelectedImage, isSelectedSvg, root, selectedFile?.path, selectedFileReadOptions, t]);
|
||||
}, [fileContentRevision, files, isSelectedImage, isSelectedSvg, resolveFileReadOptions, root, selectedFile?.path, selectedFileReadOptions, t]);
|
||||
|
||||
const handleCloseDialog = React.useCallback(() => setActiveDialog(null), []);
|
||||
|
||||
@@ -3817,9 +3770,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Row 2: Docked editor toolbar (expanded). Desktop opt-in; ALWAYS on
|
||||
for mobile — floating hover controls don't work with touch. */}
|
||||
{(settingsExpandedEditorToolbar || isMobile) && selectedFile ? (
|
||||
{/* Row 2: Docked editor toolbar. */}
|
||||
{selectedFile ? (
|
||||
<div className="flex min-w-0 items-center gap-3 border-t border-border/40 bg-[var(--surface-subtle)] px-3 py-1">
|
||||
{/* Mobile hosts already show the file name in their own header;
|
||||
a truncated duplicate here just eats toolbar width. */}
|
||||
@@ -3840,69 +3792,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 min-w-0 relative">
|
||||
{selectedFile && !isSearchOpen && !(settingsExpandedEditorToolbar || isMobile) && (
|
||||
<div
|
||||
ref={floatingToolbarRef}
|
||||
className="absolute right-3 top-3 z-30"
|
||||
onMouseLeave={() => {
|
||||
if (toolbarDropdownOpenCountRef.current > 0) return;
|
||||
setIsFloatingToolbarOpen(false);
|
||||
}}
|
||||
>
|
||||
{isFloatingToolbarOpen ? (
|
||||
renderFloatingFileControls()
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
{isMarkdown ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => saveMdViewMode(getMdViewMode() === 'preview' ? 'edit' : 'preview')}
|
||||
className={cn(
|
||||
'size-8 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-0 shadow-sm transition-colors',
|
||||
getMdViewMode() === 'preview'
|
||||
? 'bg-[var(--interactive-selection)] text-[var(--interactive-selection-foreground)] hover:bg-[var(--interactive-selection)]'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
aria-label={t(getMdViewMode() === 'preview' ? 'filesView.editor.switchToEditMode' : 'filesView.editor.switchToPreviewMode')}
|
||||
title={t(getMdViewMode() === 'preview' ? 'filesView.editor.switchToEditMode' : 'filesView.editor.switchToPreviewMode')}
|
||||
>
|
||||
<Icon name={getMdViewMode() === 'preview' ? 'eye' : 'eye-off'} className="size-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{t(getMdViewMode() === 'preview' ? 'filesView.editor.switchToEditMode' : 'filesView.editor.switchToPreviewMode')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="inline-flex"
|
||||
onMouseEnter={() => setIsFloatingToolbarOpen(true)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsFloatingToolbarOpen(true)}
|
||||
className="size-8 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-0 text-muted-foreground shadow-sm hover:text-foreground"
|
||||
aria-label={t('filesView.editor.showControlsAria')}
|
||||
title={t('filesView.editor.controlsTitle')}
|
||||
>
|
||||
<Icon name="more-2-fill" className="size-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>{t('filesView.editor.controlsTitle')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ScrollableOverlay ref={mainViewVirtualizer.setScroller} outerClassName="h-full min-w-0" className={cn('h-full min-w-0', isLargeFile && '[overflow-anchor:none]')}>
|
||||
{!selectedFile ? (
|
||||
<div className="p-3 typography-ui text-muted-foreground">{t('filesView.editor.pickFileFromTree')}</div>
|
||||
@@ -3980,7 +3869,12 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
) : selectedFile && isMarkdown && getMdViewMode() === 'preview' ? (
|
||||
<div className="h-full overflow-auto p-3">
|
||||
<div className="oc-file-preview h-full overflow-auto p-3" ref={markdownPreviewRef}>
|
||||
<FilePreviewCommentMenu
|
||||
containerRef={markdownPreviewRef}
|
||||
filePath={selectedFile.path}
|
||||
fileContent={fileContent}
|
||||
/>
|
||||
{fileContent.length > 500 * 1024 && (
|
||||
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
|
||||
{t('filesView.warning.largeFilePreviewLimited', { sizeKb: Math.round(fileContent.length / 1024) })}
|
||||
@@ -4346,7 +4240,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
) : null}
|
||||
</div>
|
||||
) : isMarkdown && getMdViewMode() === 'preview' ? (
|
||||
<div className="h-full overflow-auto p-4">
|
||||
<div className="oc-file-preview h-full overflow-auto p-4" ref={markdownPreviewRef}>
|
||||
{selectedFile ? (
|
||||
<FilePreviewCommentMenu
|
||||
containerRef={markdownPreviewRef}
|
||||
filePath={selectedFile.path}
|
||||
fileContent={fileContent}
|
||||
/>
|
||||
) : null}
|
||||
{fileContent.length > 500 * 1024 && (
|
||||
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
|
||||
{t('filesView.warning.largeFilePreviewLimited', { sizeKb: Math.round(fileContent.length / 1024) })}
|
||||
|
||||
Reference in New Issue
Block a user