feat(markdown): intercept links in MarkdownPane — relative files navigate in Files tab, external URLs open in browser
This commit is contained in:
parent
91aa711ef3
commit
cd438c2cf3
3 changed files with 75 additions and 3 deletions
|
|
@ -552,6 +552,19 @@ async fn remote_pty_kill(state: State<'_, AppState>, machine_id: String, id: Str
|
||||||
state.remote_manager.pty_kill(&machine_id, &id).await
|
state.remote_manager.pty_kill(&machine_id, &id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn open_url(url: String) -> Result<(), String> {
|
||||||
|
// Only allow http/https URLs
|
||||||
|
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||||
|
return Err("Only http/https URLs are allowed".into());
|
||||||
|
}
|
||||||
|
std::process::Command::new("xdg-open")
|
||||||
|
.arg(&url)
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to open URL: {e}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
// Force dark GTK theme for native dialogs (file chooser, etc.)
|
// Force dark GTK theme for native dialogs (file chooser, etc.)
|
||||||
|
|
@ -616,6 +629,7 @@ pub fn run() {
|
||||||
project_agent_state_load,
|
project_agent_state_load,
|
||||||
cli_get_group,
|
cli_get_group,
|
||||||
pick_directory,
|
pick_directory,
|
||||||
|
open_url,
|
||||||
frontend_log,
|
frontend_log,
|
||||||
])
|
])
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@
|
||||||
filePath: string;
|
filePath: string;
|
||||||
paneId: string;
|
paneId: string;
|
||||||
onExit?: () => void;
|
onExit?: () => void;
|
||||||
|
onNavigate?: (absolutePath: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { filePath, paneId, onExit }: Props = $props();
|
let { filePath, paneId, onExit, onNavigate }: Props = $props();
|
||||||
|
|
||||||
let renderedHtml = $state('');
|
let renderedHtml = $state('');
|
||||||
let error = $state('');
|
let error = $state('');
|
||||||
|
|
@ -72,13 +73,59 @@
|
||||||
unlisten?.();
|
unlisten?.();
|
||||||
unwatchFile(paneId).catch(() => {});
|
unwatchFile(paneId).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleLinkClick(event: MouseEvent) {
|
||||||
|
const anchor = (event.target as HTMLElement).closest('a');
|
||||||
|
if (!anchor) return;
|
||||||
|
|
||||||
|
const href = anchor.getAttribute('href');
|
||||||
|
if (!href) return;
|
||||||
|
|
||||||
|
// Anchor links — scroll within page
|
||||||
|
if (href.startsWith('#')) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// External URLs — open in system browser
|
||||||
|
if (/^https?:\/\//.test(href)) {
|
||||||
|
import('@tauri-apps/api/core').then(({ invoke }) => {
|
||||||
|
invoke('open_url', { url: href }).catch(() => {
|
||||||
|
// Fallback: do nothing (no shell plugin)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relative file link — resolve against current file's directory
|
||||||
|
if (onNavigate) {
|
||||||
|
const dir = filePath.replace(/\/[^/]*$/, '');
|
||||||
|
const resolved = resolveRelativePath(dir, href);
|
||||||
|
onNavigate(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRelativePath(base: string, relative: string): string {
|
||||||
|
// Strip any anchor or query from the link
|
||||||
|
const cleanRelative = relative.split('#')[0].split('?')[0];
|
||||||
|
const parts = base.split('/');
|
||||||
|
for (const segment of cleanRelative.split('/')) {
|
||||||
|
if (segment === '..') {
|
||||||
|
parts.pop();
|
||||||
|
} else if (segment !== '.' && segment !== '') {
|
||||||
|
parts.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts.join('/');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="markdown-pane">
|
<div class="markdown-pane">
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="error">{error}</div>
|
<div class="error">{error}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="markdown-pane-scroll">
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="markdown-pane-scroll" onclick={handleLinkClick}>
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
{@html renderedHtml}
|
{@html renderedHtml}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,17 @@
|
||||||
loadFiles(cwd);
|
loadFiles(cwd);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleNavigate(absolutePath: string) {
|
||||||
|
// If the file is in our discovered list, select it directly
|
||||||
|
const match = files.find(f => f.path === absolutePath);
|
||||||
|
if (match) {
|
||||||
|
selectedPath = absolutePath;
|
||||||
|
} else {
|
||||||
|
// File not in sidebar — set it directly (MarkdownPane handles loading)
|
||||||
|
selectedPath = absolutePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadFiles(dir: string) {
|
async function loadFiles(dir: string) {
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
|
|
@ -58,7 +69,7 @@
|
||||||
|
|
||||||
<main class="doc-content">
|
<main class="doc-content">
|
||||||
{#if selectedPath}
|
{#if selectedPath}
|
||||||
<MarkdownPane paneId="pf-{projectName}" filePath={selectedPath} />
|
<MarkdownPane paneId="pf-{projectName}" filePath={selectedPath} onNavigate={handleNavigate} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="state-msg full">Select a file</div>
|
<div class="state-msg full">Select a file</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue