feat: Add image uploader and metadata viewer components

- Implemented ImageUploader.svelte for drag-and-drop image uploads with metadata extraction.
- Created MetadataViewer.svelte to display extracted metadata in a user-friendly format.
- Added PrivacyInfo.svelte to inform users about client-side processing and privacy risks.
- Introduced ThemeToggle.svelte for dark mode functionality.
- Set up main.js for application entry point and configured Svelte with vite.
- Added Tailwind CSS for styling and configured Vite for asset management.
- Established svelte.config.js and tailwind.config.js for project settings.
This commit is contained in:
sharif committed 2025-06-23 22:23:22 +06:00
1 parent cf0d872dba
commit cbce104341
18 files changed
+5263

No files matched your search

+333
View File
@@ -0,0 +1,333 @@
<script>
import { onMount } from "svelte";
import ImageUploader from "./lib/ImageUploader.svelte";
import MetadataViewer from "./lib/MetadataViewer.svelte";
import ImageProcessor from "./lib/ImageProcessor.svelte";
import ThemeToggle from "./lib/ThemeToggle.svelte";
import Header from "./lib/Header.svelte";
import Footer from "./lib/Footer.svelte";
import PrivacyInfo from "./lib/PrivacyInfo.svelte";
let selectedFile = null;
let metadata = null;
let processedImage = null;
let isProcessing = false;
let showMetadata = false;
let showFileInfo = false;
let showPrivacyInfo = false;
let darkMode = false;
onMount(() => {
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
darkMode = savedTheme === "dark";
} else {
darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
}
updateTheme();
});
function updateTheme() {
if (darkMode) {
document.documentElement.setAttribute("data-theme", "dark");
} else {
document.documentElement.removeAttribute("data-theme");
}
localStorage.setItem("theme", darkMode ? "dark" : "light");
}
function handleThemeToggle() {
darkMode = !darkMode;
updateTheme();
}
function handleFileSelected(event) {
selectedFile = event.detail.file;
metadata = event.detail.metadata;
processedImage = null;
showMetadata = false;
}
function handleProcessingStart() {
isProcessing = true;
}
function handleProcessingComplete(event) {
isProcessing = false;
processedImage = event.detail.processedImage;
}
function handleReset() {
selectedFile = null;
metadata = null;
processedImage = null;
isProcessing = false;
showMetadata = false;
}
function toggleMetadata() {
showMetadata = !showMetadata;
}
function toggleFileInfo() {
showFileInfo = !showFileInfo;
}
function togglePrivacyInfo() {
showPrivacyInfo = !showPrivacyInfo;
}
</script>
<ThemeToggle {darkMode} on:toggle={handleThemeToggle} />
<main class="container py-8">
<Header />
<div class="grid gap-6">
<section class="card">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Upload Your Image</h2>
</div>
<ImageUploader
on:fileSelected={handleFileSelected}
on:reset={handleReset}
disabled={isProcessing}
/>
</section>
<!-- Processing Section -->
{#if selectedFile}
<section class="grid grid-2 gap-6">
<div class="card">
<div class="flex justify-between items-center mb-3">
<h3 class="text-lg font-semibold">Original</h3>
{#if metadata}
<button class="btn btn-ghost text-sm" on:click={toggleMetadata}>
{showMetadata ? "Hide" : "Show"} Metadata
</button>
{/if}
</div>
<div class="text-center mb-3">
<img
src={URL.createObjectURL(selectedFile)}
alt="Original"
class="image-preview"
/>
</div>
<div class="text-xs text-secondary">
<p>
{selectedFile.name} • {(selectedFile.size / 1024).toFixed(1)} KB
</p>
{#if metadata && metadata.message && !metadata.hasMetadata}
<p class="text-success">✓ No metadata detected</p>
{:else if metadata}
<p class="text-warning">
⚠️ {Object.keys(metadata).filter(
(key) => !["fileType", "hasMetadata", "message"].includes(key)
).length} metadata entries found
</p>
{/if}
</div>
{#if showMetadata && metadata}
<div class="mt-4">
<MetadataViewer {metadata} />
</div>
{/if}
</div>
<div class="card">
<h3 class="text-lg font-semibold mb-3">Clean Metadata</h3>
<ImageProcessor
file={selectedFile}
on:processingStart={handleProcessingStart}
on:processingComplete={handleProcessingComplete}
disabled={isProcessing}
/>
{#if isProcessing}
<div class="mt-3">
<div class="progress-bar">
<div class="progress-fill" style="width: 100%;"></div>
</div>
<p class="text-xs text-secondary mt-1">Processing...</p>
</div>
{/if}
{#if processedImage}
<div class="mt-4 slide-up">
<h4 class="font-medium mb-2">✅ Clean Image</h4>
<div class="text-center mb-3">
<img
src={processedImage.url}
alt="Cleaned"
class="image-preview"
/>
</div>
<div class="text-xs text-secondary mb-3">
<p>
{(processedImage.size / 1024).toFixed(1)} KB •
{processedImage.compressionRatio
? `${processedImage.compressionRatio > 0 ? "-" : "+"}${Math.abs(processedImage.compressionRatio)}% size`
: "Processed"}
</p>
{#if processedImage.wasSecondPass}
<p class="status-warning">
⚠️ Used enhanced cleaning (second pass)
</p>
{/if}
<p class="status-success">✓ All EXIF metadata removed</p>
<p class="status-success">✓ AI/C2PA provenance data stripped</p>
<p class="status-success">✓ Location data cleared</p>
</div>
<a
href={processedImage.url}
download={processedImage.filename}
class="btn btn-success btn-lg w-full"
>
<svg
width="20"
height="20"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
Download Cleaned Image
</a>
</div>
{/if}
</div>
</section>
{/if}
<section class="card">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">🔐 Privacy & Security</h2>
<button
class="btn btn-ghost text-sm"
on:click={togglePrivacyInfo}
type="button"
>
{showPrivacyInfo ? "Hide" : "Show"} Details
</button>
</div>
<div class="mb-3">
<p class="text-sm text-secondary">
<span class="text-success font-semibold">✓</span> 100% client-side
processing
<span class="mx-2">•</span>
<span class="text-success font-semibold">✓</span> No data collection
<span class="mx-2">•</span>
<span class="text-success font-semibold">✓</span> Open source
</p>
</div>
{#if showPrivacyInfo}
<PrivacyInfo />
{/if}
</section>
</div>
</main>
<Footer />
<style>
.w-full {
width: 100%;
}
.status-warning {
color: var(--warning-color, #f59e0b);
}
.image-preview {
max-width: 100%;
max-height: 200px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.progress-bar {
width: 100%;
height: 4px;
background-color: var(--border-color);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: var(--primary-color);
animation: pulse 1s ease-in-out infinite;
}
.slide-up {
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.btn-ghost {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.btn-ghost:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.grid {
display: grid;
gap: 1.5rem;
}
.grid-2 {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 768px) {
.grid-2 {
grid-template-columns: 1fr;
}
.image-preview {
max-height: 150px;
}
}
.text-secondary {
color: var(--text-secondary);
}
</style>