synabun 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — Image Gallery
|
|
3
|
+
// Browse, filter, favorite, and manage images in data/images/.
|
|
4
|
+
// Draggable/resizable floating window (Session Monitor pattern).
|
|
5
|
+
// ═══════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
import { emit, on } from './state.js';
|
|
8
|
+
import { fetchImages, toggleImageFavorite, deleteImage } from './api.js';
|
|
9
|
+
import { addImageToWhiteboard } from './ui-whiteboard.js';
|
|
10
|
+
|
|
11
|
+
const $ = (id) => document.getElementById(id);
|
|
12
|
+
|
|
13
|
+
// ── State ──
|
|
14
|
+
let _panel = null;
|
|
15
|
+
let _backdrop = null;
|
|
16
|
+
let _images = [];
|
|
17
|
+
let _filter = 'all'; // 'all' | 'screenshot' | 'attachment' | 'whiteboard' | 'paste'
|
|
18
|
+
let _search = '';
|
|
19
|
+
let _sort = 'newest'; // 'newest' | 'oldest' | 'name' | 'size'
|
|
20
|
+
let _favOnly = false;
|
|
21
|
+
let _lightboxImg = null; // filename currently in lightbox
|
|
22
|
+
let isVisible = false;
|
|
23
|
+
|
|
24
|
+
// ── Drag state ──
|
|
25
|
+
let _dragging = false;
|
|
26
|
+
let _dragOff = { x: 0, y: 0 };
|
|
27
|
+
|
|
28
|
+
// ── Icons ──
|
|
29
|
+
const ICON_CLOSE = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
|
30
|
+
const ICON_REFRESH = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8a5.5 5.5 0 019.2-4M13.5 8a5.5 5.5 0 01-9.2 4"/><path d="M11.5 2v2.5H14M4.5 14v-2.5H2"/></svg>';
|
|
31
|
+
const ICON_HEART = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 13.7C4.3 11 2 8.7 2 6.3A3.3 3.3 0 018 4.6a3.3 3.3 0 016 1.7c0 2.4-2.3 4.7-6 7.4z"/></svg>';
|
|
32
|
+
const ICON_HEART_FILL = '<svg viewBox="0 0 16 16" fill="currentColor" stroke="none"><path d="M8 13.7C4.3 11 2 8.7 2 6.3A3.3 3.3 0 018 4.6a3.3 3.3 0 016 1.7c0 2.4-2.3 4.7-6 7.4z"/></svg>';
|
|
33
|
+
const ICON_TRASH = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V2.5h6V4M3.5 4v9.5a1 1 0 001 1h7a1 1 0 001-1V4"/></svg>';
|
|
34
|
+
const ICON_DOWNLOAD = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v9M5 8l3 3 3-3"/><path d="M2 12v2h12v-2"/></svg>';
|
|
35
|
+
const ICON_WHITEBOARD = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2" width="13" height="12" rx="1.5"/><path d="M4 7h8M4 10h5"/></svg>';
|
|
36
|
+
const ICON_GALLERY = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2.5" width="13" height="11" rx="2"/><circle cx="5.5" cy="6.5" r="1.5"/><path d="M1.5 11l3.5-3.5 2.5 2.5 2-1.5L14.5 13"/></svg>';
|
|
37
|
+
|
|
38
|
+
const TYPE_LABELS = {
|
|
39
|
+
all: 'All',
|
|
40
|
+
screenshot: 'Screenshots',
|
|
41
|
+
attachment: 'Attachments',
|
|
42
|
+
whiteboard: 'Whiteboard',
|
|
43
|
+
paste: 'Pastes',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ── Helpers ──
|
|
47
|
+
|
|
48
|
+
function filtered() {
|
|
49
|
+
let list = _images;
|
|
50
|
+
if (_filter !== 'all') list = list.filter(i => i.type === _filter);
|
|
51
|
+
if (_favOnly) list = list.filter(i => i.favorite);
|
|
52
|
+
if (_search) {
|
|
53
|
+
const q = _search.toLowerCase();
|
|
54
|
+
list = list.filter(i => i.filename.toLowerCase().includes(q));
|
|
55
|
+
}
|
|
56
|
+
switch (_sort) {
|
|
57
|
+
case 'oldest': list = [...list].sort((a, b) => a.modifiedAt - b.modifiedAt); break;
|
|
58
|
+
case 'name': list = [...list].sort((a, b) => a.filename.localeCompare(b.filename)); break;
|
|
59
|
+
case 'size': list = [...list].sort((a, b) => b.size - a.size); break;
|
|
60
|
+
default: list = [...list].sort((a, b) => b.modifiedAt - a.modifiedAt); break;
|
|
61
|
+
}
|
|
62
|
+
return list;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatSize(bytes) {
|
|
66
|
+
if (bytes < 1024) return bytes + ' B';
|
|
67
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
68
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatDate(ms) {
|
|
72
|
+
const d = new Date(ms);
|
|
73
|
+
const now = new Date();
|
|
74
|
+
const diff = now - d;
|
|
75
|
+
if (diff < 60000) return 'Just now';
|
|
76
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
|
77
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
|
78
|
+
if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago';
|
|
79
|
+
return d.toLocaleDateString();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function imageUrl(filename) {
|
|
83
|
+
return `/api/images/file/${encodeURIComponent(filename)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Build HTML ──
|
|
87
|
+
|
|
88
|
+
function buildPanelHTML() {
|
|
89
|
+
const list = filtered();
|
|
90
|
+
const counts = { all: _images.length };
|
|
91
|
+
for (const img of _images) counts[img.type] = (counts[img.type] || 0) + 1;
|
|
92
|
+
|
|
93
|
+
return `
|
|
94
|
+
<div class="ig-header" id="ig-header">
|
|
95
|
+
<div class="ig-header-left">
|
|
96
|
+
<span class="ig-icon">${ICON_GALLERY}</span>
|
|
97
|
+
<span class="ig-title">Image Gallery</span>
|
|
98
|
+
<span class="ig-count">${list.length}${list.length !== _images.length ? ' / ' + _images.length : ''}</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="ig-header-right">
|
|
101
|
+
<button class="ig-btn" id="ig-refresh" data-tooltip="Refresh">${ICON_REFRESH}</button>
|
|
102
|
+
<button class="backdrop-toggle-btn" id="ig-backdrop-toggle" data-tooltip="Toggle backdrop">
|
|
103
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
104
|
+
</button>
|
|
105
|
+
<button class="ig-btn" id="ig-close" data-tooltip="Close">${ICON_CLOSE}</button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="ig-toolbar">
|
|
110
|
+
<input type="text" class="ig-search" id="ig-search" placeholder="Search images..." data-tooltip="Search by filename">
|
|
111
|
+
<select class="ig-filter" id="ig-filter" data-tooltip="Filter by type">
|
|
112
|
+
${Object.entries(TYPE_LABELS).map(([k, v]) => `<option value="${k}"${_filter === k ? ' selected' : ''}>${v}${counts[k] ? ' (' + counts[k] + ')' : ''}</option>`).join('')}
|
|
113
|
+
</select>
|
|
114
|
+
<select class="ig-sort" id="ig-sort" data-tooltip="Sort order">
|
|
115
|
+
<option value="newest"${_sort === 'newest' ? ' selected' : ''}>Newest</option>
|
|
116
|
+
<option value="oldest"${_sort === 'oldest' ? ' selected' : ''}>Oldest</option>
|
|
117
|
+
<option value="name"${_sort === 'name' ? ' selected' : ''}>Name</option>
|
|
118
|
+
<option value="size"${_sort === 'size' ? ' selected' : ''}>Size</option>
|
|
119
|
+
</select>
|
|
120
|
+
<button class="ig-btn ig-fav-toggle${_favOnly ? ' active' : ''}" id="ig-fav-toggle" data-tooltip="Favorites only">${ICON_HEART}</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="ig-body" id="ig-body">
|
|
124
|
+
${list.length === 0
|
|
125
|
+
? `<div class="ig-empty">${_images.length === 0 ? 'No images yet' : 'No matches'}</div>`
|
|
126
|
+
: `<div class="ig-grid">${list.map(img => buildCard(img)).join('')}</div>`
|
|
127
|
+
}
|
|
128
|
+
</div>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildCard(img) {
|
|
133
|
+
return `
|
|
134
|
+
<div class="ig-card" data-filename="${img.filename}">
|
|
135
|
+
<div class="ig-thumb-wrap">
|
|
136
|
+
<img class="ig-thumb" src="${imageUrl(img.filename)}" loading="lazy" alt="${img.filename}">
|
|
137
|
+
<div class="ig-card-actions">
|
|
138
|
+
<button class="ig-card-btn ig-card-fav${img.favorite ? ' active' : ''}" data-action="fav" data-tooltip="${img.favorite ? 'Unfavorite' : 'Favorite'}">${img.favorite ? ICON_HEART_FILL : ICON_HEART}</button>
|
|
139
|
+
<button class="ig-card-btn ig-card-wb" data-action="whiteboard" data-tooltip="Add to Whiteboard">${ICON_WHITEBOARD}</button>
|
|
140
|
+
<button class="ig-card-btn ig-card-dl" data-action="download" data-tooltip="Download">${ICON_DOWNLOAD}</button>
|
|
141
|
+
<button class="ig-card-btn ig-card-del" data-action="delete" data-tooltip="Delete">${ICON_TRASH}</button>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="ig-card-info">
|
|
145
|
+
<span class="ig-card-name">${img.filename.length > 30 ? img.filename.slice(0, 27) + '...' : img.filename}</span>
|
|
146
|
+
<span class="ig-card-meta">${formatSize(img.size)} · ${formatDate(img.modifiedAt)}</span>
|
|
147
|
+
</div>
|
|
148
|
+
</div>`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Lightbox ──
|
|
152
|
+
|
|
153
|
+
function openLightbox(filename) {
|
|
154
|
+
_lightboxImg = filename;
|
|
155
|
+
let lb = document.getElementById('ig-lightbox');
|
|
156
|
+
if (!lb) {
|
|
157
|
+
lb = document.createElement('div');
|
|
158
|
+
lb.id = 'ig-lightbox';
|
|
159
|
+
lb.className = 'ig-lightbox';
|
|
160
|
+
document.body.appendChild(lb);
|
|
161
|
+
}
|
|
162
|
+
const img = _images.find(i => i.filename === filename);
|
|
163
|
+
lb.innerHTML = `
|
|
164
|
+
<div class="ig-lb-overlay" id="ig-lb-overlay"></div>
|
|
165
|
+
<div class="ig-lb-content">
|
|
166
|
+
<img class="ig-lb-img" src="${imageUrl(filename)}" alt="${filename}">
|
|
167
|
+
<div class="ig-lb-bar">
|
|
168
|
+
<span class="ig-lb-name">${filename}</span>
|
|
169
|
+
${img ? `<span class="ig-lb-size">${formatSize(img.size)}</span>` : ''}
|
|
170
|
+
<div class="ig-lb-actions">
|
|
171
|
+
<button class="ig-btn" id="ig-lb-fav" data-tooltip="Toggle favorite">${img?.favorite ? ICON_HEART_FILL : ICON_HEART}</button>
|
|
172
|
+
<button class="ig-btn" id="ig-lb-wb" data-tooltip="Add to Whiteboard">${ICON_WHITEBOARD}</button>
|
|
173
|
+
<button class="ig-btn" id="ig-lb-dl" data-tooltip="Download">${ICON_DOWNLOAD}</button>
|
|
174
|
+
<button class="ig-btn" id="ig-lb-del" data-tooltip="Delete">${ICON_TRASH}</button>
|
|
175
|
+
<button class="ig-btn" id="ig-lb-close" data-tooltip="Close">${ICON_CLOSE}</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<button class="ig-lb-nav ig-lb-prev" id="ig-lb-prev">‹</button>
|
|
179
|
+
<button class="ig-lb-nav ig-lb-next" id="ig-lb-next">›</button>
|
|
180
|
+
</div>
|
|
181
|
+
`;
|
|
182
|
+
lb.classList.add('open');
|
|
183
|
+
wireLightbox();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function closeLightbox() {
|
|
187
|
+
_lightboxImg = null;
|
|
188
|
+
const lb = document.getElementById('ig-lightbox');
|
|
189
|
+
if (lb) { lb.classList.remove('open'); lb.innerHTML = ''; }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function navigateLightbox(dir) {
|
|
193
|
+
const list = filtered();
|
|
194
|
+
const idx = list.findIndex(i => i.filename === _lightboxImg);
|
|
195
|
+
if (idx === -1) return;
|
|
196
|
+
const next = idx + dir;
|
|
197
|
+
if (next >= 0 && next < list.length) openLightbox(list[next].filename);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function wireLightbox() {
|
|
201
|
+
const lb = document.getElementById('ig-lightbox');
|
|
202
|
+
if (!lb) return;
|
|
203
|
+
|
|
204
|
+
// Overlay click disabled — close only via ESC or close button
|
|
205
|
+
lb.querySelector('#ig-lb-close')?.addEventListener('click', closeLightbox);
|
|
206
|
+
lb.querySelector('#ig-lb-prev')?.addEventListener('click', () => navigateLightbox(-1));
|
|
207
|
+
lb.querySelector('#ig-lb-next')?.addEventListener('click', () => navigateLightbox(1));
|
|
208
|
+
|
|
209
|
+
lb.querySelector('#ig-lb-fav')?.addEventListener('click', async () => {
|
|
210
|
+
const img = _images.find(i => i.filename === _lightboxImg);
|
|
211
|
+
if (!img) return;
|
|
212
|
+
await toggleImageFavorite(img.filename, !img.favorite);
|
|
213
|
+
img.favorite = !img.favorite;
|
|
214
|
+
render();
|
|
215
|
+
openLightbox(img.filename);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
lb.querySelector('#ig-lb-wb')?.addEventListener('click', () => {
|
|
219
|
+
if (_lightboxImg) {
|
|
220
|
+
addImageToWhiteboard(imageUrl(_lightboxImg));
|
|
221
|
+
closeLightbox();
|
|
222
|
+
closePanel();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
lb.querySelector('#ig-lb-dl')?.addEventListener('click', () => {
|
|
227
|
+
if (_lightboxImg) downloadImage(_lightboxImg);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
lb.querySelector('#ig-lb-del')?.addEventListener('click', async () => {
|
|
231
|
+
if (!_lightboxImg) return;
|
|
232
|
+
if (!confirm(`Delete ${_lightboxImg}?`)) return;
|
|
233
|
+
await deleteImage(_lightboxImg);
|
|
234
|
+
_images = _images.filter(i => i.filename !== _lightboxImg);
|
|
235
|
+
closeLightbox();
|
|
236
|
+
render();
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Actions ──
|
|
241
|
+
|
|
242
|
+
function downloadImage(filename) {
|
|
243
|
+
const a = document.createElement('a');
|
|
244
|
+
a.href = imageUrl(filename);
|
|
245
|
+
a.download = filename;
|
|
246
|
+
document.body.appendChild(a);
|
|
247
|
+
a.click();
|
|
248
|
+
a.remove();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function refreshImages() {
|
|
252
|
+
try {
|
|
253
|
+
const data = await fetchImages();
|
|
254
|
+
_images = data.images || [];
|
|
255
|
+
render();
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.error('[image-gallery] refresh failed:', err);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Render ──
|
|
262
|
+
|
|
263
|
+
function render() {
|
|
264
|
+
if (!_panel) return;
|
|
265
|
+
const body = _panel.querySelector('#ig-body');
|
|
266
|
+
const countEl = _panel.querySelector('.ig-count');
|
|
267
|
+
const list = filtered();
|
|
268
|
+
|
|
269
|
+
if (countEl) {
|
|
270
|
+
countEl.textContent = list.length + (list.length !== _images.length ? ' / ' + _images.length : '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!body) return;
|
|
274
|
+
|
|
275
|
+
if (list.length === 0) {
|
|
276
|
+
body.innerHTML = `<div class="ig-empty">${_images.length === 0 ? 'No images yet' : 'No matches'}</div>`;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
body.innerHTML = `<div class="ig-grid">${list.map(img => buildCard(img)).join('')}</div>`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Panel lifecycle ──
|
|
284
|
+
|
|
285
|
+
function wirePanel() {
|
|
286
|
+
_panel.querySelector('#ig-close')?.addEventListener('click', closePanel);
|
|
287
|
+
_panel.querySelector('#ig-refresh')?.addEventListener('click', refreshImages);
|
|
288
|
+
|
|
289
|
+
const igBdToggle = _panel.querySelector('#ig-backdrop-toggle');
|
|
290
|
+
if (igBdToggle) {
|
|
291
|
+
igBdToggle.addEventListener('click', () => {
|
|
292
|
+
if (_backdrop) {
|
|
293
|
+
_backdrop.classList.toggle('backdrop-hidden');
|
|
294
|
+
igBdToggle.classList.toggle('active', _backdrop.classList.contains('backdrop-hidden'));
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Search
|
|
300
|
+
const searchInput = _panel.querySelector('#ig-search');
|
|
301
|
+
if (searchInput) {
|
|
302
|
+
let debounce = null;
|
|
303
|
+
searchInput.addEventListener('input', () => {
|
|
304
|
+
clearTimeout(debounce);
|
|
305
|
+
debounce = setTimeout(() => { _search = searchInput.value; render(); }, 200);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Filter
|
|
310
|
+
_panel.querySelector('#ig-filter')?.addEventListener('change', (e) => {
|
|
311
|
+
_filter = e.target.value;
|
|
312
|
+
render();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Sort
|
|
316
|
+
_panel.querySelector('#ig-sort')?.addEventListener('change', (e) => {
|
|
317
|
+
_sort = e.target.value;
|
|
318
|
+
render();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Favorites toggle
|
|
322
|
+
_panel.querySelector('#ig-fav-toggle')?.addEventListener('click', (e) => {
|
|
323
|
+
_favOnly = !_favOnly;
|
|
324
|
+
e.currentTarget.classList.toggle('active', _favOnly);
|
|
325
|
+
render();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Grid clicks (delegate)
|
|
329
|
+
_panel.querySelector('#ig-body')?.addEventListener('click', async (e) => {
|
|
330
|
+
const btn = e.target.closest('[data-action]');
|
|
331
|
+
const card = e.target.closest('.ig-card');
|
|
332
|
+
if (!card) return;
|
|
333
|
+
|
|
334
|
+
const filename = card.dataset.filename;
|
|
335
|
+
if (!filename) return;
|
|
336
|
+
|
|
337
|
+
if (btn) {
|
|
338
|
+
e.stopPropagation();
|
|
339
|
+
const action = btn.dataset.action;
|
|
340
|
+
if (action === 'fav') {
|
|
341
|
+
const img = _images.find(i => i.filename === filename);
|
|
342
|
+
if (!img) return;
|
|
343
|
+
await toggleImageFavorite(filename, !img.favorite);
|
|
344
|
+
img.favorite = !img.favorite;
|
|
345
|
+
render();
|
|
346
|
+
} else if (action === 'whiteboard') {
|
|
347
|
+
addImageToWhiteboard(imageUrl(filename));
|
|
348
|
+
closePanel();
|
|
349
|
+
} else if (action === 'download') {
|
|
350
|
+
downloadImage(filename);
|
|
351
|
+
} else if (action === 'delete') {
|
|
352
|
+
if (!confirm(`Delete ${filename}?`)) return;
|
|
353
|
+
await deleteImage(filename);
|
|
354
|
+
_images = _images.filter(i => i.filename !== filename);
|
|
355
|
+
render();
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
openLightbox(filename);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Drag
|
|
363
|
+
const header = _panel.querySelector('#ig-header');
|
|
364
|
+
if (header) {
|
|
365
|
+
header.addEventListener('mousedown', (e) => {
|
|
366
|
+
if (e.target.closest('button')) return;
|
|
367
|
+
_dragging = true;
|
|
368
|
+
_panel.classList.add('dragging');
|
|
369
|
+
const rect = _panel.getBoundingClientRect();
|
|
370
|
+
_dragOff = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
371
|
+
e.preventDefault();
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Keyboard in lightbox
|
|
376
|
+
document.addEventListener('keydown', handleKey);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function handleKey(e) {
|
|
380
|
+
if (e.key === 'Escape') {
|
|
381
|
+
if (_lightboxImg) { closeLightbox(); e.stopPropagation(); }
|
|
382
|
+
else if (_panel) { closePanel(); e.stopPropagation(); }
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (!_lightboxImg) return;
|
|
386
|
+
if (e.key === 'ArrowLeft') navigateLightbox(-1);
|
|
387
|
+
else if (e.key === 'ArrowRight') navigateLightbox(1);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function openPanel() {
|
|
391
|
+
if (_panel) { _panel.focus(); return; }
|
|
392
|
+
|
|
393
|
+
_backdrop = document.createElement('div');
|
|
394
|
+
_backdrop.className = 'ig-backdrop';
|
|
395
|
+
// Backdrop click disabled — close only via ESC or close button
|
|
396
|
+
document.body.appendChild(_backdrop);
|
|
397
|
+
|
|
398
|
+
_panel = document.createElement('div');
|
|
399
|
+
_panel.className = 'image-gallery-panel glass resizable';
|
|
400
|
+
_panel.id = 'image-gallery-panel';
|
|
401
|
+
_panel.innerHTML = buildPanelHTML();
|
|
402
|
+
document.body.appendChild(_panel);
|
|
403
|
+
|
|
404
|
+
_panel.style.left = Math.max(20, (window.innerWidth - 780) / 2) + 'px';
|
|
405
|
+
_panel.style.top = Math.max(48, (window.innerHeight - 560) / 2) + 'px';
|
|
406
|
+
|
|
407
|
+
wirePanel();
|
|
408
|
+
isVisible = true;
|
|
409
|
+
refreshImages();
|
|
410
|
+
|
|
411
|
+
// Global mouse handlers for drag
|
|
412
|
+
const onMouseMove = (e) => {
|
|
413
|
+
if (!_dragging) return;
|
|
414
|
+
_panel.style.left = (e.clientX - _dragOff.x) + 'px';
|
|
415
|
+
_panel.style.top = (e.clientY - _dragOff.y) + 'px';
|
|
416
|
+
};
|
|
417
|
+
const onMouseUp = () => {
|
|
418
|
+
_dragging = false;
|
|
419
|
+
if (_panel) _panel.classList.remove('dragging');
|
|
420
|
+
};
|
|
421
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
422
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
423
|
+
|
|
424
|
+
// Store cleanup ref
|
|
425
|
+
_panel._cleanup = () => {
|
|
426
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
427
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
428
|
+
document.removeEventListener('keydown', handleKey);
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
requestAnimationFrame(() => {
|
|
432
|
+
_backdrop.classList.add('open');
|
|
433
|
+
_panel.classList.add('open');
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function closePanel() {
|
|
438
|
+
if (!_panel) return;
|
|
439
|
+
closeLightbox();
|
|
440
|
+
if (_panel._cleanup) _panel._cleanup();
|
|
441
|
+
if (_backdrop) { _backdrop.remove(); _backdrop = null; }
|
|
442
|
+
_panel.remove();
|
|
443
|
+
_panel = null;
|
|
444
|
+
isVisible = false;
|
|
445
|
+
_search = '';
|
|
446
|
+
_filter = 'all';
|
|
447
|
+
_sort = 'newest';
|
|
448
|
+
_favOnly = false;
|
|
449
|
+
emit('image-gallery:closed');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── Exports ──
|
|
453
|
+
|
|
454
|
+
export function toggleImageGallery() {
|
|
455
|
+
if (_panel) closePanel();
|
|
456
|
+
else openPanel();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function initImageGallery() {
|
|
460
|
+
// Nothing needed at init — panel is created on toggle
|
|
461
|
+
}
|