mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-01 19:50:41 +02:00
refac
This commit is contained in:
@@ -19,6 +19,26 @@ export type TerminalFileSearchResponse = {
|
||||
results: TerminalFileSearchResult[];
|
||||
};
|
||||
|
||||
export type TerminalContentMatch = {
|
||||
line: number;
|
||||
column: number;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type TerminalFileMatch = {
|
||||
path: string;
|
||||
relative_path: string;
|
||||
name: string;
|
||||
type: 'file' | 'directory';
|
||||
name_match: boolean;
|
||||
content_matches: TerminalContentMatch[];
|
||||
};
|
||||
|
||||
export type TerminalFileMatchesResponse = {
|
||||
results: TerminalFileMatch[];
|
||||
next_offset: number | null;
|
||||
};
|
||||
|
||||
export type ListeningPort = {
|
||||
port: number;
|
||||
pid: number | null;
|
||||
@@ -194,6 +214,37 @@ export const searchFiles = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const getFileMatches = async (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
query: string,
|
||||
path: string = '.',
|
||||
showHidden: boolean = false,
|
||||
offset: number = 0,
|
||||
sessionId?: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<TerminalFileMatchesResponse | null> => {
|
||||
const headers: Record<string, string> = bearerHeaders(apiKey);
|
||||
if (sessionId) headers['X-Session-Id'] = sessionId;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
query,
|
||||
path,
|
||||
show_hidden: String(showHidden),
|
||||
offset: String(offset)
|
||||
});
|
||||
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/files/matches?${params.toString()}`, {
|
||||
headers,
|
||||
signal
|
||||
}).catch((err) => {
|
||||
if (err?.name !== 'AbortError') console.error('open-terminal getFileMatches error:', err);
|
||||
return null;
|
||||
});
|
||||
if (!res?.ok) return null;
|
||||
const json = await res.json().catch(() => null);
|
||||
return Array.isArray(json?.results) ? json : null;
|
||||
};
|
||||
|
||||
export const readFile = async (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import {
|
||||
getCwd,
|
||||
getTerminalConfig,
|
||||
getFileMatches,
|
||||
listFiles,
|
||||
readFile,
|
||||
downloadFileBlob,
|
||||
@@ -29,6 +30,8 @@
|
||||
moveEntry,
|
||||
setCwd,
|
||||
type FileEntry,
|
||||
type TerminalContentMatch,
|
||||
type TerminalFileMatch,
|
||||
type TerminalFileRoot,
|
||||
type TerminalCwd
|
||||
} from '$lib/apis/terminal';
|
||||
@@ -106,6 +109,12 @@
|
||||
depth: number;
|
||||
rowIndex: number;
|
||||
};
|
||||
type FileSearchTarget = {
|
||||
line: number;
|
||||
column: number;
|
||||
length: number;
|
||||
requestId: number;
|
||||
};
|
||||
|
||||
let sortBy: SortMode = 'name';
|
||||
let sortAsc = true;
|
||||
@@ -115,6 +124,22 @@
|
||||
let treeCache: Map<string, FileEntry[]> = new Map();
|
||||
let loadingDirs: Set<string> = new Set();
|
||||
let directoryMenu: { x: number; y: number } | null = null;
|
||||
let searchQuery = '';
|
||||
let matchResults: TerminalFileMatch[] | null = null;
|
||||
let matchLoading = false;
|
||||
let matchLoadingMore = false;
|
||||
let matchError: string | null = null;
|
||||
let matchLoadMoreError = false;
|
||||
let nextMatchOffset: number | null = null;
|
||||
let matchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let matchController: AbortController | null = null;
|
||||
let matchRequestId = 0;
|
||||
let searchTargetRequestId = 0;
|
||||
|
||||
$: searchText = searchQuery.trim();
|
||||
$: isSearching = Boolean(searchText);
|
||||
$: filenameMatches = matchResults?.filter((match) => match.name_match) ?? [];
|
||||
$: contentOnlyMatches = matchResults?.filter((match) => !match.name_match) ?? [];
|
||||
|
||||
/** Normalize Windows backslashes and collapse duplicate separators. */
|
||||
const normalizePath = (path: string) => path.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
||||
@@ -276,6 +301,7 @@
|
||||
let fileDocxData: ArrayBuffer | null = null;
|
||||
let fileLoading = false;
|
||||
let filePreviewRef: FilePreview;
|
||||
let fileSearchTarget: FileSearchTarget | null = null;
|
||||
|
||||
// ── Office preview state ────────────────────────────────────────────
|
||||
let fileOfficeHtml: string | null = null;
|
||||
@@ -447,6 +473,132 @@
|
||||
directoryMenu = null;
|
||||
};
|
||||
|
||||
const parentDirectoryPath = (path: string) => {
|
||||
const normalized = normalizePath(path);
|
||||
const slash = normalized.lastIndexOf('/');
|
||||
return slash > 0 ? asDirectoryPath(normalized.slice(0, slash)) : '/';
|
||||
};
|
||||
|
||||
const relativeParentPath = (path: string) => {
|
||||
const slash = path.lastIndexOf('/');
|
||||
return slash === -1 ? '' : path.slice(0, slash);
|
||||
};
|
||||
|
||||
const clearMatchRequest = () => {
|
||||
if (matchTimer) {
|
||||
clearTimeout(matchTimer);
|
||||
matchTimer = null;
|
||||
}
|
||||
matchController?.abort();
|
||||
matchController = null;
|
||||
};
|
||||
|
||||
const resetMatches = () => {
|
||||
matchResults = null;
|
||||
matchLoading = false;
|
||||
matchLoadingMore = false;
|
||||
matchError = null;
|
||||
matchLoadMoreError = false;
|
||||
nextMatchOffset = null;
|
||||
};
|
||||
|
||||
const queueFileSearch = (
|
||||
query: string,
|
||||
terminal: { url: string; key: string } | null,
|
||||
path: string,
|
||||
hiddenVisible: boolean,
|
||||
activeFile: string | null,
|
||||
activePort: number | null
|
||||
) => {
|
||||
clearMatchRequest();
|
||||
matchRequestId += 1;
|
||||
const requestId = matchRequestId;
|
||||
if (!query || !terminal || activeFile || activePort !== null) {
|
||||
resetMatches();
|
||||
return;
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
closeDirectoryMenu();
|
||||
creatingFolder = false;
|
||||
creatingFile = false;
|
||||
matchLoading = true;
|
||||
matchLoadingMore = false;
|
||||
matchError = null;
|
||||
matchLoadMoreError = false;
|
||||
nextMatchOffset = null;
|
||||
matchResults = null;
|
||||
const controller = new AbortController();
|
||||
matchController = controller;
|
||||
matchTimer = setTimeout(async () => {
|
||||
const data = await getFileMatches(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
query,
|
||||
path,
|
||||
hiddenVisible,
|
||||
0,
|
||||
chatId ?? undefined,
|
||||
controller.signal
|
||||
);
|
||||
if (requestId !== matchRequestId) return;
|
||||
if (data) {
|
||||
matchResults = data.results;
|
||||
nextMatchOffset = data.next_offset;
|
||||
} else if (!controller.signal.aborted) {
|
||||
matchError = $i18n.t('Failed to search files');
|
||||
matchResults = [];
|
||||
}
|
||||
matchLoading = false;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
$: queueFileSearch(searchText, selectedTerminal, currentPath, showHidden, selectedFile, previewPort);
|
||||
|
||||
const loadMoreMatches = async () => {
|
||||
const offset = nextMatchOffset;
|
||||
if (offset === null || !isSearching || matchLoading || matchLoadingMore || matchLoadMoreError) {
|
||||
return;
|
||||
}
|
||||
const terminal = selectedTerminal;
|
||||
if (!terminal) return;
|
||||
|
||||
const requestId = matchRequestId;
|
||||
matchLoadingMore = true;
|
||||
const controller = new AbortController();
|
||||
matchController = controller;
|
||||
const data = await getFileMatches(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
searchText,
|
||||
currentPath,
|
||||
showHidden,
|
||||
offset,
|
||||
chatId ?? undefined,
|
||||
controller.signal
|
||||
);
|
||||
if (requestId === matchRequestId) {
|
||||
if (data) {
|
||||
matchResults = [...(matchResults ?? []), ...data.results];
|
||||
nextMatchOffset = data.next_offset;
|
||||
} else if (!controller.signal.aborted) {
|
||||
matchLoadMoreError = true;
|
||||
}
|
||||
matchLoadingMore = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMoreOnVisible = (node: HTMLElement) => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) void loadMoreMatches();
|
||||
},
|
||||
{ rootMargin: '160px' }
|
||||
);
|
||||
observer.observe(node);
|
||||
return { destroy: () => observer.disconnect() };
|
||||
};
|
||||
|
||||
const labelFromPath = (path: string) => {
|
||||
const parts = normalizePath(path).split('/').filter(Boolean);
|
||||
return parts.at(-1) ?? '/';
|
||||
@@ -538,6 +690,7 @@
|
||||
|
||||
// ── File preview management ──────────────────────────────────────────
|
||||
const clearFilePreview = () => {
|
||||
fileSearchTarget = null;
|
||||
fileContent = null;
|
||||
if (fileImageUrl) {
|
||||
URL.revokeObjectURL(fileImageUrl);
|
||||
@@ -762,6 +915,33 @@
|
||||
fileLoading = false;
|
||||
};
|
||||
|
||||
const openFileMatch = async (match: TerminalFileMatch) => {
|
||||
if (match.type === 'directory') {
|
||||
searchQuery = '';
|
||||
await loadDir(match.path);
|
||||
return;
|
||||
}
|
||||
await openEntry({
|
||||
name: match.name,
|
||||
type: 'file',
|
||||
size: 0,
|
||||
fullPath: match.path,
|
||||
parentPath: parentDirectoryPath(match.path),
|
||||
depth: 0,
|
||||
rowIndex: -1
|
||||
} as BrowserRow);
|
||||
};
|
||||
|
||||
const openContentMatch = async (match: TerminalFileMatch, contentMatch: TerminalContentMatch) => {
|
||||
await openFileMatch(match);
|
||||
fileSearchTarget = {
|
||||
line: contentMatch.line,
|
||||
column: contentMatch.column,
|
||||
length: searchText.length,
|
||||
requestId: ++searchTargetRequestId
|
||||
};
|
||||
};
|
||||
|
||||
let downloading = false;
|
||||
|
||||
const downloadFile = async (path: string) => {
|
||||
@@ -1247,6 +1427,7 @@
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearMatchRequest();
|
||||
if (fileImageUrl) URL.revokeObjectURL(fileImageUrl);
|
||||
if (fileVideoUrl) URL.revokeObjectURL(fileVideoUrl);
|
||||
if (fileAudioUrl) URL.revokeObjectURL(fileAudioUrl);
|
||||
@@ -1302,13 +1483,13 @@
|
||||
<div
|
||||
bind:this={containerEl}
|
||||
class="flex flex-col h-full min-h-0 min-w-0 relative"
|
||||
on:dragover={handleDragOver}
|
||||
on:dragover={(e) => !isSearching && handleDragOver(e)}
|
||||
on:dragleave={() => (isDragOver = false)}
|
||||
on:drop={handleDrop}
|
||||
on:drop={(e) => !isSearching && handleDrop(e)}
|
||||
role="region"
|
||||
aria-label={$i18n.t('File browser')}
|
||||
>
|
||||
{#if isDragOver}
|
||||
{#if isDragOver && !isSearching}
|
||||
<div
|
||||
class="absolute inset-1 z-10 flex items-center justify-center rounded-lg border-2 border-dashed border-blue-400 bg-blue-500/10 dark:border-blue-500 pointer-events-none"
|
||||
>
|
||||
@@ -1479,8 +1660,31 @@
|
||||
</Tooltip>
|
||||
</FileNavToolbar>
|
||||
|
||||
{#if !selectedFile}
|
||||
<div
|
||||
class="flex h-8 shrink-0 items-center gap-1.5 border-b border-gray-50 px-3 dark:border-gray-850/30"
|
||||
>
|
||||
<Icon name="search" size={13} strokeWidth={1.5} class="shrink-0 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
class="min-w-0 flex-1 border-none bg-transparent text-xs text-gray-800 outline-none placeholder:text-gray-400 dark:text-gray-200 dark:placeholder:text-gray-600"
|
||||
placeholder={$i18n.t('Search files and contents')}
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
{#if searchQuery}
|
||||
<button
|
||||
class="flex shrink-0 items-center text-gray-400 transition hover:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click={() => (searchQuery = '')}
|
||||
aria-label={$i18n.t('Clear search')}
|
||||
>
|
||||
<Icon name="xmark" size={11} strokeWidth={1.5} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Bulk action bar -->
|
||||
{#if selectedCount > 0}
|
||||
{#if selectedCount > 0 && !isSearching}
|
||||
<BulkActionBar
|
||||
count={selectedCount}
|
||||
canDelete={selectedEntriesWritable}
|
||||
@@ -1503,7 +1707,7 @@
|
||||
if (e.target === e.currentTarget && selectedCount > 0) clearSelection();
|
||||
}}
|
||||
on:contextmenu={(e) => {
|
||||
if (selectedFile || previewPort !== null) return;
|
||||
if (selectedFile || previewPort !== null || isSearching) return;
|
||||
if ((e.target as HTMLElement)?.closest('[data-file-row]')) return;
|
||||
e.preventDefault();
|
||||
directoryMenu = { x: e.clientX, y: e.clientY };
|
||||
@@ -1539,6 +1743,7 @@
|
||||
{fileOfficeSlides}
|
||||
{excelSheetNames}
|
||||
{selectedExcelSheet}
|
||||
searchTarget={fileSearchTarget}
|
||||
onSheetChange={async (sheet) => {
|
||||
if (!excelWorkbook) return;
|
||||
selectedExcelSheet = sheet;
|
||||
@@ -1564,7 +1769,125 @@
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
{#if uploading}
|
||||
{#if isSearching}
|
||||
{#if matchLoading}
|
||||
<div class="flex justify-center pt-8"><Spinner className="size-4" /></div>
|
||||
{:else if matchError}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">{matchError}</div>
|
||||
</div>
|
||||
{:else if !matchResults?.length}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">{$i18n.t('No matches')}</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if filenameMatches.length > 0}
|
||||
<div
|
||||
class="px-2 pt-1 pb-0.5 text-[0.625rem] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{$i18n.t('Filename matches')}
|
||||
</div>
|
||||
{#each filenameMatches as match (match.path)}
|
||||
<button
|
||||
class="flex h-7 w-full items-center gap-2 rounded-lg px-2 text-left transition-colors duration-75 hover:bg-gray-50 dark:hover:bg-white/4"
|
||||
on:click={() => openFileMatch(match)}
|
||||
>
|
||||
<FileTypeIcon name={match.name} type={match.type} />
|
||||
<span class="min-w-0 flex-1 truncate text-xs text-gray-800 dark:text-gray-200">
|
||||
{match.name}
|
||||
{#if relativeParentPath(match.relative_path)}
|
||||
<span class="ml-1.5 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{relativeParentPath(match.relative_path)}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{#if match.content_matches.length > 0}
|
||||
{@const preview = match.content_matches[0]}
|
||||
<button
|
||||
class="flex h-6 w-full items-center gap-2 rounded-lg pl-8 pr-2 text-left transition-colors duration-75 hover:bg-gray-50 dark:hover:bg-white/4"
|
||||
on:click={() => openContentMatch(match, preview)}
|
||||
>
|
||||
<span
|
||||
class="w-6 shrink-0 text-right font-mono text-[0.625rem] text-gray-400 dark:text-gray-600"
|
||||
>{preview.line}</span
|
||||
>
|
||||
<span
|
||||
class="min-w-0 flex-1 truncate font-mono text-[0.6875rem] text-gray-500 dark:text-gray-500"
|
||||
>{preview.text}</span
|
||||
>
|
||||
{#if match.content_matches.length > 1}
|
||||
<span class="shrink-0 text-[0.625rem] text-gray-400 dark:text-gray-600">
|
||||
+{match.content_matches.length - 1}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if contentOnlyMatches.length > 0}
|
||||
<div
|
||||
class="px-2 pt-2 pb-0.5 text-[0.625rem] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{$i18n.t('Content matches')}
|
||||
</div>
|
||||
{#each contentOnlyMatches as match (match.path)}
|
||||
<button
|
||||
class="flex h-7 w-full items-center gap-2 rounded-lg px-2 text-left transition-colors duration-75 hover:bg-gray-50 dark:hover:bg-white/4"
|
||||
on:click={() => openFileMatch(match)}
|
||||
>
|
||||
<FileTypeIcon name={match.name} type={match.type} />
|
||||
<span class="min-w-0 flex-1 truncate text-xs text-gray-800 dark:text-gray-200">
|
||||
{match.name}
|
||||
{#if relativeParentPath(match.relative_path)}
|
||||
<span class="ml-1.5 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{relativeParentPath(match.relative_path)}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{#if match.content_matches.length > 0}
|
||||
{@const preview = match.content_matches[0]}
|
||||
<button
|
||||
class="flex h-6 w-full items-center gap-2 rounded-lg pl-8 pr-2 text-left transition-colors duration-75 hover:bg-gray-50 dark:hover:bg-white/4"
|
||||
on:click={() => openContentMatch(match, preview)}
|
||||
>
|
||||
<span
|
||||
class="w-6 shrink-0 text-right font-mono text-[0.625rem] text-gray-400 dark:text-gray-600"
|
||||
>{preview.line}</span
|
||||
>
|
||||
<span
|
||||
class="min-w-0 flex-1 truncate font-mono text-[0.6875rem] text-gray-500 dark:text-gray-500"
|
||||
>{preview.text}</span
|
||||
>
|
||||
{#if match.content_matches.length > 1}
|
||||
<span class="shrink-0 text-[0.625rem] text-gray-400 dark:text-gray-600">
|
||||
+{match.content_matches.length - 1}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if nextMatchOffset !== null}
|
||||
<div use:loadMoreOnVisible class="flex h-8 items-center justify-center">
|
||||
{#if matchLoadingMore}
|
||||
<Spinner className="size-3" />
|
||||
{:else if matchLoadMoreError}
|
||||
<button
|
||||
class="text-[0.6875rem] text-gray-400 transition-colors duration-75 hover:text-gray-600 dark:text-gray-600 dark:hover:text-gray-400"
|
||||
on:click={() => {
|
||||
matchLoadMoreError = false;
|
||||
void loadMoreMatches();
|
||||
}}>{$i18n.t('Retry')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if uploading}
|
||||
<div class="flex items-center justify-center gap-2 p-4 text-xs text-gray-500">
|
||||
<Spinner className="size-4" />
|
||||
{$i18n.t('Uploading...')}
|
||||
@@ -1581,7 +1904,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !loading && !error && !uploading && !($selectedTerminalId && $terminalServers === null)}
|
||||
{#if !isSearching && !loading && !error && !uploading && !($selectedTerminalId && $terminalServers === null)}
|
||||
{#if creatingFolder}
|
||||
<div class="flex h-7 items-center gap-2 px-2">
|
||||
<FileTypeIcon name={newFolderName} type="directory" />
|
||||
@@ -1663,7 +1986,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Port detection -->
|
||||
{#if selectedTerminal && !selectedFile && previewPort === null}
|
||||
{#if selectedTerminal && !selectedFile && previewPort === null && !isSearching}
|
||||
<div class="shrink-0 border-t border-gray-50 dark:border-gray-850/30">
|
||||
<PortList
|
||||
baseUrl={selectedTerminal.url}
|
||||
|
||||
@@ -12,12 +12,19 @@
|
||||
export let value = '';
|
||||
export let filePath: string | null = null;
|
||||
export let onSave: ((content: string) => Promise<void>) | null = null;
|
||||
export let searchTarget: {
|
||||
line: number;
|
||||
column: number;
|
||||
length: number;
|
||||
requestId: number;
|
||||
} | null = null;
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let editor: EditorView | null = null;
|
||||
let editorTheme = new Compartment();
|
||||
let editorLanguage = new Compartment();
|
||||
let internalValue = '';
|
||||
let lastSearchTargetRequestId = 0;
|
||||
|
||||
/** Return the current editor content */
|
||||
export const getValue = (): string => {
|
||||
@@ -37,6 +44,20 @@
|
||||
editor?.focus();
|
||||
};
|
||||
|
||||
const revealSearchTarget = () => {
|
||||
if (!editor || !searchTarget || searchTarget.requestId === lastSearchTargetRequestId) return;
|
||||
lastSearchTargetRequestId = searchTarget.requestId;
|
||||
const lineNumber = Math.min(Math.max(searchTarget.line, 1), editor.state.doc.lines);
|
||||
const line = editor.state.doc.line(lineNumber);
|
||||
const from = line.from + Math.min(Math.max(searchTarget.column - 1, 0), line.length);
|
||||
const to = Math.min(from + Math.max(searchTarget.length, 1), line.to);
|
||||
editor.dispatch({
|
||||
selection: { anchor: from, head: to },
|
||||
effects: EditorView.scrollIntoView(from, { y: 'center' })
|
||||
});
|
||||
editor.focus();
|
||||
};
|
||||
|
||||
const detectLanguage = async (path: string | null) => {
|
||||
if (!path) return;
|
||||
const match = LanguageDescription.matchFilename(languages, path);
|
||||
@@ -61,6 +82,10 @@
|
||||
detectLanguage(filePath);
|
||||
}
|
||||
|
||||
$: if (editor && searchTarget) {
|
||||
revealSearchTarget();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
internalValue = value;
|
||||
@@ -105,6 +130,7 @@
|
||||
});
|
||||
|
||||
detectLanguage(filePath);
|
||||
revealSearchTarget();
|
||||
|
||||
// Watch dark mode
|
||||
const observer = new MutationObserver(() => {
|
||||
|
||||
@@ -47,6 +47,12 @@
|
||||
export let readOnly = false;
|
||||
|
||||
export let onSave: ((content: string) => Promise<void>) | null = null;
|
||||
export let searchTarget: {
|
||||
line: number;
|
||||
column: number;
|
||||
length: number;
|
||||
requestId: number;
|
||||
} | null = null;
|
||||
|
||||
export let editing = false;
|
||||
let editContent = '';
|
||||
@@ -350,7 +356,17 @@
|
||||
className="w-full h-full"
|
||||
/>
|
||||
{:else if fileContent !== null}
|
||||
{#if isHtml && !showRaw && serveUrl}
|
||||
{#if searchTarget}
|
||||
<div class="absolute inset-0">
|
||||
<FileCodeEditor
|
||||
bind:this={fileCodeEditorRef}
|
||||
value={fileContent ?? ''}
|
||||
filePath={selectedFile}
|
||||
onSave={readOnly ? null : onSave}
|
||||
{searchTarget}
|
||||
/>
|
||||
</div>
|
||||
{:else if isHtml && !showRaw && serveUrl}
|
||||
{#if overlay}
|
||||
<div class="absolute top-0 left-0 right-0 bottom-0 z-10"></div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user