Compare commits
No commits in common. "main" and "v1.12.8" have entirely different histories.
@ -14,4 +14,3 @@ pub mod updater;
|
||||
pub mod tools_commands_r2;
|
||||
pub mod workspace_commands;
|
||||
pub mod docker_commands;
|
||||
pub mod window_commands;
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
use tauri::AppHandle;
|
||||
use tauri::WebviewWindowBuilder;
|
||||
|
||||
/// Open a child window from the Rust side using WebviewWindowBuilder.
|
||||
///
|
||||
/// The `url` parameter supports hash fragments (e.g. "index.html#/tool/ping?sessionId=abc").
|
||||
/// WebviewUrl::App takes a PathBuf and cannot handle hash/query — so we load plain
|
||||
/// index.html and set the hash via JS after the window is created.
|
||||
#[tauri::command]
|
||||
pub fn open_child_window(
|
||||
app_handle: AppHandle,
|
||||
label: String,
|
||||
title: String,
|
||||
url: String,
|
||||
width: f64,
|
||||
height: f64,
|
||||
) -> Result<(), String> {
|
||||
// Split "index.html#/tool/ping?sessionId=abc" into path and fragment
|
||||
let (path, hash) = match url.split_once('#') {
|
||||
Some((p, h)) => (p.to_string(), Some(format!("#{}", h))),
|
||||
None => (url.clone(), None),
|
||||
};
|
||||
|
||||
let webview_url = tauri::WebviewUrl::App(path.into());
|
||||
let window = WebviewWindowBuilder::new(&app_handle, &label, webview_url)
|
||||
.title(&title)
|
||||
.inner_size(width, height)
|
||||
.resizable(true)
|
||||
.center()
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create window '{}': {}", label, e))?;
|
||||
|
||||
// Set the hash fragment after the window loads — this triggers App.vue's
|
||||
// onMounted hash detection to render the correct tool/detached component.
|
||||
if let Some(hash) = hash {
|
||||
let _ = window.eval(&format!("window.location.hash = '{}';", hash));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -234,7 +234,6 @@ pub fn run() {
|
||||
commands::updater::check_for_updates,
|
||||
commands::workspace_commands::save_workspace, commands::workspace_commands::load_workspace,
|
||||
commands::docker_commands::docker_list_containers, commands::docker_commands::docker_list_images, commands::docker_commands::docker_list_volumes, commands::docker_commands::docker_action,
|
||||
commands::window_commands::open_child_window,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
29
src/App.vue
29
src/App.vue
@ -2,12 +2,15 @@
|
||||
import { ref, onMounted, onErrorCaptured, defineAsyncComponent } from "vue";
|
||||
import { useAppStore } from "@/stores/app.store";
|
||||
import UnlockLayout from "@/layouts/UnlockLayout.vue";
|
||||
import ToolWindow from "@/components/tools/ToolWindow.vue";
|
||||
|
||||
const MainLayout = defineAsyncComponent({
|
||||
loader: () => import("@/layouts/MainLayout.vue"),
|
||||
onError(error) { console.error("[App] MainLayout load failed:", error); },
|
||||
});
|
||||
const ToolWindow = defineAsyncComponent({
|
||||
loader: () => import("@/components/tools/ToolWindow.vue"),
|
||||
onError(error) { console.error("[App] ToolWindow load failed:", error); },
|
||||
});
|
||||
const DetachedSession = defineAsyncComponent({
|
||||
loader: () => import("@/components/session/DetachedSession.vue"),
|
||||
onError(error) { console.error("[App] DetachedSession load failed:", error); },
|
||||
@ -16,6 +19,7 @@ const DetachedSession = defineAsyncComponent({
|
||||
const app = useAppStore();
|
||||
const appError = ref<string | null>(null);
|
||||
|
||||
// Tool window mode — detected from URL hash: #/tool/network-scanner?sessionId=abc
|
||||
const isToolMode = ref(false);
|
||||
const isDetachedMode = ref(false);
|
||||
const toolName = ref("");
|
||||
@ -27,39 +31,32 @@ onErrorCaptured((err) => {
|
||||
return false;
|
||||
});
|
||||
|
||||
/** Parse hash and set mode flags. Called on mount and on hashchange. */
|
||||
function applyHash(hash: string): void {
|
||||
onMounted(async () => {
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith("#/tool/")) {
|
||||
isToolMode.value = true;
|
||||
const rest = hash.substring(7);
|
||||
const rest = hash.substring(7); // after "#/tool/"
|
||||
const [name, query] = rest.split("?");
|
||||
toolName.value = name;
|
||||
toolSessionId.value = new URLSearchParams(query || "").get("sessionId") || "";
|
||||
} else if (hash.startsWith("#/detached-session")) {
|
||||
isDetachedMode.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Check hash at load time (present if JS-side WebviewWindow set it in the URL)
|
||||
applyHash(window.location.hash);
|
||||
|
||||
// Also listen for hash changes (Rust-side window sets hash via eval after load)
|
||||
window.addEventListener("hashchange", () => applyHash(window.location.hash));
|
||||
|
||||
// Only init vault for the main app window (no hash)
|
||||
if (!isToolMode.value && !isDetachedMode.value) {
|
||||
} else {
|
||||
await app.checkVaultState();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Error display for debugging -->
|
||||
<div v-if="appError" class="fixed inset-0 z-50 flex items-center justify-center bg-[#0d1117] text-red-400 p-8 text-sm font-mono whitespace-pre-wrap">
|
||||
{{ appError }}
|
||||
</div>
|
||||
<!-- Detached session window mode -->
|
||||
<DetachedSession v-else-if="isDetachedMode" />
|
||||
<!-- Tool popup window mode -->
|
||||
<ToolWindow v-else-if="isToolMode" :tool="toolName" :session-id="toolSessionId" />
|
||||
<!-- Normal app mode -->
|
||||
<div v-else class="app-root">
|
||||
<UnlockLayout v-if="!app.isUnlocked" />
|
||||
<MainLayout v-else />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-[48px] flex items-center justify-between px-6 bg-[var(--wraith-bg-secondary)] border-t border-[var(--wraith-border)] text-base text-[var(--wraith-text-muted)] shrink-0">
|
||||
<div class="h-6 flex items-center justify-between px-4 bg-[var(--wraith-bg-secondary)] border-t border-[var(--wraith-border)] text-[10px] text-[var(--wraith-text-muted)] shrink-0">
|
||||
<!-- Left: connection info -->
|
||||
<div class="flex items-center gap-3">
|
||||
<template v-if="sessionStore.activeSession">
|
||||
|
||||
@ -204,21 +204,15 @@ onBeforeUnmount(() => {
|
||||
if (resizeTimeout) { clearTimeout(resizeTimeout); resizeTimeout = null; }
|
||||
});
|
||||
|
||||
// Focus canvas, re-check dimensions, and force full frame on tab switch.
|
||||
// Uses 300ms delay to let the flex layout fully settle (copilot panel toggle, etc.)
|
||||
// Focus canvas, re-check dimensions, and force full frame on tab switch
|
||||
watch(
|
||||
() => props.isActive,
|
||||
(active) => {
|
||||
if (!active || !canvasRef.value) return;
|
||||
|
||||
// Immediate focus so keyboard works right away
|
||||
if (keyboardGrabbed.value) canvasRef.value.focus();
|
||||
|
||||
// Immediate force refresh to show SOMETHING while we check dimensions
|
||||
invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {});
|
||||
|
||||
// Delayed dimension check — layout needs time to settle
|
||||
setTimeout(() => {
|
||||
// Wait for layout to settle after tab becomes visible
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const wrapper = canvasWrapper.value;
|
||||
const canvas = canvasRef.value;
|
||||
if (!wrapper || !canvas) return;
|
||||
@ -227,22 +221,27 @@ watch(
|
||||
const newW = Math.round(cw) & ~1;
|
||||
const newH = Math.round(ch);
|
||||
|
||||
// If container size differs from canvas resolution, resize the RDP session
|
||||
if (newW >= 200 && newH >= 200 && (newW !== canvas.width || newH !== canvas.height)) {
|
||||
invoke("rdp_resize", {
|
||||
sessionId: props.sessionId,
|
||||
width: newW,
|
||||
height: newH,
|
||||
}).then(() => {
|
||||
if (canvas) {
|
||||
canvas.width = newW;
|
||||
canvas.height = newH;
|
||||
}
|
||||
setTimeout(() => {
|
||||
invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {});
|
||||
}, 500);
|
||||
}, 200);
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
// Same size — just refresh the frame
|
||||
invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {});
|
||||
}
|
||||
}, 300);
|
||||
|
||||
if (keyboardGrabbed.value) canvas.focus();
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
@ -133,14 +133,17 @@ async function detachTab(): Promise<void> {
|
||||
session.active = false;
|
||||
|
||||
// Open a new Tauri window for this session
|
||||
try {
|
||||
await invoke("open_child_window", {
|
||||
label: `detached-${session.id.substring(0, 8)}-${Date.now()}`,
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const label = `detached-${session.id.substring(0, 8)}-${Date.now()}`;
|
||||
const wv = new WebviewWindow(label, {
|
||||
title: `${session.name} — Wraith`,
|
||||
width: 900,
|
||||
height: 600,
|
||||
resizable: true,
|
||||
center: true,
|
||||
url: `index.html#/detached-session?sessionId=${session.id}&name=${encodeURIComponent(session.name)}&protocol=${session.protocol}`,
|
||||
width: 900, height: 600,
|
||||
});
|
||||
} catch (err) { console.error("Detach window error:", err); }
|
||||
wv.once("tauri://error", (e) => { console.error("Detach window error:", e); });
|
||||
}
|
||||
|
||||
function closeMenuTab(): void {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="stats"
|
||||
class="flex items-center gap-4 px-6 h-[48px] bg-[var(--wraith-bg-tertiary)] border-t border-[var(--wraith-border)] text-base font-mono shrink-0 select-none"
|
||||
class="flex items-center gap-4 px-4 h-6 bg-[var(--wraith-bg-tertiary)] border-t border-[var(--wraith-border)] text-[10px] font-mono shrink-0 select-none"
|
||||
>
|
||||
<!-- CPU -->
|
||||
<span class="flex items-center gap-1">
|
||||
|
||||
@ -367,14 +367,17 @@ function closeHelpMenuDeferred(): void {
|
||||
|
||||
async function handleHelpAction(page: string): Promise<void> {
|
||||
showHelpMenu.value = false;
|
||||
try {
|
||||
await invoke("open_child_window", {
|
||||
label: `help-${page}-${Date.now()}`,
|
||||
title: "Wraith — Help",
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const label = `help-${page}-${Date.now()}`;
|
||||
const wv = new WebviewWindow(label, {
|
||||
title: `Wraith — Help`,
|
||||
width: 750,
|
||||
height: 600,
|
||||
resizable: true,
|
||||
center: true,
|
||||
url: `index.html#/tool/help?page=${page}`,
|
||||
width: 750, height: 600,
|
||||
});
|
||||
} catch (err) { console.error("Help window error:", err); alert("Window error: " + String(err)); }
|
||||
wv.once("tauri://error", (e) => { console.error("Help window error:", e); alert("Window error: " + JSON.stringify(e.payload)); });
|
||||
}
|
||||
|
||||
async function handleToolAction(tool: string): Promise<void> {
|
||||
@ -388,6 +391,8 @@ async function handleToolAction(tool: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
|
||||
const toolConfig: Record<string, { title: string; width: number; height: number }> = {
|
||||
"network-scanner": { title: "Network Scanner", width: 800, height: 600 },
|
||||
"port-scanner": { title: "Port Scanner", width: 700, height: 500 },
|
||||
@ -408,14 +413,17 @@ async function handleToolAction(tool: string): Promise<void> {
|
||||
|
||||
const sessionId = activeSessionId.value || "";
|
||||
|
||||
try {
|
||||
await invoke("open_child_window", {
|
||||
label: `tool-${tool}-${Date.now()}`,
|
||||
// Open tool in a new Tauri window
|
||||
const label = `tool-${tool}-${Date.now()}`;
|
||||
const wv = new WebviewWindow(label, {
|
||||
title: `Wraith — ${config.title}`,
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
resizable: true,
|
||||
center: true,
|
||||
url: `index.html#/tool/${tool}?sessionId=${sessionId}`,
|
||||
width: config.width, height: config.height,
|
||||
});
|
||||
} catch (err) { console.error("Tool window error:", err); alert("Tool window error: " + String(err)); }
|
||||
wv.once("tauri://error", (e) => { console.error("Tool window error:", e); alert("Window error: " + JSON.stringify(e.payload)); });
|
||||
}
|
||||
|
||||
async function handleFileMenuAction(action: string): Promise<void> {
|
||||
@ -435,14 +443,20 @@ function handleThemeSelect(theme: ThemeDefinition): void {
|
||||
async function handleOpenFile(entry: FileEntry): Promise<void> {
|
||||
if (!activeSessionId.value) return;
|
||||
try {
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const fileName = entry.path.split("/").pop() || entry.path;
|
||||
const label = `editor-${Date.now()}`;
|
||||
const sessionId = activeSessionId.value;
|
||||
await invoke("open_child_window", {
|
||||
label: `editor-${Date.now()}`,
|
||||
|
||||
const wv = new WebviewWindow(label, {
|
||||
title: `${fileName} — Wraith Editor`,
|
||||
width: 800,
|
||||
height: 600,
|
||||
resizable: true,
|
||||
center: true,
|
||||
url: `index.html#/tool/editor?sessionId=${sessionId}&path=${encodeURIComponent(entry.path)}`,
|
||||
width: 800, height: 600,
|
||||
});
|
||||
wv.once("tauri://error", (e) => { console.error("Editor window error:", e); alert("Window error: " + JSON.stringify(e.payload)); });
|
||||
} catch (err) { console.error("Failed to open editor:", err); }
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user