153 lines
5.4 KiB
Vue
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>
|