Bring the full EditmaskwithAI application into the repo under paintplus/ (429 files) so the service is self-contained — the installer copies the vendored source to ~/docker/paintplus/src instead of cloning at runtime. Rename to PaintPlus (service + branding; app logic untouched): - services/editmaskwithai.sh -> services/paintplus.sh (register_service paintplus, install_paintplus, ~/docker/paintplus, Caddy paintplus:8000, Authelia option preserved) - container names -> paintplus across docker-compose*.yml; dev network -> paintplus-network - browser <title> -> "PaintPlus - AI Image Editor"; README heading -> PaintPlus with upstream provenance note - README utilities table: editmaskwithai -> paintplus Backend/frontend code (help strings referencing the old container name, the ai_photo_edit.db filename) is intentionally left as-is to avoid touching application logic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nb2vJ8W7bHKx1JXVvpCraH
115 lines
4.1 KiB
JavaScript
115 lines
4.1 KiB
JavaScript
/**
|
||
* Layer Alignment — align the active layer (or multiple selected layers) to the canvas.
|
||
* Operations: center H, center V, center both, align left/right/top/bottom, distribute.
|
||
* Shows as a compact floating toolbar.
|
||
*
|
||
* Menu target: layer/align.align
|
||
*/
|
||
|
||
import app from './../../app.js';
|
||
import config from './../../config.js';
|
||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||
|
||
var instance = null;
|
||
|
||
const BUTTONS = [
|
||
{ id: 'ch', label: '⬌', title: 'Center horizontally on canvas' },
|
||
{ id: 'cv', label: '⬍', title: 'Center vertically on canvas' },
|
||
{ id: 'cc', label: '⊕', title: 'Center on canvas' },
|
||
{ id: 'sep', label: '|', title: '', sep: true },
|
||
{ id: 'al', label: '⇤', title: 'Align left edge to canvas' },
|
||
{ id: 'ar', label: '⇥', title: 'Align right edge to canvas' },
|
||
{ id: 'at', label: '⇡', title: 'Align top edge to canvas' },
|
||
{ id: 'ab', label: '⇣', title: 'Align bottom edge to canvas' },
|
||
];
|
||
|
||
class Layer_align_class {
|
||
constructor() {
|
||
if (instance) return instance;
|
||
instance = this;
|
||
this._panel = null;
|
||
}
|
||
|
||
align() {
|
||
if (this._panel) { this._removePanel(); return; }
|
||
this._mountPanel();
|
||
}
|
||
|
||
_mountPanel() {
|
||
this._removePanel();
|
||
const panel = document.createElement('div');
|
||
panel.id = 'align_panel';
|
||
Object.assign(panel.style, {
|
||
position: 'fixed',
|
||
top: '60px',
|
||
left: '50%',
|
||
transform: 'translateX(-50%)',
|
||
background: '#1a1a1a',
|
||
border: '1px solid #3a3a3a',
|
||
borderRadius: '10px',
|
||
padding: '7px 10px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '4px',
|
||
zIndex: '8889',
|
||
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
|
||
fontFamily: 'sans-serif',
|
||
userSelect: 'none',
|
||
});
|
||
|
||
const btnHtml = BUTTONS.map(b => {
|
||
if (b.sep) return `<span style="color:#444;padding:0 2px;">│</span>`;
|
||
return `<button data-align="${b.id}" title="${b.title}"
|
||
style="width:30px;height:30px;border-radius:6px;border:1px solid #444;
|
||
background:#252525;color:#ccc;cursor:pointer;font-size:16px;
|
||
display:flex;align-items:center;justify-content:center;
|
||
transition:background .12s;"
|
||
onmouseover="this.style.background='#333'"
|
||
onmouseout="this.style.background='#252525'">${b.label}</button>`;
|
||
}).join('');
|
||
|
||
panel.innerHTML = `
|
||
<span style="font-size:11px;color:#555;margin-right:4px;">Align:</span>
|
||
${btnHtml}
|
||
<span id="align-close" style="margin-left:6px;cursor:pointer;color:#555;font-size:18px;">×</span>`;
|
||
|
||
document.body.appendChild(panel);
|
||
this._panel = panel;
|
||
|
||
panel.querySelector('#align-close').addEventListener('click', () => this._removePanel());
|
||
panel.querySelectorAll('[data-align]').forEach(btn => {
|
||
btn.addEventListener('click', () => this._doAlign(btn.dataset.align));
|
||
});
|
||
}
|
||
|
||
_doAlign(op) {
|
||
const layer = config.layer;
|
||
if (!layer) { alertify.error('Select a layer first.'); return; }
|
||
|
||
const cw = config.WIDTH;
|
||
const ch = config.HEIGHT;
|
||
const lw = layer.width;
|
||
const lh = layer.height;
|
||
|
||
let newX = layer.x;
|
||
let newY = layer.y;
|
||
|
||
if (op === 'ch' || op === 'cc') newX = Math.round((cw - lw) / 2);
|
||
if (op === 'cv' || op === 'cc') newY = Math.round((ch - lh) / 2);
|
||
if (op === 'al') newX = 0;
|
||
if (op === 'ar') newX = cw - lw;
|
||
if (op === 'at') newY = 0;
|
||
if (op === 'ab') newY = ch - lh;
|
||
|
||
app.State.do_action(
|
||
new app.Actions.Update_layer_action(layer.id, { x: newX, y: newY })
|
||
);
|
||
}
|
||
|
||
_removePanel() {
|
||
if (this._panel) { this._panel.remove(); this._panel = null; }
|
||
}
|
||
}
|
||
|
||
export default Layer_align_class;
|