This commit is contained in:
Julian Freeman
2026-01-19 13:30:40 -04:00
parent 84ae92d1e3
commit d25f87abe0
2 changed files with 138 additions and 63 deletions

View File

@@ -181,21 +181,36 @@ const applyAll = () => {
</h2> </h2>
<!-- Auto Detect Controls --> <!-- Auto Detect Controls -->
<div class="flex gap-2"> <div class="flex flex-col gap-2">
<button <label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">自动检测水印</label>
@click="store.detectAllWatermarks()" <div class="flex gap-2">
class="flex-1 bg-blue-600 hover:bg-blue-500 text-white py-2 rounded flex items-center justify-center gap-2 text-sm transition-colors" <button
> @click="store.detectCurrentWatermark()"
<Sparkles class="w-4 h-4" /> 自动检测 class="flex-1 bg-gray-700 hover:bg-gray-600 text-white py-2 rounded flex items-center justify-center gap-2 text-sm transition-colors"
</button> :disabled="store.isDetecting || store.selectedIndex < 0"
<button >
@click="store.selectedIndex >= 0 && store.clearMask(store.selectedIndex)" <div v-if="store.isDetecting" class="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
class="bg-gray-700 hover:bg-gray-600 text-gray-300 px-3 rounded flex items-center justify-center transition-colors" <Sparkles v-else class="w-4 h-4" />
title="清空遮罩" 当前
:disabled="store.selectedIndex < 0" </button>
> <button
<Trash2 class="w-4 h-4" /> @click="store.detectAllWatermarks()"
</button> class="flex-1 bg-gray-700 hover:bg-gray-600 text-white py-2 rounded flex items-center justify-center gap-2 text-sm transition-colors"
:disabled="store.isDetecting || store.images.length === 0"
>
<div v-if="store.isDetecting" class="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
<Sparkles v-else class="w-4 h-4" />
全部
</button>
<button
@click="store.selectedIndex >= 0 && store.clearMask(store.selectedIndex)"
class="bg-gray-700 hover:bg-gray-600 text-gray-300 px-3 rounded flex items-center justify-center transition-colors"
title="清空当前遮罩"
:disabled="store.selectedIndex < 0"
>
<Trash2 class="w-4 h-4" />
</button>
</div>
</div> </div>
<p class="text-xs text-gray-400">涂抹想要移除的水印AI 将自动填充背景</p> <p class="text-xs text-gray-400">涂抹想要移除的水印AI 将自动填充背景</p>
@@ -215,18 +230,32 @@ const applyAll = () => {
/> />
</div> </div>
<div class="p-3 bg-red-900/20 border border-red-900/50 rounded text-xs text-red-200"> <div class="p-3 bg-red-900/20 border border-red-900/50 rounded text-xs text-red-200 flex flex-col gap-1">
AI 修复功能已就绪 <span>AI 修复功能已就绪</span>
<span v-if="store.isProcessing" class="text-yellow-300 animate-pulse">正在处理中请稍候...</span>
</div> </div>
<button <!-- Execution Controls -->
@click="store.selectedIndex >= 0 && store.processInpainting(store.selectedIndex)" <div class="flex flex-col gap-2">
class="w-full bg-red-600 hover:bg-red-500 text-white py-3 rounded font-bold shadow-lg transition-colors flex items-center justify-center gap-2" <button
:disabled="store.selectedIndex < 0" @click="store.processInpainting(store.selectedIndex)"
> class="w-full bg-red-600 hover:bg-red-500 text-white py-3 rounded font-bold shadow-lg transition-colors flex items-center justify-center gap-2"
<Eraser class="w-5 h-5" /> :disabled="store.isProcessing || store.selectedIndex < 0"
执行移除 >
</button> <div v-if="store.isProcessing" class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
<Eraser v-else class="w-5 h-5" />
执行移除 (当前)
</button>
<button
@click="store.processAllInpainting()"
class="w-full bg-red-800 hover:bg-red-700 text-white py-2 rounded text-sm font-medium shadow transition-colors flex items-center justify-center gap-2"
:disabled="store.isProcessing || store.images.length === 0"
>
<Eraser class="w-4 h-4" />
执行移除 (全部有遮罩)
</button>
</div>
<button <button
v-if="store.selectedImage && store.selectedImage.path !== store.selectedImage.originalPath" v-if="store.selectedImage && store.selectedImage.path !== store.selectedImage.originalPath"

View File

@@ -52,6 +52,9 @@ export const useGalleryStore = defineStore("gallery", () => {
opacity: 0.5 // visual only opacity: 0.5 // visual only
}); });
const isDetecting = ref(false);
const isProcessing = ref(false);
const selectedImage = computed(() => { const selectedImage = computed(() => {
if (selectedIndex.value >= 0 && selectedIndex.value < images.value.length) { if (selectedIndex.value >= 0 && selectedIndex.value < images.value.length) {
return images.value[selectedIndex.value]; return images.value[selectedIndex.value];
@@ -108,31 +111,50 @@ export const useGalleryStore = defineStore("gallery", () => {
} }
} }
// Helper to run detection on a single image object
async function detectWatermarkForImage(img: ImageItem) {
try {
// Clear existing mask first
img.maskStrokes = [];
// @ts-ignore
const result = await invoke<{rects: {x: number, y: number, width: number, height: number}[]}>("detect_watermark", { path: img.path });
if (result.rects && result.rects.length > 0) {
result.rects.forEach(r => {
img.maskStrokes!.push({
type: 'rect',
rect: { x: r.x, y: r.y, w: r.width, h: r.height }
});
});
}
} catch (e) {
console.error(`Detection failed for ${img.name}`, e);
}
}
async function detectCurrentWatermark() {
if (selectedIndex.value < 0 || !images.value[selectedIndex.value]) return;
isDetecting.value = true;
try {
await detectWatermarkForImage(images.value[selectedIndex.value]);
} finally {
isDetecting.value = false;
}
}
async function detectAllWatermarks() { async function detectAllWatermarks() {
if (images.value.length === 0) return; if (images.value.length === 0) return;
isDetecting.value = true;
const batchSize = 5; try {
for (let i = 0; i < images.value.length; i += batchSize) { const batchSize = 5;
const batch = images.value.slice(i, i + batchSize).map(async (img) => { for (let i = 0; i < images.value.length; i += batchSize) {
try { const batch = images.value.slice(i, i + batchSize).map(img => detectWatermarkForImage(img));
// @ts-ignore await Promise.all(batch);
const result = await invoke<{rects: {x: number, y: number, width: number, height: number}[]}>("detect_watermark", { path: img.path }); }
} finally {
if (result.rects && result.rects.length > 0) { isDetecting.value = false;
if (!img.maskStrokes) img.maskStrokes = [];
result.rects.forEach(r => {
img.maskStrokes!.push({
type: 'rect',
rect: { x: r.x, y: r.y, w: r.width, h: r.height }
});
});
}
} catch (e) {
console.error(`Detection failed for ${img.name}`, e);
}
});
await Promise.all(batch);
} }
} }
@@ -153,9 +175,6 @@ export const useGalleryStore = defineStore("gallery", () => {
baseScale: baseScale baseScale: baseScale
}); });
// Find index again just in case sort changed? No, we rely on reference or index.
// Img is reference. But setter needs index.
// Let's use image reference finding.
const idx = images.value.indexOf(img); const idx = images.value.indexOf(img);
if (idx >= 0) { if (idx >= 0) {
setImageManualPosition(idx, result.x, result.y); setImageManualPosition(idx, result.x, result.y);
@@ -169,9 +188,9 @@ export const useGalleryStore = defineStore("gallery", () => {
} }
} }
async function processInpainting(index: number) { // Internal helper for single image inpainting logic
const img = images.value[index]; async function runInpaintingForImage(img: ImageItem) {
if (!img || !img.maskStrokes || img.maskStrokes.length === 0) return; if (!img.maskStrokes || img.maskStrokes.length === 0) return;
try { try {
const newPath = await invoke<string>("run_inpainting", { const newPath = await invoke<string>("run_inpainting", {
@@ -179,20 +198,43 @@ export const useGalleryStore = defineStore("gallery", () => {
strokes: img.maskStrokes strokes: img.maskStrokes
}); });
// Update the image path to point to the processed version
// NOTE: This updates the "Source" of the image in the store.
// In a real app, maybe we keep original? But for "Wizard", modifying is the goal.
img.path = newPath; img.path = newPath;
// Clear mask after success
img.maskStrokes = []; img.maskStrokes = [];
// Force UI refresh (thumbnail) - might be needed
// Generate new thumb?
// Since path changed, converting src should trigger reload.
} catch (e) { } catch (e) {
console.error("Inpainting failed", e); console.error("Inpainting failed", e);
alert("Inpainting failed: " + e); throw e; // Propagate error
}
}
async function processInpainting(index: number) {
const img = images.value[index];
if (!img) return;
isProcessing.value = true;
try {
await runInpaintingForImage(img);
} catch (e) {
alert("处理失败: " + e);
} finally {
isProcessing.value = false;
}
}
async function processAllInpainting() {
const candidates = images.value.filter(img => img.maskStrokes && img.maskStrokes.length > 0);
if (candidates.length === 0) return;
isProcessing.value = true;
try {
// Sequential processing to avoid freezing UI or overloading backend
for (const img of candidates) {
await runInpaintingForImage(img);
}
alert("批量处理完成!");
} catch (e) {
alert("批量处理部分失败: " + e);
} finally {
isProcessing.value = false;
} }
} }
@@ -241,6 +283,8 @@ export const useGalleryStore = defineStore("gallery", () => {
editMode, editMode,
watermarkSettings, watermarkSettings,
brushSettings, brushSettings,
isDetecting,
isProcessing,
setImages, setImages,
selectImage, selectImage,
updateWatermarkSettings, updateWatermarkSettings,
@@ -249,9 +293,11 @@ export const useGalleryStore = defineStore("gallery", () => {
applySettingsToAll, applySettingsToAll,
addMaskStroke, addMaskStroke,
clearMask, clearMask,
detectCurrentWatermark,
detectAllWatermarks, detectAllWatermarks,
recalcAllWatermarks, recalcAllWatermarks,
processInpainting, processInpainting,
processAllInpainting,
restoreImage restoreImage
}; };
}); });