added more tools, fixed locales

This commit is contained in:
Gamosoft 2026-04-21 17:39:22 +02:00
parent c83454ae2b
commit 7edbc28a9c
13 changed files with 214 additions and 32 deletions

View File

@ -3,6 +3,8 @@
// Configuration constants
const CONFIG = {
AUTOSAVE_DELAY: 1000, // ms - Debounce before note save (autoSave) and drawing PNG autosave (_drawingScheduleAutosave)
/** Must match drawingRedraw() fill and eraser stroke color (opaque “whiteboard”). */
DRAWING_BACKGROUND: '#ffffff',
SEARCH_DEBOUNCE_DELAY: 500, // ms - Delay before running note search while typing
SAVE_INDICATOR_DURATION: 2000, // ms - How long to show "saved" indicator
SCROLL_SYNC_DELAY: 50, // ms - Delay to prevent scroll sync interference
@ -504,6 +506,8 @@ function noteApp() {
drawingRedoStack: [],
drawingDraft: null,
drawingIsPointerDown: false,
/** True after the PNG from disk has been decoded into _drawingBaseImage; false after Clear. */
drawingHasRasterFromFile: false,
_drawingAutosaveTimeout: null,
// DOM element cache (to avoid repeated querySelector calls)
@ -2904,7 +2908,7 @@ function noteApp() {
const dpr = this._drawingDpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = '#ffffff';
ctx.fillStyle = CONFIG.DRAWING_BACKGROUND;
ctx.fillRect(0, 0, w, h);
if (this._drawingBaseImage && this._drawingBaseImage.complete) {
ctx.drawImage(this._drawingBaseImage, 0, 0, w, h);
@ -3016,6 +3020,7 @@ function noteApp() {
this.drawingDraft = null;
this.drawingIsPointerDown = false;
this._drawingBaseImage = null;
this.drawingHasRasterFromFile = false;
this._drawingLoadToken = Symbol();
const token = this._drawingLoadToken;
@ -3050,28 +3055,98 @@ function noteApp() {
});
if (token !== this._drawingLoadToken) return;
this._drawingBaseImage = img;
this.drawingHasRasterFromFile = true;
this._drawingLayoutCanvas();
} catch (e) {
if (token !== this._drawingLoadToken) return;
ErrorHandler.handle('load drawing', e);
this.drawingHasRasterFromFile = false;
this._drawingLayoutCanvas();
}
},
_drawingRgbToHex(r, g, b) {
const h = (n) => {
const s = n.toString(16);
return s.length === 1 ? `0${s}` : s;
};
return `#${h(r)}${h(g)}${h(b)}`;
},
/** Sample visible canvas color at logical (css) coordinates; sets drawingColor. */
drawingSampleColor(e) {
const canvas = this._drawingCanvasEl;
const ctx = this._drawingCtx;
if (!canvas || !ctx) return;
const { x, y } = this._drawingCanvasCoords(e);
this.drawingRedraw();
const dpr = this._drawingDpr || 1;
let ix = Math.floor(x * dpr);
let iy = Math.floor(y * dpr);
ix = Math.max(0, Math.min(ix, canvas.width - 1));
iy = Math.max(0, Math.min(iy, canvas.height - 1));
const pix = ctx.getImageData(ix, iy, 1, 1).data;
this.drawingColor = this._drawingRgbToHex(pix[0], pix[1], pix[2]);
},
/** True when there is a loaded bitmap and/or session strokes to clear away. */
drawingClearEnabled() {
if (this.currentMediaType !== 'drawing') return false;
return !!(this.drawingHasRasterFromFile || (this.drawingOps && this.drawingOps.length > 0));
},
/**
* Replace the in-memory drawing with a blank canvas and schedule save so the file on disk
* becomes a fresh white PNG (same dimensions as the viewer). Drops the loaded raster.
*/
async drawingClear() {
if (this.currentMediaType !== 'drawing' || !this.currentMedia) return;
if (!this._drawingBaseImage && this.drawingOps.length === 0) return;
const ok = await this.confirmModalAsk({
title: this.t('drawing.clear_title'),
message: this.t('drawing.clear_confirm'),
danger: true,
confirmLabel: this.t('drawing.clear'),
});
if (!ok) return;
this._drawingCancelAutosave();
if (this._drawingObjectURL) {
try {
URL.revokeObjectURL(this._drawingObjectURL);
} catch (_) {
/* ignore */
}
this._drawingObjectURL = null;
}
this._drawingBaseImage = null;
this.drawingHasRasterFromFile = false;
this.drawingDraft = null;
this.drawingIsPointerDown = false;
this.drawingOps = [];
this.drawingRedoStack = [];
this.drawingRedraw();
this._drawingScheduleAutosave();
},
drawingPointerDown(e) {
if (this.currentMediaType !== 'drawing' || e.button !== 0) return;
const canvas = this._drawingCanvasEl;
if (!canvas) return;
const tool = this.drawingTool;
if (tool === 'eyedropper') {
e.preventDefault();
this.drawingSampleColor(e);
return;
}
this._drawingCancelAutosave();
canvas.setPointerCapture(e.pointerId);
this._drawingPointerId = e.pointerId;
const { x, y } = this._drawingCanvasCoords(e);
this.drawingIsPointerDown = true;
this.drawingRedoStack = [];
const color = this.drawingColor;
const lw = this.drawingLineWidth;
const tool = this.drawingTool;
if (tool === 'freehand') {
const color = tool === 'eraser' ? CONFIG.DRAWING_BACKGROUND : this.drawingColor;
if (tool === 'freehand' || tool === 'eraser') {
this.drawingDraft = { type: 'stroke', color, lineWidth: lw, points: [[x, y]] };
} else if (tool === 'line') {
this.drawingDraft = { type: 'line', color, lineWidth: lw, x1: x, y1: y, x2: x, y2: y };
@ -3187,7 +3262,7 @@ function noteApp() {
},
/**
* Persist the flattened canvas to disk (Ctrl+S, toolbar, autosave). Same feedback as saveNote:
* Persist the flattened canvas to disk (Ctrl+S, autosave). Same feedback as saveNote:
* header "Saved" only never clears stroke undo/redo or reloads the image; stacks reset when
* opening another drawing via initDrawingViewer().
*/

View File

@ -2705,27 +2705,77 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<div class="w-px h-5 mx-1" style="background-color: var(--border-primary);"></div>
<label class="flex items-center gap-1 text-xs cursor-pointer" style="color: var(--text-secondary);">
<span x-text="t('drawing.color')"></span>
<input type="color" x-model="drawingColor" class="w-8 h-7 p-0 border rounded cursor-pointer" style="border-color: var(--border-primary);" />
</label>
<label class="flex items-center gap-1 text-xs" style="color: var(--text-secondary);">
<span x-text="t('drawing.width')"></span>
<input type="range" min="1" max="32" x-model.number="drawingLineWidth" class="w-24 md:w-32 align-middle" />
<span class="tabular-nums w-5" x-text="drawingLineWidth"></span>
</label>
<button
type="button"
class="p-2 rounded-lg ml-auto transition-colors"
style="background-color: var(--accent-primary); color: white;"
@click="drawingSave()"
:title="t('drawing.save_hint')"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'eraser' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'eraser'"
:title="t('drawing.tool_eraser')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21M22 21H7M5 11l9 9" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'eyedropper' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'eyedropper'"
:title="t('drawing.tool_eyedropper')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 11.25 1.425 1.425c.671.671.671 1.757 0 2.428l-1.06 1.06c-.671.671-1.757.671-2.428 0l-1.425-1.425m6-6 1.425-1.425c.671-.671 1.757-.671 2.428 0l1.06 1.06c.671.671.671 1.757 0 2.428L21 15.75m-6-6L8.25 3.75a2.121 2.121 0 0 0-3 3L12 15.75l3-3m-3-3-6 6" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:class="!drawingClearEnabled() ? 'opacity-40 cursor-not-allowed' : ''"
:style="!drawingClearEnabled()
? 'border-color: var(--border-primary); color: var(--text-secondary);'
: 'border-color: var(--border-primary); color: var(--text-secondary); background-color: var(--bg-tertiary);'"
:disabled="!drawingClearEnabled()"
@click="drawingClear()"
:title="t('drawing.clear_hint')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<label
class="flex h-9 items-center gap-2 rounded-lg border px-2 shrink-0 cursor-pointer"
style="border-color: var(--border-primary); background-color: var(--bg-tertiary);"
:title="t('drawing.color')"
>
<span class="text-xs select-none" style="color: var(--text-secondary);" x-text="t('drawing.color')"></span>
<input
type="color"
x-model="drawingColor"
class="h-7 w-8 shrink-0 cursor-pointer rounded border p-0"
style="border-color: var(--border-primary);"
:aria-label="t('drawing.color')"
/>
</label>
<div
class="flex h-9 min-w-0 shrink items-center gap-2 rounded-lg border px-2"
style="border-color: var(--border-primary); background-color: var(--bg-tertiary);"
:title="t('drawing.width')"
>
<span class="text-xs whitespace-nowrap select-none shrink-0" style="color: var(--text-secondary);" x-text="t('drawing.width')"></span>
<input
type="range"
min="1"
max="32"
x-model.number="drawingLineWidth"
class="w-24 md:w-36 align-middle min-w-0 flex-1"
style="accent-color: var(--accent-primary);"
:aria-label="t('drawing.width')"
:aria-valuemin="1"
:aria-valuemax="32"
:aria-valuenow="drawingLineWidth"
/>
<span class="text-xs tabular-nums shrink-0 w-5 text-right" style="color: var(--text-secondary);" x-text="drawingLineWidth"></span>
</div>
</div>
<!-- Editor/Preview/Graph -->
@ -3011,7 +3061,9 @@
<canvas
x-ref="drawingCanvas"
class="drawing-surface block w-full h-full"
style="touch-action: none; cursor: crosshair; background: #fff;"
:style="drawingTool === 'eyedropper'
? 'touch-action: none; cursor: copy; background: #fff;'
: 'touch-action: none; cursor: crosshair; background: #fff;'"
@pointerdown.prevent="drawingPointerDown($event)"
@pointermove.prevent="drawingPointerMove($event)"
@pointerup.prevent="drawingPointerUp($event)"

View File

@ -215,7 +215,12 @@
"tool_ellipse": "Kreis",
"color": "Farbe",
"width": "Strichstärke",
"save_hint": "Zeichnung speichern (Strg+S)"
"tool_eraser": "Radierer (malt mit Hintergrundfarbe)",
"tool_eyedropper": "Pipette — Farbe vom Bild aufnehmen",
"clear": "Leeren",
"clear_hint": "Durch leeres Bild ersetzen",
"clear_title": "Durch leere Leinwand ersetzen?",
"clear_confirm": "Die gespeicherte PNG-Datei und alle Striche in dieser Sitzung werden durch ein weißes Bild ersetzt. Dies kann nicht rückgängig gemacht werden."
},
"media": {
"confirm_delete": "\"{{name}}\" löschen?",

View File

@ -214,7 +214,12 @@
"tool_ellipse": "Circle",
"color": "Colour",
"width": "Stroke width",
"save_hint": "Save drawing (Ctrl+S)"
"tool_eraser": "Eraser (paints with background colour)",
"tool_eyedropper": "Eyedropper — pick a colour from the canvas",
"clear": "Clear",
"clear_hint": "Replace with a blank image",
"clear_title": "Replace with a blank canvas?",
"clear_confirm": "The saved PNG and all strokes in this session will be replaced with a blank white image. You cannot undo this."
},
"media": {
"confirm_delete": "Delete \"{{name}}\"?",

View File

@ -215,7 +215,12 @@
"tool_ellipse": "Circle",
"color": "Color",
"width": "Stroke width",
"save_hint": "Save drawing (Ctrl+S)"
"tool_eraser": "Eraser (paints with background color)",
"tool_eyedropper": "Eyedropper — pick a color from the canvas",
"clear": "Clear",
"clear_hint": "Replace with a blank image",
"clear_title": "Replace with a blank canvas?",
"clear_confirm": "The saved PNG and all strokes in this session will be replaced with a blank white image. You cannot undo this."
},
"media": {
"confirm_delete": "Delete \"{{name}}\"?",

View File

@ -215,7 +215,12 @@
"tool_ellipse": "Círculo",
"color": "Color",
"width": "Grosor del trazo",
"save_hint": "Guardar dibujo (Ctrl+S)"
"tool_eraser": "Borrador (pinta con el color de fondo)",
"tool_eyedropper": "Cuentagotas — tomar color del lienzo",
"clear": "Borrar",
"clear_hint": "Sustituir por imagen en blanco",
"clear_title": "¿Sustituir por lienzo en blanco?",
"clear_confirm": "El PNG guardado y todos los trazos de esta sesión se sustituirán por una imagen en blanco. No se puede deshacer."
},
"media": {
"confirm_delete": "¿Eliminar \"{{name}}\"?",

View File

@ -215,7 +215,12 @@
"tool_ellipse": "Cercle",
"color": "Couleur",
"width": "Épaisseur du trait",
"save_hint": "Enregistrer le dessin (Ctrl+S)"
"tool_eraser": "Gomme (peint avec la couleur de fond)",
"tool_eyedropper": "Pipette — choisir une couleur sur le canevas",
"clear": "Effacer",
"clear_hint": "Remplacer par une image vide",
"clear_title": "Remplacer par un canevas vide ?",
"clear_confirm": "Le PNG enregistré et tous les traits de cette session seront remplacés par une image blanche vide. Cette action est irréversible."
},
"media": {
"confirm_delete": "Supprimer \"{{name}}\" ?",

View File

@ -215,7 +215,12 @@
"tool_ellipse": "Kör",
"color": "Szín",
"width": "Vonalvastagság",
"save_hint": "Rajz mentése (Ctrl+S)"
"tool_eraser": "Radír (háttérszínnel fest)",
"tool_eyedropper": "Pipetta — szín mintavétele a vászonról",
"clear": "Törlés",
"clear_hint": "Üres képre cserélés",
"clear_title": "Üres vászonra cserélés?",
"clear_confirm": "A mentett PNG és az ebben a munkamenetben rajzolt vonalak fehér üres képre cserélődnek. Ez nem vonható vissza."
},
"media": {
"confirm_delete": "Biztos törlöd a következőt: \"{{name}}\"?",

View File

@ -214,7 +214,12 @@
"tool_ellipse": "Cerchio",
"color": "Colore",
"width": "Spessore tratto",
"save_hint": "Salva disegno (Ctrl+S)"
"tool_eraser": "Gomma (disegna col colore di sfondo)",
"tool_eyedropper": "Contagocce — prendi un colore dal canvas",
"clear": "Svuota",
"clear_hint": "Sostituisci con immagine vuota",
"clear_title": "Sostituire con tela bianca?",
"clear_confirm": "Il PNG salvato e tutti i tratti di questa sessione saranno sostituiti da un'immagine bianca vuota. Non è possibile annullare."
},
"media": {
"confirm_delete": "Eliminare \"{{name}}\"?",

View File

@ -214,7 +214,12 @@
"tool_ellipse": "円",
"color": "色",
"width": "線の太さ",
"save_hint": "お絵かきを保存Ctrl+S"
"tool_eraser": "消しゴム(背景色で塗る)",
"tool_eyedropper": "スポイト — キャンバスから色を取得",
"clear": "クリア",
"clear_hint": "白紙の画像に置き換え",
"clear_title": "白紙のキャンバスに置き換えますか?",
"clear_confirm": "保存済みのPNGとこの作業中の線は、白い空白画像に置き換えられます。元に戻せません。"
},
"media": {
"confirm_delete": "「{{name}}」を削除しますか?",

View File

@ -214,7 +214,12 @@
"tool_ellipse": "Круг",
"color": "Цвет",
"width": "Толщина линии",
"save_hint": "Сохранить рисунок (Ctrl+S)"
"tool_eraser": "Ластик (рисует цветом фона)",
"tool_eyedropper": "Пипетка — взять цвет с холста",
"clear": "Очистить",
"clear_hint": "Заменить пустым изображением",
"clear_title": "Заменить пустым холстом?",
"clear_confirm": "Сохранённый PNG и все штрихи этого сеанса будут заменены пустым белым изображением. Это действие нельзя отменить."
},
"media": {
"confirm_delete": "Удалить «{{name}}»?",

View File

@ -214,7 +214,12 @@
"tool_ellipse": "Krog",
"color": "Barva",
"width": "Debelina črte",
"save_hint": "Shrani risbo (Ctrl+S)"
"tool_eraser": "Radirka (riše z barvo ozadja)",
"tool_eyedropper": "Kapalka — vzemi barvo s platna",
"clear": "Počisti",
"clear_hint": "Zamenjaj s prazno sliko",
"clear_title": "Zamenjaj s praznim platnom?",
"clear_confirm": "Shranjena datoteka PNG in vse poteze v tej seji bodo zamenjane s prazno belo sliko. Tega ni mogoče razveljaviti."
},
"media": {
"confirm_delete": "Izbrišem \"{{name}}\"?",

View File

@ -214,7 +214,12 @@
"tool_ellipse": "圆形",
"color": "颜色",
"width": "线条粗细",
"save_hint": "保存绘图Ctrl+S"
"tool_eraser": "橡皮擦(使用背景色绘制)",
"tool_eyedropper": "吸管 — 从画布取色",
"clear": "清除",
"clear_hint": "替换为空白图像",
"clear_title": "替换为空白画布?",
"clear_confirm": "已保存的 PNG 和本会话中的笔划都将被替换为空白白色图像,且无法撤销。"
},
"media": {
"confirm_delete": "删除 \"{{name}}\"",