Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions src/islands/draw/Whiteboard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
import { useEffect, useRef, useState, type ComponentType } from 'react';

// Builds a fingerprint that distinguishes scenes even when getSceneVersion
// returns the same sum for two different element sets (e.g. two separate
// one-element drawings each at version 1). Combines version sum + element
// IDs + file IDs so any real scene change is reliably detected.
function computeSceneKey(
elements: readonly unknown[],
files: Record<string, unknown> | undefined,
getVersion: (els: readonly unknown[]) => number,
): string {
const ver = getVersion(elements);
const ids = (elements as { id?: string }[]).map(e => e.id ?? '').join('\0');
const fk = Object.keys(files ?? {}).sort().join('\0');
return `${ver}|${ids}|${fk}`;
}
import { Maximize2, Minimize2, Check, ChevronUp, ChevronDown } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { loadScene, saveScene, type WhiteboardScene } from '@/tools/draw/whiteboard.store';
Expand Down Expand Up @@ -104,12 +119,12 @@ export default function Whiteboard({ lang = 'en' }: { lang?: Lang }) {
const dirty = useRef(false);
const ready = useRef(false);
const sceneVersionOf = useRef<(els: readonly unknown[]) => number>(() => 0);
const savedVersion = useRef<number | null>(null);
const savedKey = useRef<string | null>(null);

const flushSave = () => {
if (!latestScene.current || !dirty.current) return;
dirty.current = false;
savedVersion.current = sceneVersionOf.current(latestScene.current.elements);
savedKey.current = computeSceneKey(latestScene.current.elements, latestScene.current.files, sceneVersionOf.current);
setCountdown(0); // back to the "Saved" state; countdown stops
void saveScene(latestScene.current);
};
Expand All @@ -124,11 +139,11 @@ export default function Whiteboard({ lang = 'en' }: { lang?: Lang }) {
appState: { viewBackgroundColor: appState?.viewBackgroundColor },
files,
};
const version = sceneVersionOf.current(elements);
const key = computeSceneKey(elements, files, sceneVersionOf.current);
// Baseline on the first change (restored scene) and during warm-up.
if (savedVersion.current === null || !ready.current) { savedVersion.current = version; return; }
// Only a genuine element change starts the countdown.
if (version !== savedVersion.current && !dirty.current) {
if (savedKey.current === null || !ready.current) { savedKey.current = key; return; }
// Only a genuine element or file change starts the countdown.
if (key !== savedKey.current && !dirty.current) {
dirty.current = true;
setCountdown(SAVE_INTERVAL);
}
Expand Down Expand Up @@ -172,6 +187,16 @@ export default function Whiteboard({ lang = 'en' }: { lang?: Lang }) {
// getSceneVersion changes only on real element edits, not on cursor /
// selection / hover onChange noise — so we can tell "actually changed".
sceneVersionOf.current = m.getSceneVersion as (els: readonly unknown[]) => number;
// Rebase savedKey now that we have the real getSceneVersion — the
// default () => 0 would produce a stale key that mismatches on the
// next onChange even when nothing actually changed.
if (latestScene.current) {
savedKey.current = computeSceneKey(
latestScene.current.elements,
latestScene.current.files,
sceneVersionOf.current,
);
}
setExcalidraw(() => m.Excalidraw);
})
.catch(() => { if (alive) setFailed(true); });
Expand Down
18 changes: 10 additions & 8 deletions src/registry/tool-seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const en: Record<string, ToolSeoContent> = {
],
},
'spreadsheet-viewer': {
title: 'Free Spreadsheet Viewer — Open XLSX, ODS & CSV',
title: 'Free Spreadsheet/CSV Viewer — Open XLSX, ODS & CSV',
description: 'A free spreadsheet viewer to open Excel (.xlsx/.xls), OpenDocument (.ods) and CSV files in your browser — every sheet as a table. 100% private; nothing is uploaded.',
intro: 'This free spreadsheet viewer opens Excel, OpenDocument and CSV files right in your browser and shows every sheet as a clean, scrollable table — no Excel, Google Sheets or account needed. The file is read on your device and never uploaded.',
howTo: [
Expand Down Expand Up @@ -827,9 +827,9 @@ const en: Record<string, ToolSeoContent> = {
],
},
'image-watermark': {
title: 'Free Image Watermark Tool — Stamp Text on Photos',
description: 'A free online image watermark tool to stamp custom text across any photo — 100% private. Your image is processed in your browser and never uploaded.',
intro: 'This free image watermark tool lets you drop or paste an image and stamp your own text across it — diagonal, tiled, or in the corner — with control over size, opacity and color. Everything runs on your device, so your images never leave your browser.',
title: 'Free Image Watermark Tool — Stamp Text on Photos & KTP',
description: 'A free online image watermark tool to stamp custom text on any photo or ID card (KTP) — 100% private. Your image is processed in your browser and never uploaded.',
intro: 'This free image watermark tool lets you drop or paste an image and stamp your own text across it — diagonal, tiled, or in the corner — with control over size, opacity and color. Works great for watermarking KTP, SIM, passport scans, and any photo. Everything runs on your device, so your images never leave your browser.',
howTo: [
'Drop or paste an image, or click to browse for one.',
'Type your watermark text and choose a layout: Diagonal, Tiled or Corner.',
Expand All @@ -841,6 +841,7 @@ const en: Record<string, ToolSeoContent> = {
{ q: 'What watermark layouts can I choose?', a: 'Three: Diagonal across the image, Tiled repeating over the whole picture, or a single Corner stamp in the bottom-right.' },
{ q: 'Can I control how the watermark looks?', a: 'Yes. You can set the text, pick any color, and use sliders to adjust its scale and opacity so it\'s as subtle or bold as you want.' },
{ q: 'What format is the watermarked image saved in?', a: 'The download keeps your image\'s original format where possible, so a JPEG stays a JPEG and a PNG stays a PNG.' },
{ q: 'Can I use this to watermark a KTP or ID card photo?', a: 'Yes. Drop in a scan or photo of your KTP, SIM, passport or any identity document, type your watermark text (e.g. "COPY" or your name), pick Diagonal or Tiled layout, set the opacity, and download. Everything stays on your device — nothing is uploaded.' },
],
},
'image-stamp': {
Expand Down Expand Up @@ -1633,7 +1634,7 @@ const id: Record<string, ToolSeoContent> = {
],
},
'spreadsheet-viewer': {
title: 'Penampil Spreadsheet Gratis — Buka XLSX, ODS & CSV',
title: 'Penampil Spreadsheet/CSV Gratis — Buka XLSX, ODS & CSV',
description: 'Penampil spreadsheet gratis untuk membuka berkas Excel (.xlsx/.xls), OpenDocument (.ods), dan CSV di browser — setiap lembar sebagai tabel. 100% privat; tidak ada yang diunggah.',
intro: 'Penampil spreadsheet gratis ini membuka berkas Excel, OpenDocument, dan CSV langsung di browser Anda serta menampilkan setiap lembar sebagai tabel yang rapi dan dapat digulir — tanpa Excel, Google Sheets, atau akun. Berkas dibaca di perangkat Anda dan tidak pernah diunggah.',
howTo: [
Expand Down Expand Up @@ -2333,9 +2334,9 @@ const id: Record<string, ToolSeoContent> = {
],
},
'image-watermark': {
title: 'Tool Watermark Gambar Gratis — Stempel Teks di Foto',
description: 'Tool watermark gambar daring gratis untuk menstempel teks khusus pada foto apa pun — 100% privat. Gambar Anda diproses di browser dan tidak pernah diunggah.',
intro: 'Tool watermark gambar gratis ini memungkinkan Anda menjatuhkan atau menempelkan gambar dan menstempel teks Anda sendiri di atasnya — diagonal, berulang, atau di sudut — dengan kontrol atas ukuran, opasitas, dan warna. Semuanya berjalan di perangkat Anda, jadi gambar Anda tidak pernah meninggalkan browser.',
title: 'Tool Watermark Gambar Gratis — Watermark KTP, Foto & Dokumen',
description: 'Tool watermark gambar daring gratis untuk menstempel teks khusus pada foto, KTP, SIM, atau dokumen apa pun — 100% privat. Gambar Anda diproses di browser dan tidak pernah diunggah.',
intro: 'Tool watermark gambar gratis ini memungkinkan Anda menjatuhkan atau menempelkan gambar dan menstempel teks Anda sendiri di atasnya — diagonal, berulang, atau di sudut — dengan kontrol atas ukuran, opasitas, dan warna. Cocok untuk watermark KTP, SIM, paspor, dan foto lainnya. Semuanya berjalan di perangkat Anda, jadi gambar Anda tidak pernah meninggalkan browser.',
howTo: [
'Jatuhkan atau tempelkan gambar, atau klik untuk menelusurinya.',
'Ketik teks watermark Anda dan pilih tata letak: Diagonal, Tiled, atau Corner.',
Expand All @@ -2347,6 +2348,7 @@ const id: Record<string, ToolSeoContent> = {
{ q: 'Tata letak watermark apa saja yang bisa saya pilih?', a: 'Tiga: Diagonal melintang di gambar, Tiled berulang di seluruh gambar, atau satu stempel Corner di kanan bawah.' },
{ q: 'Bisakah saya mengontrol tampilan watermark?', a: 'Ya. Anda dapat mengatur teks, memilih warna apa pun, dan menggunakan penggeser untuk menyesuaikan skala dan opasitasnya agar sesamar atau setebal yang Anda inginkan.' },
{ q: 'Dalam format apa gambar berwatermark disimpan?', a: 'Unduhan mempertahankan format asli gambar Anda jika memungkinkan, jadi JPEG tetap JPEG dan PNG tetap PNG.' },
{ q: 'Bisakah tool ini digunakan untuk watermark KTP?', a: 'Ya. Unggah scan atau foto KTP, SIM, paspor, atau dokumen identitas lainnya, ketik teks watermark (misalnya "COPY" atau nama Anda), pilih tata letak Diagonal atau Tiled, atur opasitasnya, lalu unduh hasilnya. Semua diproses di perangkat Anda — tidak ada yang diunggah.' },
],
},
'image-stamp': {
Expand Down
2 changes: 1 addition & 1 deletion src/registry/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export const tools: ToolDef[] = [
},
{
id: 'spreadsheet-viewer',
name: 'Spreadsheet Viewer',
name: 'Spreadsheet/CSV Viewer',
category: 'Documents',
route: '/tools/spreadsheet-viewer',
keywords: ['spreadsheet', 'excel', 'xlsx', 'xls', 'ods', 'csv', 'viewer', 'open', 'read', 'sheet', 'opendocument', 'calc'],
Expand Down
Loading