phase 1 & 2 first
This commit is contained in:
194
src/App.vue
194
src/App.vue
@@ -1,160 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import HeroView from "./components/HeroView.vue";
|
||||
import ThumbnailStrip from "./components/ThumbnailStrip.vue";
|
||||
import { useGalleryStore } from "./stores/gallery";
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
const greetMsg = ref("");
|
||||
const name = ref("");
|
||||
const store = useGalleryStore();
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
greetMsg.value = await invoke("greet", { name: name.value });
|
||||
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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="container">
|
||||
<h1>Welcome to Tauri + Vue</h1>
|
||||
|
||||
<div class="row">
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo vite" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://tauri.app" target="_blank">
|
||||
<img src="/tauri.svg" class="logo tauri" alt="Tauri logo" />
|
||||
</a>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
||||
</a>
|
||||
</div>
|
||||
<p>Click on the Tauri, Vite, and Vue logos to learn more.</p>
|
||||
|
||||
<form class="row" @submit.prevent="greet">
|
||||
<input id="greet-input" v-model="name" placeholder="Enter a name..." />
|
||||
<button type="submit">Greet</button>
|
||||
</form>
|
||||
<p>{{ greetMsg }}</p>
|
||||
</main>
|
||||
<div class="h-screen w-screen bg-gray-900 text-white overflow-hidden flex flex-col">
|
||||
<header class="h-12 bg-gray-800 flex items-center justify-between px-4 border-b border-gray-700 shrink-0">
|
||||
<h1 class="text-sm font-bold tracking-wider">WATERMARK WIZARD</h1>
|
||||
<button
|
||||
@click="openFolder"
|
||||
class="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded text-sm transition-colors"
|
||||
>
|
||||
Open Folder
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 relative bg-black overflow-hidden">
|
||||
<HeroView />
|
||||
</main>
|
||||
|
||||
<footer class="h-32 bg-gray-800 border-t border-gray-700 shrink-0">
|
||||
<ThumbnailStrip />
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #249b73);
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
39
src/components/HeroView.vue
Normal file
39
src/components/HeroView.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { useGalleryStore } from "../stores/gallery";
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
|
||||
const store = useGalleryStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex items-center justify-center bg-black relative">
|
||||
<div v-if="store.selectedImage" class="relative max-w-full max-h-full">
|
||||
<img
|
||||
:src="convertFileSrc(store.selectedImage.path)"
|
||||
class="max-w-full max-h-full object-contain"
|
||||
alt="Hero Image"
|
||||
/>
|
||||
<!-- Watermark Overlay Placeholder -->
|
||||
<div
|
||||
v-if="store.selectedImage.zcaSuggestion"
|
||||
class="absolute border-2 border-dashed border-green-400 text-green-400 px-4 py-2 bg-black/50 pointer-events-none transition-all duration-500"
|
||||
:style="{
|
||||
left: (store.selectedImage.zcaSuggestion.x * 100) + '%',
|
||||
top: (store.selectedImage.zcaSuggestion.y * 100) + '%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}"
|
||||
>
|
||||
Smart Watermark ({{ store.selectedImage.zcaSuggestion.zone }})
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 border-2 border-dashed border-white text-white px-4 py-2 bg-black/50 pointer-events-none"
|
||||
>
|
||||
Calculating...
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-500">
|
||||
No image selected
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
38
src/components/ThumbnailStrip.vue
Normal file
38
src/components/ThumbnailStrip.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { useGalleryStore } from "../stores/gallery";
|
||||
// @ts-ignore
|
||||
import { RecycleScroller } from 'vue-virtual-scroller';
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
|
||||
const store = useGalleryStore();
|
||||
|
||||
const onSelect = (index: number) => {
|
||||
store.selectImage(index);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full bg-gray-900">
|
||||
<RecycleScroller
|
||||
class="h-full"
|
||||
:items="store.images"
|
||||
:item-size="100"
|
||||
key-field="path"
|
||||
direction="horizontal"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<div
|
||||
class="h-full w-[100px] p-2 cursor-pointer transition-colors"
|
||||
:class="{'bg-blue-600': store.selectedIndex === index, 'hover:bg-gray-700': store.selectedIndex !== index}"
|
||||
@click="onSelect(index)"
|
||||
>
|
||||
<div class="h-full w-full bg-gray-800 flex items-center justify-center overflow-hidden rounded border border-gray-600">
|
||||
<!-- Use actual thumbnail path later -->
|
||||
<img :src="convertFileSrc(item.path)" class="w-full h-full object-cover" loading="lazy" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,4 +1,8 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
const app = createApp(App);
|
||||
app.use(createPinia());
|
||||
app.mount("#app");
|
||||
54
src/stores/gallery.ts
Normal file
54
src/stores/gallery.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface ImageItem {
|
||||
path: string;
|
||||
thumbnail?: string;
|
||||
name: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
zcaSuggestion?: { x: number; y: number; zone: string };
|
||||
}
|
||||
|
||||
export const useGalleryStore = defineStore("gallery", () => {
|
||||
const images = ref<ImageItem[]>([]);
|
||||
const selectedIndex = ref<number>(-1);
|
||||
|
||||
const selectedImage = computed(() => {
|
||||
if (selectedIndex.value >= 0 && selectedIndex.value < images.value.length) {
|
||||
return images.value[selectedIndex.value];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
function setImages(newImages: ImageItem[]) {
|
||||
images.value = newImages;
|
||||
selectedIndex.value = -1;
|
||||
}
|
||||
|
||||
async function selectImage(index: number) {
|
||||
if (index < 0 || index >= images.value.length) return;
|
||||
selectedIndex.value = index;
|
||||
|
||||
const img = images.value[index];
|
||||
if (!img.zcaSuggestion) {
|
||||
try {
|
||||
const suggestion = await invoke<{x: number, y: number, zone: string}>("get_zca_suggestion", { path: img.path });
|
||||
// Update the item in the array
|
||||
// Note: Directly modifying the object inside ref array is reactive in Vue 3
|
||||
img.zcaSuggestion = suggestion;
|
||||
} catch (e) {
|
||||
console.error("ZCA failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
images,
|
||||
selectedIndex,
|
||||
selectedImage,
|
||||
setImages,
|
||||
selectImage,
|
||||
};
|
||||
});
|
||||
1
src/style.css
Normal file
1
src/style.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
Reference in New Issue
Block a user