Files
watermark-wizard/src/App.vue
Julian Freeman 302f106ae6 css
2026-01-19 15:12:59 -04:00

153 lines
5.4 KiB
Vue

<script setup lang="ts">
import HeroView from "./components/HeroView.vue";
import ThumbnailStrip from "./components/ThumbnailStrip.vue";
import SettingsPanel from "./components/SettingsPanel.vue";
import { useGalleryStore } from "./stores/gallery";
import { open } from '@tauri-apps/plugin-dialog';
import { invoke } from '@tauri-apps/api/core';
import { FolderOpen, Download } from 'lucide-vue-next';
import { ref, onMounted, onUnmounted } from "vue";
const store = useGalleryStore();
const isExporting = ref(false);
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'ArrowRight') {
store.nextImage();
} else if (event.key === 'ArrowLeft') {
store.prevImage();
}
}
onMounted(() => {
window.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown);
});
async function openFolder() {
try {
const selected = await open({
directory: true,
multiple: false,
});
if (selected && typeof selected === 'string') {
const images = await invoke('scan_dir', { path: selected }) as any[];
store.setImages(images);
if (images.length > 0) {
store.selectImage(0);
}
}
} catch (e) {
console.error("Failed to open folder:", e);
}
}
async function exportBatch() {
if (store.images.length === 0) return;
// Only require text if in ADD mode
if (store.editMode === 'add' && !store.watermarkSettings.text) {
alert("请输入水印文字。");
return;
}
try {
const outputDir = await open({
directory: true,
multiple: false,
title: "选择输出目录"
});
if (outputDir && typeof outputDir === 'string') {
isExporting.value = true;
// Map images to include manual settings
const exportTasks = store.images.map(img => {
// Extract filename from originalPath to ensure export uses original name
// Handles both Windows (\) and Unix (/) separators
const originalName = img.originalPath.split(/[/\\]/).pop() || "image.png";
return {
path: img.path,
output_filename: originalName,
manual_position: img.manualPosition || null,
scale: img.scale || null,
opacity: img.opacity || null,
color: img.color || null
};
});
// Pass dummy globals for rust struct compatibility
// The backend struct fields are named _manual_override and _manual_position
const rustWatermarkSettings = {
...store.watermarkSettings,
_manual_override: false,
_manual_position: { x: 0.5, y: 0.5 }
};
await invoke('export_batch', {
images: exportTasks,
watermark: rustWatermarkSettings,
outputDir: outputDir,
mode: store.editMode
});
alert("批量导出完成!");
}
} catch (e) {
console.error("Export failed:", e);
alert("导出失败: " + e);
} finally {
isExporting.value = false;
}
}
</script>
<template>
<div class="h-screen w-screen bg-gray-900 text-white overflow-hidden flex flex-col">
<header class="h-14 bg-gray-800 flex items-center justify-between px-6 border-b border-gray-700 shrink-0 shadow-md z-10">
<div class="flex items-center gap-4">
<h1 class="text-lg font-bold tracking-wider bg-linear-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">水印精灵</h1>
</div>
<div class="flex items-center gap-3">
<button
@click="openFolder"
class="flex items-center gap-2 bg-gray-700 hover:bg-gray-600 text-gray-200 px-4 py-2 rounded-md text-sm font-medium transition-all hover:shadow-lg"
>
<FolderOpen class="w-4 h-4" />
打开文件夹
</button>
<button
@click="exportBatch"
:disabled="isExporting"
class="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:bg-blue-800 disabled:cursor-not-allowed text-white px-4 py-2 rounded-md text-sm font-medium transition-all hover:shadow-lg shadow-blue-900/20"
>
<Download class="w-4 h-4" v-if="!isExporting" />
<div v-else class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
{{ isExporting ? '导出中...' : '批量导出' }}
</button>
</div>
</header>
<div class="flex-1 flex overflow-hidden">
<main class="flex-1 relative bg-black flex flex-col min-w-0">
<div class="flex-1 relative overflow-hidden">
<HeroView />
</div>
<footer class="h-32 bg-gray-800 border-t border-gray-700 shrink-0 z-10">
<ThumbnailStrip />
</footer>
</main>
<aside class="w-80 shrink-0 h-full border-l border-gray-700">
<SettingsPanel />
</aside>
</div>
</div>
</template>