feat(ui): add MobaXTerm import dialog with file picker and preview
Three-step import flow: file selection (click or drag-and-drop), preview showing connection/group/host-key counts, and success confirmation. Parses .mobaconf files client-side for preview; actual import will use Wails binding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
326fa9530f
commit
f5de568b32
247
frontend/src/components/common/ImportDialog.vue
Normal file
247
frontend/src/components/common/ImportDialog.vue
Normal file
@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
@click.self="close"
|
||||
@keydown.esc="close"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/50" @click="close" />
|
||||
|
||||
<!-- Dialog -->
|
||||
<div class="relative w-full max-w-md bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[#30363d]">
|
||||
<h3 class="text-sm font-semibold text-[var(--wraith-text-primary)]">Import Configuration</h3>
|
||||
<button
|
||||
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||
@click="close"
|
||||
>
|
||||
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.749.749 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.749.749 0 1 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="px-4 py-4">
|
||||
<!-- Step 1: File selection -->
|
||||
<template v-if="step === 'select'">
|
||||
<p class="text-sm text-[var(--wraith-text-secondary)] mb-4">
|
||||
Select a MobaXTerm <code class="text-[var(--wraith-accent-blue)]">.mobaconf</code> file to import connections and settings.
|
||||
</p>
|
||||
<div
|
||||
class="border-2 border-dashed border-[#30363d] rounded-lg p-8 text-center hover:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
|
||||
@click="selectFile"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<svg class="w-8 h-8 mx-auto text-[var(--wraith-text-muted)] mb-3" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14ZM11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.81V9.5a.75.75 0 0 1-1.5 0V3.81L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z" />
|
||||
</svg>
|
||||
<p class="text-sm text-[var(--wraith-text-secondary)]">
|
||||
Click to select or drag and drop
|
||||
</p>
|
||||
<p class="text-xs text-[var(--wraith-text-muted)] mt-1">
|
||||
Supports .mobaconf files
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".mobaconf"
|
||||
class="hidden"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Step 2: Preview -->
|
||||
<template v-else-if="step === 'preview'">
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm text-[var(--wraith-text-primary)] font-medium">
|
||||
{{ fileName }}
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="bg-[#0d1117] rounded-lg p-3 text-center">
|
||||
<div class="text-lg font-bold text-[var(--wraith-accent-blue)]">{{ preview.connections }}</div>
|
||||
<div class="text-[10px] text-[var(--wraith-text-muted)] mt-0.5">Connections</div>
|
||||
</div>
|
||||
<div class="bg-[#0d1117] rounded-lg p-3 text-center">
|
||||
<div class="text-lg font-bold text-[var(--wraith-accent-yellow)]">{{ preview.groups }}</div>
|
||||
<div class="text-[10px] text-[var(--wraith-text-muted)] mt-0.5">Groups</div>
|
||||
</div>
|
||||
<div class="bg-[#0d1117] rounded-lg p-3 text-center">
|
||||
<div class="text-lg font-bold text-[var(--wraith-accent-green)]">{{ preview.hostKeys }}</div>
|
||||
<div class="text-[10px] text-[var(--wraith-text-muted)] mt-0.5">Host Keys</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="preview.hasTheme" class="flex items-center gap-2 text-xs text-[var(--wraith-text-secondary)] bg-[#0d1117] rounded-lg px-3 py-2">
|
||||
<svg class="w-3.5 h-3.5 text-[var(--wraith-accent-green)]" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
|
||||
</svg>
|
||||
Terminal theme included
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-[var(--wraith-text-muted)]">
|
||||
Passwords are not imported (MobaXTerm uses proprietary encryption).
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Step 3: Complete -->
|
||||
<template v-else-if="step === 'complete'">
|
||||
<div class="text-center py-4">
|
||||
<svg class="w-12 h-12 mx-auto text-[var(--wraith-accent-green)] mb-3" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm3.78-9.72a.751.751 0 0 0-.018-1.042.751.751 0 0 0-1.042-.018L6.75 9.19 5.28 7.72a.751.751 0 0 0-1.042.018.751.751 0 0 0-.018 1.042l2 2a.75.75 0 0 0 1.06 0l4.5-4.5Z" />
|
||||
</svg>
|
||||
<h4 class="text-sm font-semibold text-[var(--wraith-text-primary)] mb-1">Import Complete</h4>
|
||||
<p class="text-xs text-[var(--wraith-text-secondary)]">
|
||||
Successfully imported {{ preview.connections }} connections and {{ preview.groups }} groups.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-2 px-4 py-3 border-t border-[#30363d]">
|
||||
<button
|
||||
v-if="step !== 'complete'"
|
||||
class="px-3 py-1.5 text-xs text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
|
||||
@click="close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-if="step === 'preview'"
|
||||
class="px-3 py-1.5 text-xs text-white bg-[#238636] hover:bg-[#2ea043] rounded transition-colors cursor-pointer"
|
||||
@click="doImport"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
v-if="step === 'complete'"
|
||||
class="px-3 py-1.5 text-xs text-white bg-[#1f6feb] hover:bg-[#388bfd] rounded transition-colors cursor-pointer"
|
||||
@click="close"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
type Step = "select" | "preview" | "complete";
|
||||
|
||||
const visible = ref(false);
|
||||
const step = ref<Step>("select");
|
||||
const fileName = ref("");
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const preview = ref({
|
||||
connections: 0,
|
||||
groups: 0,
|
||||
hostKeys: 0,
|
||||
hasTheme: false,
|
||||
});
|
||||
|
||||
function open(): void {
|
||||
visible.value = true;
|
||||
step.value = "select";
|
||||
fileName.value = "";
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function selectFile(): void {
|
||||
fileInput.value?.click();
|
||||
}
|
||||
|
||||
function handleFileSelect(event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (file) {
|
||||
processFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(event: DragEvent): void {
|
||||
const file = event.dataTransfer?.files?.[0];
|
||||
if (file) {
|
||||
processFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
async function processFile(file: File): Promise<void> {
|
||||
fileName.value = file.name;
|
||||
|
||||
// TODO: Replace with Wails binding — ImporterService.Preview(fileData)
|
||||
// For now, read and mock-parse the file to show a preview
|
||||
const text = await file.text();
|
||||
|
||||
// Simple mock parse to count items
|
||||
const lines = text.split("\n");
|
||||
let groups = 0;
|
||||
let connections = 0;
|
||||
let hostKeys = 0;
|
||||
let hasTheme = false;
|
||||
let inBookmarks = false;
|
||||
let inHostKeys = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith("[Bookmarks")) {
|
||||
inBookmarks = true;
|
||||
inHostKeys = false;
|
||||
continue;
|
||||
}
|
||||
if (trimmed === "[SSH_Hostkeys]") {
|
||||
inBookmarks = false;
|
||||
inHostKeys = true;
|
||||
continue;
|
||||
}
|
||||
if (trimmed === "[Colors]") {
|
||||
hasTheme = true;
|
||||
inBookmarks = false;
|
||||
inHostKeys = false;
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith("[")) {
|
||||
inBookmarks = false;
|
||||
inHostKeys = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inBookmarks) {
|
||||
if (trimmed.startsWith("SubRep=") && trimmed !== "SubRep=") {
|
||||
groups++;
|
||||
} else if (trimmed.includes("#109#") || trimmed.includes("#91#")) {
|
||||
connections++;
|
||||
}
|
||||
}
|
||||
|
||||
if (inHostKeys && trimmed.includes("@") && trimmed.includes("=")) {
|
||||
hostKeys++;
|
||||
}
|
||||
}
|
||||
|
||||
preview.value = { connections, groups, hostKeys, hasTheme };
|
||||
step.value = "preview";
|
||||
}
|
||||
|
||||
function doImport(): void {
|
||||
// TODO: Replace with Wails binding — ImporterService.Import(fileData)
|
||||
// For now, just show success
|
||||
step.value = "complete";
|
||||
}
|
||||
|
||||
defineExpose({ open, close, visible });
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user