clay-server 2.16.0 → 2.17.0-beta.10
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/README.md +20 -7
- package/bin/cli.js +158 -239
- package/lib/certs/fullchain.pem +47 -0
- package/lib/certs/privkey.pem +5 -0
- package/lib/daemon.js +20 -3
- package/lib/pages.js +22 -20
- package/lib/project.js +9 -5
- package/lib/public/app.js +45 -25
- package/lib/public/css/command-palette.css +1 -1
- package/lib/public/css/mates.css +13 -14
- package/lib/public/css/menus.css +19 -0
- package/lib/public/css/overlays.css +44 -18
- package/lib/public/css/profile.css +128 -0
- package/lib/public/css/title-bar.css +0 -4
- package/lib/public/index.html +9 -5
- package/lib/public/modules/avatar.js +36 -0
- package/lib/public/modules/command-palette.js +4 -7
- package/lib/public/modules/mate-sidebar.js +5 -8
- package/lib/public/modules/profile.js +351 -24
- package/lib/public/modules/qrcode.js +23 -3
- package/lib/public/modules/sidebar.js +26 -9
- package/lib/public/sw.js +4 -1
- package/lib/server.js +224 -3
- package/lib/sessions.js +4 -4
- package/package.json +1 -1
- package/lib/themes/clay-light.json +0 -10
- package/lib/themes/clay.json +0 -10
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
6
6
|
import { setSTTLang, getSTTLang } from './stt.js';
|
|
7
|
+
import { avatarUrl, mateAvatarUrl, AVATAR_STYLES } from './avatar.js';
|
|
7
8
|
|
|
8
9
|
var ctx;
|
|
9
|
-
var profile = { name: '', lang: 'en-US', avatarStyle: 'thumbs', avatarSeed: '', avatarColor: '#7c3aed' };
|
|
10
|
+
var profile = { name: '', lang: 'en-US', avatarStyle: 'thumbs', avatarSeed: '', avatarColor: '#7c3aed', avatarCustom: '' };
|
|
10
11
|
var profileUsername = '';
|
|
11
12
|
var popoverEl = null;
|
|
12
13
|
var saveTimer = null;
|
|
@@ -22,16 +23,7 @@ var LANGUAGES = [
|
|
|
22
23
|
{ code: 'de-DE', name: 'German' },
|
|
23
24
|
];
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
{ id: 'thumbs', name: 'Thumbs' },
|
|
27
|
-
{ id: 'bottts', name: 'Bots' },
|
|
28
|
-
{ id: 'pixel-art', name: 'Pixel' },
|
|
29
|
-
{ id: 'adventurer', name: 'Adventurer' },
|
|
30
|
-
{ id: 'micah', name: 'Micah' },
|
|
31
|
-
{ id: 'lorelei', name: 'Lorelei' },
|
|
32
|
-
{ id: 'fun-emoji', name: 'Emoji' },
|
|
33
|
-
{ id: 'icons', name: 'Icons' },
|
|
34
|
-
];
|
|
26
|
+
// AVATAR_STYLES imported from avatar.js
|
|
35
27
|
|
|
36
28
|
var COLORS = [
|
|
37
29
|
'#7c3aed', '#4f46e5', '#2563eb', '#0891b2',
|
|
@@ -41,11 +33,7 @@ var COLORS = [
|
|
|
41
33
|
'#0369a1', '#15803d',
|
|
42
34
|
];
|
|
43
35
|
|
|
44
|
-
//
|
|
45
|
-
function avatarUrl(style, seed, size) {
|
|
46
|
-
var s = encodeURIComponent(seed || 'anonymous');
|
|
47
|
-
return 'https://api.dicebear.com/9.x/' + style + '/svg?seed=' + s + '&size=' + (size || 64);
|
|
48
|
-
}
|
|
36
|
+
// avatarUrl imported from avatar.js
|
|
49
37
|
|
|
50
38
|
function getAvatarSeed() {
|
|
51
39
|
return profile.avatarSeed || 'anonymous';
|
|
@@ -80,10 +68,12 @@ function applyToIsland() {
|
|
|
80
68
|
|
|
81
69
|
var displayName = profile.name || 'Awesome Clay User';
|
|
82
70
|
|
|
83
|
-
// Replace letter fallback with
|
|
71
|
+
// Replace letter fallback with avatar img
|
|
84
72
|
var existingImg = avatarWrap.querySelector('img');
|
|
85
73
|
var existingLetter = avatarWrap.querySelector('.user-island-avatar-letter');
|
|
86
|
-
var url =
|
|
74
|
+
var url = profile.avatarCustom
|
|
75
|
+
? profile.avatarCustom
|
|
76
|
+
: avatarUrl(profile.avatarStyle || 'thumbs', getAvatarSeed(), 32);
|
|
87
77
|
|
|
88
78
|
if (existingImg) {
|
|
89
79
|
existingImg.src = url;
|
|
@@ -109,6 +99,188 @@ function applyToIsland() {
|
|
|
109
99
|
}
|
|
110
100
|
}
|
|
111
101
|
|
|
102
|
+
// --- Avatar position picker ---
|
|
103
|
+
export function showAvatarPositioner(img, objectUrl, onDone) {
|
|
104
|
+
var outputSize = 256;
|
|
105
|
+
var viewSize = 220;
|
|
106
|
+
|
|
107
|
+
// State
|
|
108
|
+
var scale = 1;
|
|
109
|
+
var offsetX = 0;
|
|
110
|
+
var offsetY = 0;
|
|
111
|
+
|
|
112
|
+
// Fit image so shorter side fills the circle
|
|
113
|
+
var baseScale = Math.max(viewSize / img.width, viewSize / img.height);
|
|
114
|
+
|
|
115
|
+
function clampOffsets() {
|
|
116
|
+
var sw = img.width * baseScale * scale;
|
|
117
|
+
var sh = img.height * baseScale * scale;
|
|
118
|
+
var maxX = Math.max(0, (sw - viewSize) / 2);
|
|
119
|
+
var maxY = Math.max(0, (sh - viewSize) / 2);
|
|
120
|
+
if (offsetX > maxX) offsetX = maxX;
|
|
121
|
+
if (offsetX < -maxX) offsetX = -maxX;
|
|
122
|
+
if (offsetY > maxY) offsetY = maxY;
|
|
123
|
+
if (offsetY < -maxY) offsetY = -maxY;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function updatePreview() {
|
|
127
|
+
var s = baseScale * scale;
|
|
128
|
+
var tx = (viewSize - img.width * s) / 2 + offsetX;
|
|
129
|
+
var ty = (viewSize - img.height * s) / 2 + offsetY;
|
|
130
|
+
previewImg.style.width = (img.width * s) + 'px';
|
|
131
|
+
previewImg.style.height = (img.height * s) + 'px';
|
|
132
|
+
previewImg.style.transform = 'translate(' + tx + 'px, ' + ty + 'px)';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Build overlay
|
|
136
|
+
var overlay = document.createElement('div');
|
|
137
|
+
overlay.className = 'avatar-positioner-overlay';
|
|
138
|
+
|
|
139
|
+
var container = document.createElement('div');
|
|
140
|
+
container.className = 'avatar-positioner-container';
|
|
141
|
+
|
|
142
|
+
var closeBtn = document.createElement('button');
|
|
143
|
+
closeBtn.className = 'avatar-positioner-close';
|
|
144
|
+
closeBtn.innerHTML = '×';
|
|
145
|
+
container.appendChild(closeBtn);
|
|
146
|
+
|
|
147
|
+
var title = document.createElement('div');
|
|
148
|
+
title.className = 'avatar-positioner-title';
|
|
149
|
+
title.textContent = 'Position your avatar';
|
|
150
|
+
container.appendChild(title);
|
|
151
|
+
|
|
152
|
+
var viewport = document.createElement('div');
|
|
153
|
+
viewport.className = 'avatar-positioner-viewport';
|
|
154
|
+
viewport.style.width = viewSize + 'px';
|
|
155
|
+
viewport.style.height = viewSize + 'px';
|
|
156
|
+
|
|
157
|
+
var previewImg = document.createElement('img');
|
|
158
|
+
previewImg.src = objectUrl;
|
|
159
|
+
previewImg.className = 'avatar-positioner-img';
|
|
160
|
+
previewImg.draggable = false;
|
|
161
|
+
viewport.appendChild(previewImg);
|
|
162
|
+
container.appendChild(viewport);
|
|
163
|
+
|
|
164
|
+
// Zoom slider
|
|
165
|
+
var sliderWrap = document.createElement('div');
|
|
166
|
+
sliderWrap.className = 'avatar-positioner-slider-wrap';
|
|
167
|
+
var slider = document.createElement('input');
|
|
168
|
+
slider.type = 'range';
|
|
169
|
+
slider.min = '1';
|
|
170
|
+
slider.max = '6';
|
|
171
|
+
slider.step = '0.05';
|
|
172
|
+
slider.value = '1';
|
|
173
|
+
slider.className = 'avatar-positioner-slider';
|
|
174
|
+
sliderWrap.appendChild(slider);
|
|
175
|
+
container.appendChild(sliderWrap);
|
|
176
|
+
|
|
177
|
+
// Buttons
|
|
178
|
+
var btnRow = document.createElement('div');
|
|
179
|
+
btnRow.className = 'avatar-positioner-buttons';
|
|
180
|
+
var cancelBtn = document.createElement('button');
|
|
181
|
+
cancelBtn.className = 'avatar-positioner-btn avatar-positioner-btn-cancel';
|
|
182
|
+
cancelBtn.textContent = 'Cancel';
|
|
183
|
+
var doneBtn = document.createElement('button');
|
|
184
|
+
doneBtn.className = 'avatar-positioner-btn avatar-positioner-btn-done';
|
|
185
|
+
doneBtn.textContent = 'Done';
|
|
186
|
+
btnRow.appendChild(cancelBtn);
|
|
187
|
+
btnRow.appendChild(doneBtn);
|
|
188
|
+
container.appendChild(btnRow);
|
|
189
|
+
|
|
190
|
+
overlay.appendChild(container);
|
|
191
|
+
document.body.appendChild(overlay);
|
|
192
|
+
updatePreview();
|
|
193
|
+
|
|
194
|
+
// Drag
|
|
195
|
+
var dragging = false;
|
|
196
|
+
var dragStartX = 0;
|
|
197
|
+
var dragStartY = 0;
|
|
198
|
+
var startOffsetX = 0;
|
|
199
|
+
var startOffsetY = 0;
|
|
200
|
+
|
|
201
|
+
viewport.addEventListener('mousedown', startDrag);
|
|
202
|
+
viewport.addEventListener('touchstart', startDrag, { passive: false });
|
|
203
|
+
|
|
204
|
+
function startDrag(e) {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
dragging = true;
|
|
207
|
+
var pt = e.touches ? e.touches[0] : e;
|
|
208
|
+
dragStartX = pt.clientX;
|
|
209
|
+
dragStartY = pt.clientY;
|
|
210
|
+
startOffsetX = offsetX;
|
|
211
|
+
startOffsetY = offsetY;
|
|
212
|
+
document.addEventListener('mousemove', onDrag);
|
|
213
|
+
document.addEventListener('mouseup', endDrag);
|
|
214
|
+
document.addEventListener('touchmove', onDrag, { passive: false });
|
|
215
|
+
document.addEventListener('touchend', endDrag);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function onDrag(e) {
|
|
219
|
+
if (!dragging) return;
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
var pt = e.touches ? e.touches[0] : e;
|
|
222
|
+
offsetX = startOffsetX + (pt.clientX - dragStartX);
|
|
223
|
+
offsetY = startOffsetY + (pt.clientY - dragStartY);
|
|
224
|
+
clampOffsets();
|
|
225
|
+
updatePreview();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function endDrag() {
|
|
229
|
+
dragging = false;
|
|
230
|
+
document.removeEventListener('mousemove', onDrag);
|
|
231
|
+
document.removeEventListener('mouseup', endDrag);
|
|
232
|
+
document.removeEventListener('touchmove', onDrag);
|
|
233
|
+
document.removeEventListener('touchend', endDrag);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Zoom
|
|
237
|
+
slider.addEventListener('input', function() {
|
|
238
|
+
scale = parseFloat(slider.value);
|
|
239
|
+
clampOffsets();
|
|
240
|
+
updatePreview();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Scroll to zoom
|
|
244
|
+
viewport.addEventListener('wheel', function(e) {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
scale += e.deltaY < 0 ? 0.1 : -0.1;
|
|
247
|
+
if (scale < 1) scale = 1;
|
|
248
|
+
if (scale > 6) scale = 6;
|
|
249
|
+
slider.value = scale;
|
|
250
|
+
clampOffsets();
|
|
251
|
+
updatePreview();
|
|
252
|
+
}, { passive: false });
|
|
253
|
+
|
|
254
|
+
function cleanup(revoke) {
|
|
255
|
+
overlay.remove();
|
|
256
|
+
if (revoke) URL.revokeObjectURL(objectUrl);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
closeBtn.addEventListener('click', function() { cleanup(true); });
|
|
260
|
+
cancelBtn.addEventListener('click', function() { cleanup(true); });
|
|
261
|
+
|
|
262
|
+
doneBtn.addEventListener('click', function() {
|
|
263
|
+
// Render cropped area to canvas
|
|
264
|
+
var canvas = document.createElement('canvas');
|
|
265
|
+
canvas.width = outputSize;
|
|
266
|
+
canvas.height = outputSize;
|
|
267
|
+
var c = canvas.getContext('2d');
|
|
268
|
+
var s = baseScale * scale;
|
|
269
|
+
var tx = (viewSize - img.width * s) / 2 + offsetX;
|
|
270
|
+
var ty = (viewSize - img.height * s) / 2 + offsetY;
|
|
271
|
+
// Map from viewSize coords to outputSize
|
|
272
|
+
var ratio = outputSize / viewSize;
|
|
273
|
+
c.drawImage(img,
|
|
274
|
+
0, 0, img.width, img.height,
|
|
275
|
+
tx * ratio, ty * ratio, img.width * s * ratio, img.height * s * ratio
|
|
276
|
+
);
|
|
277
|
+
canvas.toBlob(function(blob) {
|
|
278
|
+
cleanup(false);
|
|
279
|
+
if (blob) onDone(blob);
|
|
280
|
+
}, 'image/jpeg', 0.9);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
112
284
|
// --- Popover ---
|
|
113
285
|
function showPopover() {
|
|
114
286
|
if (popoverEl) {
|
|
@@ -136,7 +308,7 @@ function showPopover() {
|
|
|
136
308
|
// Avatar row (overlapping banner)
|
|
137
309
|
html += '<div class="profile-avatar-row">';
|
|
138
310
|
html += '<div class="profile-popover-avatar">';
|
|
139
|
-
html += '<img class="profile-popover-avatar-img" src="' + avatarUrl(currentStyle, seed, 80) + '" alt="avatar">';
|
|
311
|
+
html += '<img class="profile-popover-avatar-img" src="' + (profile.avatarCustom || avatarUrl(currentStyle, seed, 80)) + '" alt="avatar">';
|
|
140
312
|
html += '</div>';
|
|
141
313
|
html += '<div class="profile-name-display">' + escapeAttr(displayName || 'Awesome Clay User') + '</div>';
|
|
142
314
|
html += '</div>';
|
|
@@ -166,9 +338,19 @@ function showPopover() {
|
|
|
166
338
|
html += '<div class="profile-field">';
|
|
167
339
|
html += '<label class="profile-field-label">Avatar <button class="profile-shuffle-btn" title="Shuffle">' + iconHtml('shuffle') + '</button></label>';
|
|
168
340
|
html += '<div class="profile-avatar-grid">';
|
|
341
|
+
// Upload button as first cell
|
|
342
|
+
var uploadActive = profile.avatarCustom ? ' profile-avatar-option-active' : '';
|
|
343
|
+
html += '<button class="profile-avatar-option profile-avatar-upload' + uploadActive + '" title="Upload photo">';
|
|
344
|
+
if (profile.avatarCustom) {
|
|
345
|
+
html += '<img src="' + profile.avatarCustom + '" alt="Custom" class="profile-avatar-custom-preview">';
|
|
346
|
+
} else {
|
|
347
|
+
html += '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
|
|
348
|
+
}
|
|
349
|
+
html += '</button>';
|
|
350
|
+
html += '<input type="file" id="profile-avatar-file" accept="image/*" style="display:none">';
|
|
169
351
|
for (var j = 0; j < AVATAR_STYLES.length; j++) {
|
|
170
352
|
var st = AVATAR_STYLES[j];
|
|
171
|
-
var activeS = (currentStyle === st.id) ? ' profile-avatar-option-active' : '';
|
|
353
|
+
var activeS = (!profile.avatarCustom && currentStyle === st.id) ? ' profile-avatar-option-active' : '';
|
|
172
354
|
html += '<button class="profile-avatar-option' + activeS + '" data-style="' + st.id + '" title="' + st.name + '">';
|
|
173
355
|
html += '<img src="' + avatarUrl(st.id, seed, 40) + '" alt="' + st.name + '">';
|
|
174
356
|
html += '</button>';
|
|
@@ -223,9 +405,61 @@ function showPopover() {
|
|
|
223
405
|
debouncedSave();
|
|
224
406
|
});
|
|
225
407
|
|
|
408
|
+
// Avatar upload button
|
|
409
|
+
var uploadBtn = popoverEl.querySelector('.profile-avatar-upload');
|
|
410
|
+
var fileInput = popoverEl.querySelector('#profile-avatar-file');
|
|
411
|
+
if (uploadBtn && fileInput) {
|
|
412
|
+
uploadBtn.addEventListener('click', function() {
|
|
413
|
+
fileInput.click();
|
|
414
|
+
});
|
|
415
|
+
fileInput.addEventListener('change', function() {
|
|
416
|
+
var file = fileInput.files[0];
|
|
417
|
+
if (!file) return;
|
|
418
|
+
|
|
419
|
+
function uploadBlob(blob) {
|
|
420
|
+
blob.arrayBuffer().then(function(ab) {
|
|
421
|
+
var buf = new Uint8Array(ab);
|
|
422
|
+
fetch('/api/avatar', {
|
|
423
|
+
method: 'POST',
|
|
424
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
425
|
+
body: buf,
|
|
426
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
427
|
+
if (data.ok) {
|
|
428
|
+
profile.avatarCustom = data.avatar;
|
|
429
|
+
applyToIsland();
|
|
430
|
+
updatePopoverHeader();
|
|
431
|
+
if (popoverEl) {
|
|
432
|
+
popoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
|
|
433
|
+
b.classList.remove('profile-avatar-option-active');
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (uploadBtn) {
|
|
437
|
+
uploadBtn.classList.add('profile-avatar-option-active');
|
|
438
|
+
uploadBtn.innerHTML = '<img src="' + data.avatar + '" alt="Custom" class="profile-avatar-custom-preview">';
|
|
439
|
+
}
|
|
440
|
+
debouncedSave();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Load image and open position picker
|
|
447
|
+
var img = new Image();
|
|
448
|
+
var objectUrl = URL.createObjectURL(file);
|
|
449
|
+
img.onload = function() {
|
|
450
|
+
showAvatarPositioner(img, objectUrl, function(croppedBlob) {
|
|
451
|
+
URL.revokeObjectURL(objectUrl);
|
|
452
|
+
uploadBlob(croppedBlob);
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
img.src = objectUrl;
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
226
459
|
// Avatar style — clicking confirms both the style and the current previewSeed
|
|
227
460
|
popoverEl.querySelectorAll('.profile-avatar-option[data-style]').forEach(function(btn) {
|
|
228
461
|
btn.addEventListener('click', function() {
|
|
462
|
+
profile.avatarCustom = '';
|
|
229
463
|
profile.avatarStyle = btn.dataset.style;
|
|
230
464
|
profile.avatarSeed = previewSeed;
|
|
231
465
|
applyToIsland();
|
|
@@ -234,6 +468,9 @@ function showPopover() {
|
|
|
234
468
|
b.classList.remove('profile-avatar-option-active');
|
|
235
469
|
});
|
|
236
470
|
btn.classList.add('profile-avatar-option-active');
|
|
471
|
+
// Reset upload button to + icon
|
|
472
|
+
var upBtn = popoverEl.querySelector('.profile-avatar-upload');
|
|
473
|
+
if (upBtn) upBtn.innerHTML = '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
|
|
237
474
|
debouncedSave();
|
|
238
475
|
});
|
|
239
476
|
});
|
|
@@ -283,7 +520,9 @@ function updatePopoverHeader() {
|
|
|
283
520
|
if (!popoverEl) return;
|
|
284
521
|
var img = popoverEl.querySelector('.profile-popover-avatar-img');
|
|
285
522
|
var nd = popoverEl.querySelector('.profile-name-display');
|
|
286
|
-
if (img) img.src =
|
|
523
|
+
if (img) img.src = profile.avatarCustom
|
|
524
|
+
? profile.avatarCustom
|
|
525
|
+
: avatarUrl(profile.avatarStyle || 'thumbs', getAvatarSeed(), 80);
|
|
287
526
|
if (nd) nd.textContent = profile.name || 'Awesome Clay User';
|
|
288
527
|
}
|
|
289
528
|
|
|
@@ -351,6 +590,7 @@ export function initProfile(_ctx) {
|
|
|
351
590
|
if (data.avatarColor) profile.avatarColor = data.avatarColor;
|
|
352
591
|
if (data.avatarStyle) profile.avatarStyle = data.avatarStyle;
|
|
353
592
|
if (data.avatarSeed) profile.avatarSeed = data.avatarSeed;
|
|
593
|
+
if (data.avatarCustom) profile.avatarCustom = data.avatarCustom;
|
|
354
594
|
if (data.username) profileUsername = data.username;
|
|
355
595
|
|
|
356
596
|
// Auto-generate seed if none exists
|
|
@@ -393,6 +633,7 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
|
|
|
393
633
|
var mateColor = mp.avatarColor || '#7c3aed';
|
|
394
634
|
var mateStyle = mp.avatarStyle || 'bottts';
|
|
395
635
|
var mateSeed = mp.avatarSeed || mateData.id || 'mate';
|
|
636
|
+
var mateCustom = mp.avatarCustom || '';
|
|
396
637
|
matePreviewSeed = mateSeed;
|
|
397
638
|
|
|
398
639
|
matePopoverEl = document.createElement('div');
|
|
@@ -406,9 +647,10 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
|
|
|
406
647
|
html += '</div>';
|
|
407
648
|
|
|
408
649
|
// Avatar row
|
|
650
|
+
var mateAvatarSrc = mateCustom ? mateCustom : avatarUrl(mateStyle, mateSeed, 80);
|
|
409
651
|
html += '<div class="profile-avatar-row">';
|
|
410
652
|
html += '<div class="profile-popover-avatar">';
|
|
411
|
-
html += '<img class="profile-popover-avatar-img" src="' +
|
|
653
|
+
html += '<img class="profile-popover-avatar-img" src="' + mateAvatarSrc + '" alt="avatar">';
|
|
412
654
|
html += '</div>';
|
|
413
655
|
html += '<div class="profile-name-display">' + escapeAttr(mateName || 'New Mate') + '</div>';
|
|
414
656
|
html += '</div>';
|
|
@@ -426,9 +668,19 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
|
|
|
426
668
|
html += '<div class="profile-field">';
|
|
427
669
|
html += '<label class="profile-field-label">Avatar <button class="profile-shuffle-btn" title="Shuffle">' + iconHtml('shuffle') + '</button></label>';
|
|
428
670
|
html += '<div class="profile-avatar-grid">';
|
|
671
|
+
// Upload button as first cell
|
|
672
|
+
var mateUploadActive = mateCustom ? ' profile-avatar-option-active' : '';
|
|
673
|
+
html += '<button class="profile-avatar-option profile-avatar-upload' + mateUploadActive + '" title="Upload photo">';
|
|
674
|
+
if (mateCustom) {
|
|
675
|
+
html += '<img src="' + mateCustom + '" alt="Custom" class="profile-avatar-custom-preview">';
|
|
676
|
+
} else {
|
|
677
|
+
html += '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
|
|
678
|
+
}
|
|
679
|
+
html += '</button>';
|
|
680
|
+
html += '<input type="file" id="mate-profile-avatar-file" accept="image/*" style="display:none">';
|
|
429
681
|
for (var j = 0; j < AVATAR_STYLES.length; j++) {
|
|
430
682
|
var st = AVATAR_STYLES[j];
|
|
431
|
-
var activeS = (mateStyle === st.id) ? ' profile-avatar-option-active' : '';
|
|
683
|
+
var activeS = (!mateCustom && mateStyle === st.id) ? ' profile-avatar-option-active' : '';
|
|
432
684
|
html += '<button class="profile-avatar-option' + activeS + '" data-style="' + st.id + '" title="' + st.name + '">';
|
|
433
685
|
html += '<img src="' + avatarUrl(st.id, mateSeed, 40) + '" alt="' + st.name + '">';
|
|
434
686
|
html += '</button>';
|
|
@@ -458,8 +710,25 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
|
|
|
458
710
|
avatarStyle: mateStyle,
|
|
459
711
|
avatarSeed: mateSeed,
|
|
460
712
|
avatarColor: mateColor,
|
|
713
|
+
avatarCustom: mateCustom,
|
|
461
714
|
};
|
|
462
715
|
|
|
716
|
+
function refreshChatAvatars() {
|
|
717
|
+
var newUrl = mateAvatarUrl({ profile: mateProfile, id: mateData.id }, 64);
|
|
718
|
+
// Update all mate avatars in the chat area
|
|
719
|
+
var bubbles = document.querySelectorAll('.dm-bubble-avatar-mate');
|
|
720
|
+
for (var i = 0; i < bubbles.length; i++) {
|
|
721
|
+
bubbles[i].src = newUrl;
|
|
722
|
+
}
|
|
723
|
+
// Update body dataset so new messages use the updated URL
|
|
724
|
+
if (document.body.dataset.mateAvatarUrl) {
|
|
725
|
+
document.body.dataset.mateAvatarUrl = newUrl;
|
|
726
|
+
}
|
|
727
|
+
// Update mate sidebar avatar
|
|
728
|
+
var sidebarAvatar = document.querySelector('.mate-sidebar-avatar');
|
|
729
|
+
if (sidebarAvatar) sidebarAvatar.src = newUrl;
|
|
730
|
+
}
|
|
731
|
+
|
|
463
732
|
function debouncedMateUpdate() {
|
|
464
733
|
if (mateSaveTimer) clearTimeout(mateSaveTimer);
|
|
465
734
|
mateSaveTimer = setTimeout(function() {
|
|
@@ -470,8 +739,10 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
|
|
|
470
739
|
avatarStyle: mateProfile.avatarStyle,
|
|
471
740
|
avatarSeed: mateProfile.avatarSeed,
|
|
472
741
|
avatarColor: mateProfile.avatarColor,
|
|
742
|
+
avatarCustom: mateProfile.avatarCustom,
|
|
473
743
|
},
|
|
474
744
|
});
|
|
745
|
+
refreshChatAvatars();
|
|
475
746
|
mateSaveTimer = null;
|
|
476
747
|
}, 400);
|
|
477
748
|
}
|
|
@@ -480,7 +751,9 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
|
|
|
480
751
|
if (!matePopoverEl) return;
|
|
481
752
|
var img = matePopoverEl.querySelector('.profile-popover-avatar-img');
|
|
482
753
|
var nd = matePopoverEl.querySelector('.profile-name-display');
|
|
483
|
-
if (img) img.src =
|
|
754
|
+
if (img) img.src = mateProfile.avatarCustom
|
|
755
|
+
? mateProfile.avatarCustom
|
|
756
|
+
: avatarUrl(mateProfile.avatarStyle, mateProfile.avatarSeed, 80);
|
|
484
757
|
if (nd) nd.textContent = mateProfile.displayName || 'New Mate';
|
|
485
758
|
}
|
|
486
759
|
|
|
@@ -503,9 +776,60 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
|
|
|
503
776
|
nameInput.addEventListener('keyup', function(e) { e.stopPropagation(); });
|
|
504
777
|
nameInput.addEventListener('keypress', function(e) { e.stopPropagation(); });
|
|
505
778
|
|
|
779
|
+
// Avatar upload button
|
|
780
|
+
var mateUploadBtn = matePopoverEl.querySelector('.profile-avatar-upload');
|
|
781
|
+
var mateFileInput = matePopoverEl.querySelector('#mate-profile-avatar-file');
|
|
782
|
+
if (mateUploadBtn && mateFileInput) {
|
|
783
|
+
mateUploadBtn.addEventListener('click', function() {
|
|
784
|
+
mateFileInput.click();
|
|
785
|
+
});
|
|
786
|
+
mateFileInput.addEventListener('change', function() {
|
|
787
|
+
var file = mateFileInput.files[0];
|
|
788
|
+
if (!file) return;
|
|
789
|
+
|
|
790
|
+
function uploadMateBlob(blob) {
|
|
791
|
+
blob.arrayBuffer().then(function(ab) {
|
|
792
|
+
var buf = new Uint8Array(ab);
|
|
793
|
+
fetch('/api/mate-avatar/' + encodeURIComponent(mateData.id), {
|
|
794
|
+
method: 'POST',
|
|
795
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
796
|
+
body: buf,
|
|
797
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
798
|
+
if (data.ok) {
|
|
799
|
+
mateProfile.avatarCustom = data.avatar;
|
|
800
|
+
updateMatePopoverHeader();
|
|
801
|
+
if (matePopoverEl) {
|
|
802
|
+
matePopoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
|
|
803
|
+
b.classList.remove('profile-avatar-option-active');
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
if (mateUploadBtn) {
|
|
807
|
+
mateUploadBtn.classList.add('profile-avatar-option-active');
|
|
808
|
+
mateUploadBtn.innerHTML = '<img src="' + data.avatar + '" alt="Custom" class="profile-avatar-custom-preview">';
|
|
809
|
+
}
|
|
810
|
+
debouncedMateUpdate();
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Load image and open position picker
|
|
817
|
+
var img = new Image();
|
|
818
|
+
var objectUrl = URL.createObjectURL(file);
|
|
819
|
+
img.onload = function() {
|
|
820
|
+
showAvatarPositioner(img, objectUrl, function(croppedBlob) {
|
|
821
|
+
URL.revokeObjectURL(objectUrl);
|
|
822
|
+
uploadMateBlob(croppedBlob);
|
|
823
|
+
});
|
|
824
|
+
};
|
|
825
|
+
img.src = objectUrl;
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
506
829
|
// Avatar style
|
|
507
830
|
matePopoverEl.querySelectorAll('.profile-avatar-option[data-style]').forEach(function(btn) {
|
|
508
831
|
btn.addEventListener('click', function() {
|
|
832
|
+
mateProfile.avatarCustom = '';
|
|
509
833
|
mateProfile.avatarStyle = btn.dataset.style;
|
|
510
834
|
mateProfile.avatarSeed = matePreviewSeed;
|
|
511
835
|
updateMatePopoverHeader();
|
|
@@ -513,6 +837,9 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
|
|
|
513
837
|
b.classList.remove('profile-avatar-option-active');
|
|
514
838
|
});
|
|
515
839
|
btn.classList.add('profile-avatar-option-active');
|
|
840
|
+
// Reset upload button to + icon
|
|
841
|
+
var upBtn = matePopoverEl.querySelector('.profile-avatar-upload');
|
|
842
|
+
if (upBtn) upBtn.innerHTML = '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
|
|
516
843
|
debouncedMateUpdate();
|
|
517
844
|
});
|
|
518
845
|
});
|
|
@@ -12,16 +12,18 @@ function getShareUrl() {
|
|
|
12
12
|
export function triggerShare() {
|
|
13
13
|
var url = getShareUrl();
|
|
14
14
|
|
|
15
|
-
// Use Web Share API
|
|
16
|
-
|
|
15
|
+
// Use Web Share API on mobile only
|
|
16
|
+
var isMobile = window.innerWidth <= 768 || /Mobi|Android/i.test(navigator.userAgent);
|
|
17
|
+
if (isMobile && navigator.share) {
|
|
17
18
|
navigator.share({ title: document.title || "Clay", url: url }).catch(function () {});
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
//
|
|
22
|
+
// Show QR overlay
|
|
22
23
|
var qrOverlay = document.getElementById("qr-overlay");
|
|
23
24
|
var qrCanvas = document.getElementById("qr-canvas");
|
|
24
25
|
var qrUrl = document.getElementById("qr-url");
|
|
26
|
+
var qrShareBtn = document.getElementById("qr-share-btn");
|
|
25
27
|
|
|
26
28
|
var qr = qrcode(0, "M");
|
|
27
29
|
qr.addData(url);
|
|
@@ -29,6 +31,15 @@ export function triggerShare() {
|
|
|
29
31
|
qrCanvas.innerHTML = qr.createSvgTag(5, 0);
|
|
30
32
|
qrUrl.innerHTML = url + '<span class="qr-hint">click to copy</span>';
|
|
31
33
|
|
|
34
|
+
// Show browser share button if Web Share API is available
|
|
35
|
+
if (qrShareBtn) {
|
|
36
|
+
if (navigator.share) {
|
|
37
|
+
qrShareBtn.classList.remove("hidden");
|
|
38
|
+
} else {
|
|
39
|
+
qrShareBtn.classList.add("hidden");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
qrOverlay.classList.remove("hidden");
|
|
33
44
|
}
|
|
34
45
|
|
|
@@ -58,6 +69,15 @@ export function initQrCode() {
|
|
|
58
69
|
e.stopPropagation();
|
|
59
70
|
});
|
|
60
71
|
|
|
72
|
+
// Browser share button
|
|
73
|
+
var qrShareBtn = document.getElementById("qr-share-btn");
|
|
74
|
+
if (qrShareBtn) {
|
|
75
|
+
qrShareBtn.addEventListener("click", function () {
|
|
76
|
+
var url = getShareUrl();
|
|
77
|
+
navigator.share({ title: document.title || "Clay", url: url }).catch(function () {});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
// ESC to close
|
|
62
82
|
document.addEventListener("keydown", function (e) {
|
|
63
83
|
if (e.key === "Escape" && !qrOverlay.classList.contains("hidden")) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { avatarUrl, userAvatarUrl, mateAvatarUrl } from './avatar.js';
|
|
1
2
|
import { escapeHtml, copyToClipboard } from './utils.js';
|
|
2
3
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
4
|
import { openProjectSettings } from './project-settings.js';
|
|
@@ -572,9 +573,9 @@ export function updateSessionPresence(presence) {
|
|
|
572
573
|
}
|
|
573
574
|
}
|
|
574
575
|
|
|
575
|
-
function presenceAvatarUrl(
|
|
576
|
-
|
|
577
|
-
return
|
|
576
|
+
function presenceAvatarUrl(userOrStyle, seed) {
|
|
577
|
+
if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
|
|
578
|
+
return avatarUrl(userOrStyle || "thumbs", seed, 24);
|
|
578
579
|
}
|
|
579
580
|
|
|
580
581
|
function renderPresenceAvatars(el, sessionId) {
|
|
@@ -594,7 +595,7 @@ function renderPresenceAvatars(el, sessionId) {
|
|
|
594
595
|
var u = users[i];
|
|
595
596
|
var img = document.createElement("img");
|
|
596
597
|
img.className = "session-presence-avatar";
|
|
597
|
-
img.src = presenceAvatarUrl(u
|
|
598
|
+
img.src = presenceAvatarUrl(u);
|
|
598
599
|
img.alt = u.displayName;
|
|
599
600
|
img.dataset.tip = u.displayName + (u.username ? " (@" + u.username + ")" : "");
|
|
600
601
|
if (i > 0) img.style.marginLeft = "-6px";
|
|
@@ -865,7 +866,7 @@ function renderSheetSessions(listEl) {
|
|
|
865
866
|
|
|
866
867
|
var avatarEl = document.createElement("img");
|
|
867
868
|
avatarEl.className = "mobile-chat-chip-avatar";
|
|
868
|
-
avatarEl.src =
|
|
869
|
+
avatarEl.src = mateAvatarUrl(mate, 20);
|
|
869
870
|
avatarEl.alt = mp.displayName || mate.name || "";
|
|
870
871
|
chip.appendChild(avatarEl);
|
|
871
872
|
|
|
@@ -1329,6 +1330,22 @@ function renderSheetSettings(listEl) {
|
|
|
1329
1330
|
});
|
|
1330
1331
|
|
|
1331
1332
|
listEl.appendChild(themeBtn);
|
|
1333
|
+
|
|
1334
|
+
// "Open as app" — only show if not already in PWA standalone mode
|
|
1335
|
+
if (!document.documentElement.classList.contains("pwa-standalone")) {
|
|
1336
|
+
var pwaBtn = document.createElement("button");
|
|
1337
|
+
pwaBtn.className = "mobile-more-item";
|
|
1338
|
+
pwaBtn.innerHTML = '<i data-lucide="smartphone"></i><span class="mobile-more-item-label">Open as app</span>';
|
|
1339
|
+
pwaBtn.addEventListener("click", function () {
|
|
1340
|
+
closeMobileSheet();
|
|
1341
|
+
// Trigger the existing PWA install modal
|
|
1342
|
+
var installPill = document.getElementById("pwa-install-pill");
|
|
1343
|
+
if (installPill) {
|
|
1344
|
+
setTimeout(function () { installPill.click(); }, 250);
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
listEl.appendChild(pwaBtn);
|
|
1348
|
+
}
|
|
1332
1349
|
}
|
|
1333
1350
|
|
|
1334
1351
|
export function initSidebar(_ctx) {
|
|
@@ -2762,7 +2779,7 @@ export function renderSidebarPresence(onlineUsers) {
|
|
|
2762
2779
|
var ou = onlineUsers[i];
|
|
2763
2780
|
var img = document.createElement("img");
|
|
2764
2781
|
img.className = "sidebar-presence-avatar";
|
|
2765
|
-
img.src = presenceAvatarUrl(ou
|
|
2782
|
+
img.src = presenceAvatarUrl(ou);
|
|
2766
2783
|
img.alt = ou.displayName;
|
|
2767
2784
|
img.dataset.tip = ou.displayName + " (@" + ou.username + ")";
|
|
2768
2785
|
container.appendChild(img);
|
|
@@ -3337,7 +3354,7 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
|
|
|
3337
3354
|
|
|
3338
3355
|
var avatar = document.createElement("img");
|
|
3339
3356
|
avatar.className = "icon-strip-user-avatar";
|
|
3340
|
-
avatar.src =
|
|
3357
|
+
avatar.src = userAvatarUrl(u, 34);
|
|
3341
3358
|
avatar.alt = u.displayName;
|
|
3342
3359
|
el.appendChild(avatar);
|
|
3343
3360
|
|
|
@@ -3405,7 +3422,7 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
|
|
|
3405
3422
|
|
|
3406
3423
|
var avatar = document.createElement("img");
|
|
3407
3424
|
avatar.className = "icon-strip-user-avatar";
|
|
3408
|
-
avatar.src =
|
|
3425
|
+
avatar.src = mateAvatarUrl(mate, 34);
|
|
3409
3426
|
avatar.alt = mp.displayName || mate.name || "Mate";
|
|
3410
3427
|
el.appendChild(avatar);
|
|
3411
3428
|
|
|
@@ -3530,7 +3547,7 @@ function toggleDmUserPicker(anchorEl) {
|
|
|
3530
3547
|
|
|
3531
3548
|
var av = document.createElement("img");
|
|
3532
3549
|
av.className = "dm-user-picker-avatar";
|
|
3533
|
-
av.src =
|
|
3550
|
+
av.src = userAvatarUrl(u, 28);
|
|
3534
3551
|
av.alt = u.displayName;
|
|
3535
3552
|
item.appendChild(av);
|
|
3536
3553
|
|