Add My Library for reusable assets (clip art, templates)

- Save layer or selection to personal library
- Browse library with category grouping and thumbnails
- Insert assets as new layers (double-click or Insert button)
- Delete unwanted assets
- Export library to JSON for backup
- Import library from JSON backup
- Assets stored in browser IndexedDB (persistent)
- Categories: General, Shapes, Borders, Icons, Templates, etc.
This commit is contained in:
Claude
2026-01-27 13:15:58 +00:00
parent 4d4947df13
commit 8087d6967a
3 changed files with 618 additions and 0 deletions
+76
View File
@@ -563,6 +563,82 @@ IMPORTANT: any new icon should also must be added on /service-worker.js + its ve
color: var(--text-color);
cursor:pointer;
}
/* My Library browser */
.library-browser{
max-height: 400px;
overflow-y: auto;
padding: 10px;
}
.library-category{
margin-bottom: 20px;
}
.library-category h3{
margin: 0 0 10px 0;
padding-bottom: 5px;
border-bottom: 1px solid var(--border-color);
color: var(--text-color);
font-size: 14px;
}
.library-items{
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.library-item{
width: 120px;
padding: 8px;
background: var(--area-background-color);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: 0.2s;
}
.library-item:hover{
border-color: var(--background-color-active);
transform: scale(1.02);
}
.library-item img{
width: 100%;
height: 80px;
object-fit: contain;
background: repeating-conic-gradient(#808080 0% 25%, #666 0% 50%) 50% / 10px 10px;
border-radius: 2px;
}
.library-item-name{
margin-top: 5px;
font-size: 11px;
color: var(--text-color);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.library-item-actions{
display: flex;
gap: 5px;
margin-top: 5px;
}
.library-item-actions button{
flex: 1;
padding: 3px 5px;
font-size: 10px;
cursor: pointer;
border: 1px solid var(--border-color);
border-radius: 3px;
background: var(--section-background-color);
color: var(--text-color);
}
.library-item-actions .insert-btn{
background: var(--background-color-active);
color: var(--text-color-active);
}
.library-item-actions .delete-btn:hover{
background: #dc3545;
color: #fff;
}
.library-dialog .dialog_content{
min-width: 500px;
}
.sidebar_right .label{
display: inline-block;
}
+38
View File
@@ -86,6 +86,44 @@ const menuDefinition = [
name: 'Quick Load',
shortcut: 'F10',
target: 'file/quickload.quickload'
},
{
divider: true
},
{
name: 'My Library',
children: [
{
name: 'Browse Library',
ellipsis: true,
target: 'file/my_library.browse_library'
},
{
divider: true
},
{
name: 'Save Layer to Library',
ellipsis: true,
target: 'file/my_library.save_to_library'
},
{
name: 'Save Selection to Library',
ellipsis: true,
target: 'file/my_library.save_selection_to_library'
},
{
divider: true
},
{
name: 'Export Library (Backup)',
target: 'file/my_library.export_library'
},
{
name: 'Import Library',
ellipsis: true,
target: 'file/my_library.import_library'
}
]
}
]
},
+504
View File
@@ -0,0 +1,504 @@
/**
* My Library - Save and reuse your own assets (clip art, templates, etc.)
* Assets are stored in browser's IndexedDB for persistence
*/
import app from './../../app.js';
import config from './../../config.js';
import Dialog_class from './../../libs/popup.js';
import Base_layers_class from './../../core/base-layers.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
var instance = null;
// IndexedDB setup
const DB_NAME = 'miniPaintLibrary';
const DB_VERSION = 1;
const STORE_NAME = 'assets';
class File_my_library_class {
constructor() {
if (instance) {
return instance;
}
instance = this;
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.db = null;
this.initDB();
}
/**
* Initialize IndexedDB
*/
initDB() {
var _this = this;
var request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = function(event) {
console.error('IndexedDB error:', event);
};
request.onsuccess = function(event) {
_this.db = event.target.result;
};
request.onupgradeneeded = function(event) {
var db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
var store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
store.createIndex('name', 'name', { unique: false });
store.createIndex('category', 'category', { unique: false });
store.createIndex('created', 'created', { unique: false });
}
};
}
/**
* Save current layer as a library asset
*/
save_to_library() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('Please select an image layer to save');
return;
}
var settings = {
title: 'Save to My Library',
params: [
{
name: "name",
title: "Asset Name:",
value: config.layer.name || "My Asset"
},
{
name: "category",
title: "Category:",
value: "General",
values: ["General", "Shapes", "Borders", "Icons", "Templates", "Text Elements", "Backgrounds", "Other"]
},
{
name: "description",
title: "Description:",
value: ""
}
],
on_finish: function (params) {
_this.do_save_to_library(params);
},
};
this.POP.show(settings);
}
/**
* Save current selection to library (uses mask if available)
*/
save_selection_to_library() {
var _this = this;
if (!window.smartSelectMask || !window.smartSelectMask.canvas) {
alertify.error('No selection. Use a selection tool first.');
return;
}
if (config.layer.type != 'image') {
alertify.error('Please select an image layer');
return;
}
var settings = {
title: 'Save Selection to Library',
params: [
{
name: "name",
title: "Asset Name:",
value: "Selection Asset"
},
{
name: "category",
title: "Category:",
value: "General",
values: ["General", "Shapes", "Borders", "Icons", "Templates", "Text Elements", "Backgrounds", "Other"]
},
{
name: "description",
title: "Description:",
value: ""
}
],
on_finish: function (params) {
_this.do_save_selection_to_library(params);
},
};
this.POP.show(settings);
}
do_save_to_library(params) {
var _this = this;
// Get canvas from current layer
var canvas = this.Base_layers.convert_layer_to_canvas(config.layer.id, true, false);
// Create thumbnail (max 150px)
var thumbCanvas = document.createElement('canvas');
var maxSize = 150;
var scale = Math.min(maxSize / canvas.width, maxSize / canvas.height);
thumbCanvas.width = Math.round(canvas.width * scale);
thumbCanvas.height = Math.round(canvas.height * scale);
var thumbCtx = thumbCanvas.getContext('2d');
thumbCtx.drawImage(canvas, 0, 0, thumbCanvas.width, thumbCanvas.height);
var asset = {
name: params.name,
category: params.category,
description: params.description,
width: canvas.width,
height: canvas.height,
data: canvas.toDataURL('image/png'),
thumbnail: thumbCanvas.toDataURL('image/png'),
created: new Date().toISOString()
};
this.saveAsset(asset, function() {
alertify.success('Saved "' + params.name + '" to library!');
});
}
do_save_selection_to_library(params) {
var _this = this;
var layer = config.layer;
var maskCanvas = window.smartSelectMask.canvas;
// Create canvas with just the selected pixels
var canvas = document.createElement('canvas');
canvas.width = layer.width_original;
canvas.height = layer.height_original;
var ctx = canvas.getContext('2d');
ctx.drawImage(layer.link, 0, 0);
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(maskCanvas, 0, 0);
// Find bounds of selection
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
var minX = canvas.width, minY = canvas.height, maxX = 0, maxY = 0;
for (var y = 0; y < canvas.height; y++) {
for (var x = 0; x < canvas.width; x++) {
var i = (y * canvas.width + x) * 4;
if (data[i + 3] > 0) {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
}
// Crop to selection bounds
var cropWidth = maxX - minX + 1;
var cropHeight = maxY - minY + 1;
var croppedCanvas = document.createElement('canvas');
croppedCanvas.width = cropWidth;
croppedCanvas.height = cropHeight;
var croppedCtx = croppedCanvas.getContext('2d');
croppedCtx.drawImage(canvas, minX, minY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
// Create thumbnail
var thumbCanvas = document.createElement('canvas');
var maxSize = 150;
var scale = Math.min(maxSize / cropWidth, maxSize / cropHeight);
thumbCanvas.width = Math.round(cropWidth * scale);
thumbCanvas.height = Math.round(cropHeight * scale);
var thumbCtx = thumbCanvas.getContext('2d');
thumbCtx.drawImage(croppedCanvas, 0, 0, thumbCanvas.width, thumbCanvas.height);
var asset = {
name: params.name,
category: params.category,
description: params.description,
width: cropWidth,
height: cropHeight,
data: croppedCanvas.toDataURL('image/png'),
thumbnail: thumbCanvas.toDataURL('image/png'),
created: new Date().toISOString()
};
this.saveAsset(asset, function() {
alertify.success('Saved selection "' + params.name + '" to library!');
});
}
saveAsset(asset, callback) {
if (!this.db) {
alertify.error('Database not ready, please try again');
return;
}
var transaction = this.db.transaction([STORE_NAME], 'readwrite');
var store = transaction.objectStore(STORE_NAME);
var request = store.add(asset);
request.onsuccess = function() {
if (callback) callback();
};
request.onerror = function(event) {
alertify.error('Error saving asset');
console.error('Save error:', event);
};
}
/**
* Browse and insert assets from library
*/
browse_library() {
var _this = this;
this.getAllAssets(function(assets) {
_this.showLibraryBrowser(assets);
});
}
getAllAssets(callback) {
if (!this.db) {
alertify.error('Database not ready');
callback([]);
return;
}
var transaction = this.db.transaction([STORE_NAME], 'readonly');
var store = transaction.objectStore(STORE_NAME);
var request = store.getAll();
request.onsuccess = function(event) {
callback(event.target.result || []);
};
request.onerror = function() {
callback([]);
};
}
showLibraryBrowser(assets) {
var _this = this;
if (assets.length === 0) {
alertify.warning('Your library is empty. Save some assets first!');
return;
}
// Group by category
var categories = {};
assets.forEach(function(asset) {
var cat = asset.category || 'General';
if (!categories[cat]) categories[cat] = [];
categories[cat].push(asset);
});
// Build HTML for the browser
var html = '<div class="library-browser">';
html += '<div class="library-categories">';
for (var cat in categories) {
html += '<div class="library-category">';
html += '<h3>' + cat + '</h3>';
html += '<div class="library-items">';
categories[cat].forEach(function(asset) {
html += '<div class="library-item" data-id="' + asset.id + '">';
html += '<img src="' + asset.thumbnail + '" alt="' + asset.name + '" title="' + asset.name + '">';
html += '<div class="library-item-name">' + asset.name + '</div>';
html += '<div class="library-item-actions">';
html += '<button class="insert-btn" data-id="' + asset.id + '">Insert</button>';
html += '<button class="delete-btn" data-id="' + asset.id + '">Delete</button>';
html += '</div>';
html += '</div>';
});
html += '</div></div>';
}
html += '</div></div>';
// Show in dialog
var settings = {
title: 'My Library (' + assets.length + ' assets)',
params: [],
html: html,
className: 'library-dialog',
on_load: function(el) {
// Add click handlers
el.querySelectorAll('.insert-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var id = parseInt(this.dataset.id);
_this.insertAsset(id);
_this.POP.hide();
});
});
el.querySelectorAll('.delete-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var id = parseInt(this.dataset.id);
if (confirm('Delete this asset?')) {
_this.deleteAsset(id, function() {
alertify.success('Asset deleted');
_this.POP.hide();
});
}
});
});
// Double-click to insert
el.querySelectorAll('.library-item').forEach(function(item) {
item.addEventListener('dblclick', function() {
var id = parseInt(this.dataset.id);
_this.insertAsset(id);
_this.POP.hide();
});
});
}
};
this.POP.show(settings);
}
insertAsset(id) {
var _this = this;
var transaction = this.db.transaction([STORE_NAME], 'readonly');
var store = transaction.objectStore(STORE_NAME);
var request = store.get(id);
request.onsuccess = function(event) {
var asset = event.target.result;
if (asset) {
_this.createLayerFromAsset(asset);
}
};
}
createLayerFromAsset(asset) {
var _this = this;
var img = new Image();
img.onload = function() {
// Insert as new layer
var params = {
x: Math.round((config.WIDTH - asset.width) / 2),
y: Math.round((config.HEIGHT - asset.height) / 2),
width: asset.width,
height: asset.height,
width_original: asset.width,
height_original: asset.height,
type: 'image',
name: asset.name,
data: asset.data
};
app.State.do_action(
new app.Actions.Bundle_action('insert_library_asset', 'Insert Library Asset', [
new app.Actions.Insert_layer_action(params)
])
);
alertify.success('Inserted "' + asset.name + '"');
};
img.src = asset.data;
}
deleteAsset(id, callback) {
var transaction = this.db.transaction([STORE_NAME], 'readwrite');
var store = transaction.objectStore(STORE_NAME);
var request = store.delete(id);
request.onsuccess = function() {
if (callback) callback();
};
}
/**
* Export library to JSON file (backup)
*/
export_library() {
var _this = this;
this.getAllAssets(function(assets) {
if (assets.length === 0) {
alertify.warning('Library is empty');
return;
}
var data = JSON.stringify(assets, null, 2);
var blob = new Blob([data], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'my-library-backup.json';
a.click();
URL.revokeObjectURL(url);
alertify.success('Library exported!');
});
}
/**
* Import library from JSON file
*/
import_library() {
var _this = this;
var input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(event) {
try {
var assets = JSON.parse(event.target.result);
if (!Array.isArray(assets)) {
alertify.error('Invalid library file');
return;
}
var imported = 0;
assets.forEach(function(asset) {
// Remove id so it gets auto-assigned
delete asset.id;
_this.saveAsset(asset, function() {
imported++;
if (imported === assets.length) {
alertify.success('Imported ' + imported + ' assets!');
}
});
});
} catch (err) {
alertify.error('Error parsing library file');
console.error(err);
}
};
reader.readAsText(file);
};
input.click();
}
}
export default File_my_library_class;