486CHD commited on
Commit
45db04c
·
verified ·
1 Parent(s): 6b97968

Upload 12 files

Browse files
Files changed (3) hide show
  1. index.html +1404 -1240
  2. package-lock.json +943 -0
  3. style.css +1440 -1278
index.html CHANGED
@@ -1,1241 +1,1405 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>Banana Pro AI</title>
7
- <link rel="stylesheet" href="style.css">
8
- <style>
9
- /* 顶部固定布局样式 */
10
- .input-section {
11
- position: fixed !important;
12
- top: 0 !important;
13
- bottom: auto !important;
14
- left: 0 !important;
15
- right: 0 !important;
16
- transform: none !important;
17
- width: 100% !important;
18
- max-width: 100% !important;
19
- border-radius: 0 !important;
20
- z-index: 1000 !important;
21
- box-shadow: 0 2px 20px rgba(0,0,0,0.3) !important;
22
- }
23
-
24
- header {
25
- margin-top: 80px; /* 为输入区留空间 */
26
- }
27
-
28
- .history-container {
29
- padding-bottom: 20px !important;
30
- }
31
-
32
- /* 调整输入区内部布局 */
33
- .control-bar {
34
- max-width: 1200px;
35
- margin: 0 auto;
36
- padding: 12px 20px !important;
37
- }
38
-
39
- .preview-bar {
40
- max-width: 1200px;
41
- margin: 0 auto;
42
- padding: 0 20px;
43
- }
44
-
45
- .preview-bar.visible {
46
- padding: 12px 20px;
47
- }
48
-
49
- /* 动态调整header间距 */
50
- .has-preview header {
51
- margin-top: 180px;
52
- }
53
-
54
- @media (max-width: 768px) {
55
- header {
56
- margin-top: 70px;
57
- }
58
- .has-preview header {
59
- margin-top: 150px;
60
- }
61
- }
62
- </style>
63
- </head>
64
- <body>
65
- <!-- 在预览栏显示时添加 has-preview 类 -->
66
- <script>
67
- // 监听预览栏变化
68
- const observePreviewBar = () => {
69
- const previewBar = document.getElementById('preview-bar');
70
- const observer = new MutationObserver(() => {
71
- if (previewBar.classList.contains('visible')) {
72
- document.body.classList.add('has-preview');
73
- } else {
74
- document.body.classList.remove('has-preview');
75
- }
76
- });
77
- observer.observe(previewBar, { attributes: true, attributeFilter: ['class'] });
78
- };
79
- document.addEventListener('DOMContentLoaded', observePreviewBar);
80
- </script>
81
-
82
- <!-- 登录界面 -->
83
- <div id="login-overlay">
84
- <div class="login-card glass-panel">
85
- <h1 style="font-size: 2rem;">🍌</h1>
86
- <h3 style="margin: 10px 0; color: #cbd5e1;">Banana Pro</h3>
87
- <input type="password" id="pwd" placeholder="Password" onkeypress="if(event.key==='Enter')doLogin()">
88
- <button class="btn-3d" onclick="doLogin()" style="width: 100%; padding: 10px;">Unlock</button>
89
- </div>
90
- </div>
91
-
92
- <!-- 主应用 -->
93
- <div class="app-container" id="app" style="filter: blur(10px); pointer-events: none;">
94
- <!-- 顶部固定输入区 -->
95
- <div class="input-section glass-panel" id="drop-zone">
96
- <!-- 上部分:预览条 -->
97
- <div class="preview-bar" id="preview-bar"></div>
98
-
99
- <!-- 下部分:操作栏 -->
100
- <div class="control-bar">
101
- <button class="upload-trigger" id="upload-btn" title="上传参考图">📷</button>
102
- <textarea id="prompt" class="main-input" rows="1" placeholder="描述画面... (支持拖拽图片)"></textarea>
103
- <button class="btn-3d send-btn" id="send-btn">
104
- <span>生成</span>
105
- <div class="loader"></div>
106
- </button>
107
- </div>
108
- </div>
109
-
110
- <header>
111
- <div>
112
- <h2>创意画板 提示词用法库https://nanobananamaker.com/</h2>
113
- <div style="font-size: 12px; color: var(--text-sub);" id="status-bar">Ready 生成较慢,放后台等待即可</div>
114
- </div>
115
- </header>
116
-
117
- <!-- 历史画廊 -->
118
- <div class="history-container">
119
- <div class="grid-layout" id="gallery"></div>
120
-
121
- <section class="public-gallery-section glass-panel">
122
- <div class="section-title">
123
- <div>
124
- <h3>✨ 社区创意画廊</h3>
125
- <p class="section-subtitle" id="public-gallery-hint">分享你的作品,欣赏社区灵感</p>
126
- </div>
127
- <div class="section-actions">
128
- <button class="icon-btn" id="refresh-public-gallery" title="刷新公共画廊" aria-label="刷新公共画廊">⟳</button>
129
- </div>
130
- </div>
131
- <div class="grid-layout public-grid" id="public-gallery"></div>
132
- </section>
133
- </div>
134
-
135
- <input type="file" id="file-input" multiple accept="image/*" hidden>
136
- </div>
137
-
138
- <!-- 详情弹窗 -->
139
- <div class="modal" id="modal">
140
- <div class="modal-content glass-panel">
141
- <button class="close-modal" onclick="closeModal()">×</button>
142
- <div class="modal-img-area">
143
- <img id="m-img" src="">
144
- </div>
145
- <div class="modal-footer">
146
- <div class="input-refs" id="m-refs"></div>
147
- <div class="prompt-display" id="m-prompt"></div>
148
- <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;">
149
- 🎨 复用参数与图片
150
- </button>
151
- </div>
152
- </div>
153
- </div>
154
-
155
- <script>
156
- // ============================================
157
- // 全局状态管理
158
- // ============================================
159
- const AppState = {
160
- db: null,
161
- currentImages: [], // 当前上传的图片 Base64 数组
162
- galleryData: [], // 个人画廊数据缓存
163
- publicGalleryData: [], // 公共画廊数据缓存
164
- currentModalItem: null // 当前弹窗显示的项目
165
- };
166
-
167
- const STORAGE_KEYS = {
168
- publicGalleryTokens: 'BananaPro_PublicGallery_Tokens_v1'
169
- };
170
-
171
- const DEFAULT_PUBLIC_HINT = '分享你的作品,欣赏社区灵感';
172
-
173
- const DB_NAME = 'BananaProDB_v3';
174
- const DB_VERSION = 1;
175
-
176
- const STORE_NAME = 'artworks';
177
-
178
- // ============================================
179
- // IndexedDB 模块(使用 Blob 存储避免 Base64 问题)
180
- // ============================================
181
- const Database = {
182
- async init() {
183
- return new Promise((resolve, reject) => {
184
- const request = indexedDB.open(DB_NAME, DB_VERSION);
185
-
186
- request.onerror = () => reject(request.error);
187
-
188
- request.onsuccess = () => {
189
- AppState.db = request.result;
190
- resolve();
191
- };
192
-
193
- request.onupgradeneeded = (event) => {
194
- const db = event.target.result;
195
- if (!db.objectStoreNames.contains(STORE_NAME)) {
196
- const store = db.createObjectStore(STORE_NAME, {
197
- keyPath: 'id',
198
- autoIncrement: true
199
- });
200
- store.createIndex('timestamp', 'timestamp', { unique: false });
201
- }
202
- };
203
- });
204
- },
205
-
206
- async save(item) {
207
- return new Promise((resolve, reject) => {
208
- const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
209
- const store = tx.objectStore(STORE_NAME);
210
-
211
- // 直接存储完整对象,不做任何转换
212
- const record = {
213
- prompt: item.prompt,
214
- image: item.image, // 完整的 data:image/... 字符串
215
- inputImages: item.inputImages || [],
216
- timestamp: Date.now()
217
- };
218
-
219
- const request = store.add(record);
220
- request.onsuccess = () => resolve(request.result);
221
- request.onerror = () => reject(request.error);
222
- });
223
- },
224
-
225
- async getAll() {
226
- return new Promise((resolve, reject) => {
227
- const tx = AppState.db.transaction([STORE_NAME], 'readonly');
228
- const store = tx.objectStore(STORE_NAME);
229
- const request = store.getAll();
230
-
231
- request.onsuccess = () => {
232
- // 按时间戳倒序
233
- const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
234
- resolve(results);
235
- };
236
- request.onerror = () => reject(request.error);
237
- });
238
- },
239
-
240
- async delete(id) {
241
- return new Promise((resolve, reject) => {
242
- const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
243
- const store = tx.objectStore(STORE_NAME);
244
- const request = store.delete(id);
245
- request.onsuccess = () => resolve();
246
- request.onerror = () => reject(request.error);
247
- });
248
- },
249
-
250
- async getById(id) {
251
- return new Promise((resolve, reject) => {
252
- const tx = AppState.db.transaction([STORE_NAME], 'readonly');
253
- const store = tx.objectStore(STORE_NAME);
254
- const request = store.get(id);
255
- request.onsuccess = () => resolve(request.result);
256
- request.onerror = () => reject(request.error);
257
- });
258
- }
259
- };
260
-
261
- // ============================================
262
- // 图片处理模块
263
- // ============================================
264
- const ImageHandler = {
265
- // 文件转 Base64
266
- fileToBase64(file) {
267
- return new Promise((resolve, reject) => {
268
- const reader = new FileReader();
269
- reader.onload = () => resolve(reader.result);
270
- reader.onerror = () => reject(reader.error);
271
- reader.readAsDataURL(file);
272
- });
273
- },
274
-
275
- // 压缩图片(手机端优化)
276
- async compressImage(base64Data, maxWidth = 1280) {
277
- return new Promise((resolve) => {
278
- const img = new Image();
279
- img.onload = () => {
280
- if (img.width <= maxWidth && base64Data.length < 500000) {
281
- resolve(base64Data);
282
- return;
283
- }
284
-
285
- const canvas = document.createElement('canvas');
286
- const ctx = canvas.getContext('2d');
287
- const scale = Math.min(1, maxWidth / img.width);
288
- canvas.width = img.width * scale;
289
- canvas.height = img.height * scale;
290
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
291
-
292
- resolve(canvas.toDataURL('image/jpeg', 0.8));
293
- };
294
- img.onerror = () => resolve(base64Data);
295
- img.src = base64Data;
296
- });
297
- },
298
-
299
- // 处理上传的文件
300
- async processFiles(files) {
301
- const maxImages = 16;
302
- const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
303
-
304
- for (const file of imageFiles) {
305
- if (AppState.currentImages.length >= maxImages) {
306
- alert(`最多只能上传 ${maxImages} 张图片`);
307
- break;
308
- }
309
-
310
- try {
311
- let base64 = await this.fileToBase64(file);
312
- base64 = await this.compressImage(base64);
313
- AppState.currentImages.push(base64);
314
- } catch (err) {
315
- console.error('图片读取失败:', err);
316
- }
317
- }
318
-
319
- PreviewManager.render();
320
- },
321
-
322
- // 移除指定索引的图片
323
- removeAt(index) {
324
- AppState.currentImages.splice(index, 1);
325
- PreviewManager.render();
326
- },
327
-
328
- // 清空所有上传的图片
329
- clear() {
330
- AppState.currentImages = [];
331
- PreviewManager.render();
332
- },
333
-
334
- // 设置图片(用于复用功能)
335
- setImages(images) {
336
- AppState.currentImages = images ? [...images] : [];
337
- PreviewManager.render();
338
- }
339
- };
340
-
341
- // ============================================
342
- // 预览条管理模块
343
- // ============================================
344
- const PreviewManager = {
345
- container: null,
346
- uploadBtn: null,
347
- statusBar: null,
348
-
349
- init() {
350
- this.container = document.getElementById('preview-bar');
351
- this.uploadBtn = document.getElementById('upload-btn');
352
- this.statusBar = document.getElementById('status-bar');
353
- },
354
-
355
- render() {
356
- // 清空容器
357
- this.container.innerHTML = '';
358
-
359
- const images = AppState.currentImages;
360
-
361
- if (images.length === 0) {
362
- this.container.classList.remove('visible');
363
- this.uploadBtn.classList.remove('active');
364
- this.statusBar.textContent = 'Ready';
365
- return;
366
- }
367
-
368
- this.container.classList.add('visible');
369
- this.uploadBtn.classList.add('active');
370
- this.statusBar.textContent = `已选择 ${images.length}/16 张图片`;
371
-
372
- // 使用 DOM API 创建元素,避免 innerHTML 导致的编码问题
373
- images.forEach((imgData, index) => {
374
- const wrapper = document.createElement('div');
375
- wrapper.className = 'thumb-wrapper';
376
-
377
- const img = document.createElement('img');
378
- img.src = imgData; // 直接设置 src,不经过字符串拼接
379
-
380
- const removeBtn = document.createElement('div');
381
- removeBtn.className = 'thumb-remove';
382
- removeBtn.textContent = '×';
383
- removeBtn.onclick = (e) => {
384
- e.stopPropagation();
385
- ImageHandler.removeAt(index);
386
- };
387
-
388
- wrapper.appendChild(img);
389
- wrapper.appendChild(removeBtn);
390
- this.container.appendChild(wrapper);
391
- });
392
- }
393
- };
394
-
395
- // ============================================
396
- // 画廊管理模块
397
- // ============================================
398
- const GalleryManager = {
399
- container: null,
400
-
401
- init() {
402
- this.container = document.getElementById('gallery');
403
- },
404
-
405
- async load() {
406
- try {
407
- AppState.galleryData = await Database.getAll();
408
- this.render();
409
- } catch (err) {
410
- console.error('加载画廊失败:', err);
411
- }
412
- },
413
-
414
- render() {
415
- this.container.innerHTML = '';
416
-
417
- if (AppState.galleryData.length === 0) {
418
- this.container.innerHTML = `
419
- <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
420
- <div style="font-size: 48px; margin-bottom: 10px;">🎨</div>
421
- <div>暂无作品,开始创作吧!</div>
422
- </div>
423
- `;
424
- return;
425
- }
426
-
427
- const fragment = document.createDocumentFragment();
428
- AppState.galleryData.forEach(item => {
429
- const card = this.createCard(item);
430
- fragment.appendChild(card);
431
- });
432
- this.container.appendChild(fragment);
433
- },
434
-
435
- createCard(item) {
436
- const el = document.createElement('div');
437
- el.className = 'history-item';
438
- el.dataset.id = item.id;
439
-
440
- // 参考图标记
441
- if (item.inputImages && item.inputImages.length > 0) {
442
- const badge = document.createElement('div');
443
- badge.className = 'item-badge';
444
- badge.textContent = `📎 ${item.inputImages.length}`;
445
- el.appendChild(badge);
446
- }
447
-
448
- // 主图片 - 使用 DOM API 设置 src
449
- const img = document.createElement('img');
450
- img.loading = 'lazy';
451
- img.decoding = 'async';
452
- img.src = item.image;
453
- img.alt = `作品 ${item.id}`;
454
- img.onerror = () => {
455
- console.error('图片加载失败, ID:', item.id);
456
- img.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect fill="%23333" width="100" height="100"/><text fill="%23666" x="50%" y="50%" text-anchor="middle" dy=".3em">Error</text></svg>';
457
- };
458
- el.appendChild(img);
459
-
460
- // 操作按钮
461
- const actions = document.createElement('div');
462
- actions.className = 'item-actions';
463
-
464
- const shareBtn = document.createElement('button');
465
- shareBtn.className = 'icon-btn share-btn';
466
- shareBtn.textContent = '🌐';
467
- shareBtn.title = '发布到公共画廊';
468
- shareBtn.setAttribute('aria-label', '发布到公共画廊');
469
- shareBtn.onclick = (e) => {
470
- e.stopPropagation();
471
- PublicGalleryManager.share(item, shareBtn);
472
- };
473
-
474
- const downloadBtn = document.createElement('button');
475
- downloadBtn.className = 'icon-btn';
476
- downloadBtn.textContent = '⬇';
477
- downloadBtn.title = '下载图片';
478
- downloadBtn.onclick = (e) => {
479
- e.stopPropagation();
480
- this.downloadImage(item);
481
- };
482
-
483
- const deleteBtn = document.createElement('button');
484
- deleteBtn.className = 'icon-btn';
485
- deleteBtn.style.background = 'rgba(239,68,68,0.8)';
486
- deleteBtn.textContent = '🗑';
487
- deleteBtn.title = '删除图片';
488
- deleteBtn.onclick = (e) => {
489
- e.stopPropagation();
490
- this.deleteItem(item.id);
491
- };
492
-
493
- actions.appendChild(shareBtn);
494
- actions.appendChild(downloadBtn);
495
- actions.appendChild(deleteBtn);
496
- el.appendChild(actions);
497
-
498
- // 点击打开弹窗
499
- el.onclick = () => ModalManager.open(item);
500
-
501
- return el;
502
- },
503
-
504
- downloadImage(item) {
505
- const link = document.createElement('a');
506
- link.href = item.image;
507
- link.download = `banana-pro-${item.id}-${Date.now()}.png`;
508
- document.body.appendChild(link);
509
- link.click();
510
- document.body.removeChild(link);
511
- },
512
-
513
- async deleteItem(id) {
514
- if (!confirm('确定要删除这张图片吗?')) return;
515
-
516
- try {
517
- await Database.delete(id);
518
- await this.load();
519
- } catch (err) {
520
- console.error('删除失败:', err);
521
- alert('删除失败');
522
- }
523
- }
524
- };
525
-
526
- // ============================================
527
- // 弹窗管理模块
528
- // ============================================
529
- const ModalManager = {
530
- modal: null,
531
- imgEl: null,
532
- promptEl: null,
533
- refsEl: null,
534
- reuseBtn: null,
535
-
536
- init() {
537
- this.modal = document.getElementById('modal');
538
- this.imgEl = document.getElementById('m-img');
539
- this.promptEl = document.getElementById('m-prompt');
540
- this.refsEl = document.getElementById('m-refs');
541
- this.reuseBtn = document.getElementById('m-reuse');
542
-
543
- // 点击背景关闭
544
- this.modal.onclick = (e) => {
545
- if (e.target === this.modal) this.close();
546
- };
547
-
548
- // ESC 关闭
549
- document.addEventListener('keydown', (e) => {
550
- if (e.key === 'Escape') this.close();
551
- });
552
- },
553
-
554
- open(item) {
555
- AppState.currentModalItem = item;
556
-
557
- // 设置主图 - 直接赋值
558
- this.imgEl.src = item.image;
559
-
560
- // 设置提示词
561
- this.promptEl.textContent = item.prompt;
562
-
563
- // 渲染参考图
564
- this.refsEl.innerHTML = '';
565
- if (item.inputImages && item.inputImages.length > 0) {
566
- item.inputImages.forEach(imgData => {
567
- const thumb = document.createElement('img');
568
- thumb.className = 'ref-thumb';
569
- thumb.src = imgData; // 直接赋值
570
- thumb.onclick = () => window.open(imgData, '_blank');
571
- this.refsEl.appendChild(thumb);
572
- });
573
- }
574
-
575
- // 绑定复用按钮
576
- this.reuseBtn.onclick = () => this.reuse();
577
-
578
- this.modal.style.display = 'flex';
579
- },
580
-
581
- close() {
582
- this.modal.style.display = 'none';
583
- AppState.currentModalItem = null;
584
- },
585
-
586
- reuse() {
587
- const item = AppState.currentModalItem;
588
- if (!item) return;
589
-
590
- // 复用提示词
591
- const textarea = document.getElementById('prompt');
592
- textarea.value = item.prompt;
593
- textarea.style.height = 'auto';
594
- textarea.style.height = textarea.scrollHeight + 'px';
595
-
596
- // 复用参考图
597
- if (item.inputImages && item.inputImages.length > 0) {
598
- ImageHandler.setImages(item.inputImages);
599
- } else {
600
- ImageHandler.clear();
601
- }
602
-
603
- this.close();
604
- textarea.focus();
605
- }
606
- };
607
-
608
- // ============================================
609
- // 公共画廊管理模块
610
- // ============================================
611
- const PublicGalleryManager = {
612
- container: null,
613
- refreshBtn: null,
614
- hintEl: null,
615
- tokens: {},
616
- isLoading: false,
617
- defaultHint: DEFAULT_PUBLIC_HINT,
618
- hintTimer: null,
619
- syncTimer: null,
620
- syncInterval: 0,
621
- loadTimeout: null,
622
- lastFetchTime: 0,
623
- minFetchInterval: 5000,
624
-
625
- init() {
626
- this.container = document.getElementById('public-gallery');
627
- this.refreshBtn = document.getElementById('refresh-public-gallery');
628
- this.hintEl = document.getElementById('public-gallery-hint');
629
- this.tokens = this.loadTokens();
630
- if (this.hintEl && this.hintEl.textContent) {
631
- this.defaultHint = this.hintEl.textContent;
632
- }
633
- this.setHint(this.defaultHint);
634
-
635
- if (this.refreshBtn) {
636
- this.refreshBtn.onclick = () => this.fetch();
637
- }
638
-
639
- this.initialFetch();
640
- },
641
-
642
- async initialFetch() {
643
- await this.fetch();
644
- },
645
-
646
- startRealtimeSync() {
647
- // 实时同步已禁用 - 改为手动刷新提高性能
648
- },
649
-
650
- stopRealtimeSync() {
651
- if (this.syncTimer) {
652
- clearInterval(this.syncTimer);
653
- this.syncTimer = null;
654
- }
655
- },
656
-
657
- loadTokens() {
658
- try {
659
- const raw = localStorage.getItem(STORAGE_KEYS.publicGalleryTokens);
660
- return raw ? JSON.parse(raw) : {};
661
- } catch (error) {
662
- console.warn('无法读取公共画廊令牌:', error);
663
- return {};
664
- }
665
- },
666
-
667
- saveTokens() {
668
- try {
669
- localStorage.setItem(STORAGE_KEYS.publicGalleryTokens, JSON.stringify(this.tokens));
670
- } catch (error) {
671
- console.warn('无法保存公共画廊令牌:', error);
672
- }
673
- },
674
-
675
- setHint(message, mode = 'default') {
676
- if (!this.hintEl) return;
677
- this.hintEl.textContent = message;
678
- this.hintEl.classList.remove('is-error', 'is-success');
679
- if (mode === 'error') {
680
- this.hintEl.classList.add('is-error');
681
- } else if (mode === 'success') {
682
- this.hintEl.classList.add('is-success');
683
- }
684
- clearTimeout(this.hintTimer);
685
- if (mode !== 'default') {
686
- this.hintTimer = setTimeout(() => {
687
- this.setHint(this.defaultHint);
688
- }, 4000);
689
- }
690
- },
691
-
692
- setLoading(loading) {
693
- this.isLoading = loading;
694
- if (this.refreshBtn) {
695
- this.refreshBtn.classList.toggle('spinning', loading);
696
- this.refreshBtn.disabled = loading;
697
- }
698
- // 防止加载超时 - 30秒后强制停止
699
- if (loading) {
700
- clearTimeout(this.loadTimeout);
701
- this.loadTimeout = setTimeout(() => {
702
- if (this.isLoading) {
703
- this.setLoading(false);
704
- this.setHint('加载超时,请稍后重试', 'error');
705
- }
706
- }, 30000);
707
- } else {
708
- clearTimeout(this.loadTimeout);
709
- }
710
- },
711
-
712
- async fetch() {
713
- if (!this.container) return;
714
-
715
- // 防止过于频繁的请求
716
- const now = Date.now();
717
- if (now - this.lastFetchTime < this.minFetchInterval) {
718
- return;
719
- }
720
- this.lastFetchTime = now;
721
-
722
- this.setLoading(true);
723
- try {
724
- const controller = new AbortController();
725
- const timeoutId = setTimeout(() => controller.abort(), 15000);
726
-
727
- const res = await fetch('/api/public-gallery', {
728
- signal: controller.signal
729
- });
730
- clearTimeout(timeoutId);
731
- const data = await res.json();
732
-
733
- if (!res.ok || !data.success) {
734
- throw new Error(data.message || '无法加载公共画廊');
735
- }
736
-
737
- AppState.publicGalleryData = data.items || [];
738
- this.render();
739
- this.setHint(this.defaultHint);
740
- } catch (error) {
741
- console.error('公共画廊加载失败:', error);
742
-
743
- let errorMessage = '加载失败,请稍后重试';
744
- if (error.name === 'AbortError') {
745
- errorMessage = '加载超时,请检查网络';
746
- this.setHint('加载超时,请检查网络', 'error');
747
- } else {
748
- this.setHint(errorMessage, 'error');
749
- }
750
-
751
- if (AppState.publicGalleryData.length === 0) {
752
- this.showEmpty(errorMessage);
753
- }
754
- } finally {
755
- this.setLoading(false);
756
- }
757
- },
758
-
759
- showEmpty(message) {
760
- if (!this.container) return;
761
- this.container.innerHTML = `
762
- <div class="public-gallery-empty">
763
- <div>🌌</div>
764
- <p>${message}</p>
765
- </div>
766
- `;
767
- },
768
-
769
- render() {
770
- if (!this.container) return;
771
-
772
- if (!AppState.publicGalleryData || AppState.publicGalleryData.length === 0) {
773
- this.showEmpty('还没有人分享作品,成为第一个吧!');
774
- return;
775
- }
776
-
777
- this.container.innerHTML = '';
778
- const fragment = document.createDocumentFragment();
779
- AppState.publicGalleryData.forEach(item => {
780
- const card = this.createCard(item);
781
- fragment.appendChild(card);
782
- });
783
- this.container.appendChild(fragment);
784
- },
785
-
786
- createCard(item) {
787
- const card = document.createElement('div');
788
- card.className = 'public-card';
789
- card.dataset.id = item.id;
790
-
791
- const img = document.createElement('img');
792
- img.loading = 'lazy';
793
- img.decoding = 'async';
794
- img.src = item.image;
795
- img.alt = item.prompt || '创意作品';
796
- card.appendChild(img);
797
-
798
- const info = document.createElement('div');
799
- info.className = 'public-card-info';
800
-
801
- const promptText = document.createElement('p');
802
- promptText.className = 'public-card-prompt';
803
- promptText.textContent = item.prompt || '未提供提示词';
804
- promptText.title = item.prompt;
805
- info.appendChild(promptText);
806
-
807
- const footer = document.createElement('div');
808
- footer.className = 'public-card-footer';
809
-
810
- const timeText = document.createElement('span');
811
- timeText.textContent = this.formatTimestamp(item.timestamp);
812
- footer.appendChild(timeText);
813
-
814
- if (this.canDelete(item.id)) {
815
- const deleteBtn = document.createElement('button');
816
- deleteBtn.className = 'icon-btn small public-delete-btn';
817
- deleteBtn.textContent = '🗑';
818
- deleteBtn.title = '删除这张作品';
819
- deleteBtn.onclick = (e) => {
820
- e.stopPropagation();
821
- this.delete(item.id);
822
- };
823
- footer.appendChild(deleteBtn);
824
- }
825
-
826
- info.appendChild(footer);
827
- card.appendChild(info);
828
-
829
- card.onclick = () => ModalManager.open(item);
830
-
831
- return card;
832
- },
833
-
834
- formatTimestamp(timestamp) {
835
- if (!timestamp) return '';
836
- const date = new Date(timestamp);
837
- if (Number.isNaN(date.getTime())) {
838
- return '';
839
- }
840
- return date.toLocaleString();
841
- },
842
-
843
- canDelete(id) {
844
- return true;
845
- },
846
-
847
- markOwned(id, token) {
848
- if (!id || !token) return;
849
- this.tokens[id] = token;
850
- this.saveTokens();
851
- },
852
-
853
- removeOwnership(id) {
854
- if (this.tokens[id]) {
855
- delete this.tokens[id];
856
- this.saveTokens();
857
- }
858
- },
859
-
860
- async share(item, triggerBtn) {
861
- if (!item) return;
862
-
863
- const confirmShare = confirm('确认将这张作品发布到公共画廊?提示词将对所有人可见。');
864
- if (!confirmShare) {
865
- return;
866
- }
867
-
868
- if (triggerBtn) {
869
- triggerBtn.disabled = true;
870
- }
871
-
872
- try {
873
- const res = await fetch('/api/public-gallery', {
874
- method: 'POST',
875
- headers: { 'Content-Type': 'application/json' },
876
- body: JSON.stringify({
877
- prompt: item.prompt,
878
- image: item.image,
879
- inputImages: item.inputImages || []
880
- })
881
- });
882
-
883
- const data = await res.json();
884
-
885
- if (!res.ok || !data.success) {
886
- throw new Error(data.message || '发布失败');
887
- }
888
-
889
- AppState.publicGalleryData = [data.item, ...(AppState.publicGalleryData || [])];
890
- this.markOwned(data.item.id, data.deleteToken);
891
- this.render();
892
- this.setHint('作品已发布至公共画廊', 'success');
893
- } catch (error) {
894
- console.error('发布到公共画廊失败:', error);
895
- alert('发布失败: ' + error.message);
896
- this.setHint('发布失败,请稍后重试', 'error');
897
- } finally {
898
- if (triggerBtn) {
899
- triggerBtn.disabled = false;
900
- }
901
- }
902
- },
903
-
904
- async delete(id) {
905
- const confirmDelete = confirm('确定要删除这张公共画廊作品吗?');
906
- if (!confirmDelete) {
907
- return;
908
- }
909
-
910
- try {
911
- const res = await fetch(`/api/public-gallery/${id}`, {
912
- method: 'DELETE',
913
- headers: { 'Content-Type': 'application/json' }
914
- });
915
-
916
- const data = await res.json();
917
-
918
- if (!res.ok || !data.success) {
919
- throw new Error(data.message || '删除失败');
920
- }
921
-
922
- AppState.publicGalleryData = AppState.publicGalleryData.filter(item => item.id !== id);
923
- this.removeOwnership(id);
924
- this.render();
925
- this.setHint('作品已删除', 'success');
926
- } catch (error) {
927
- console.error('删除公共画廊作品失败:', error);
928
- alert('删除失败: ' + error.message);
929
- this.setHint('删除失败,请稍后再试', 'error');
930
- }
931
- }
932
- };
933
-
934
- // ============================================
935
- // 生成请求模块
936
- // ============================================
937
- const Generator = {
938
- sendBtn: null,
939
- textarea: null,
940
- statusBar: null,
941
- isGenerating: false,
942
- generateTimeout: null,
943
-
944
- init() {
945
- this.sendBtn = document.getElementById('send-btn');
946
- this.textarea = document.getElementById('prompt');
947
- this.statusBar = document.getElementById('status-bar');
948
-
949
- this.sendBtn.onclick = () => this.generate();
950
-
951
- // 输入框自动高度
952
- this.textarea.addEventListener('input', () => {
953
- this.textarea.style.height = 'auto';
954
- this.textarea.style.height = this.textarea.scrollHeight + 'px';
955
- });
956
-
957
- // 回车发送(Shift+Enter 换行)
958
- this.textarea.addEventListener('keydown', (e) => {
959
- if (e.key === 'Enter' && !e.shiftKey) {
960
- e.preventDefault();
961
- this.generate();
962
- }
963
- });
964
- },
965
-
966
- setLoading(loading) {
967
- this.isGenerating = loading;
968
- if (loading) {
969
- this.sendBtn.classList.add('loading');
970
- this.sendBtn.disabled = true;
971
- this.statusBar.textContent = '⏳ 生成中...';
972
-
973
- // 防止生成超时 - 3分钟后强制停止
974
- clearTimeout(this.generateTimeout);
975
- this.generateTimeout = setTimeout(() => {
976
- if (this.isGenerating) {
977
- this.setLoading(false);
978
- this.statusBar.textContent = '生成超时,请重试';
979
- alert('生成超时,请检查网络后重试');
980
- }
981
- }, 180000);
982
- } else {
983
- this.sendBtn.classList.remove('loading');
984
- this.sendBtn.disabled = false;
985
- this.statusBar.textContent = 'Ready';
986
- clearTimeout(this.generateTimeout);
987
- }
988
- },
989
-
990
- async generate() {
991
- if (this.isGenerating) return;
992
-
993
- const prompt = this.textarea.value.trim();
994
- if (!prompt) {
995
- alert('请输入提示词');
996
- return;
997
- }
998
-
999
- this.setLoading(true);
1000
-
1001
- try {
1002
- const controller = new AbortController();
1003
- const timeoutId = setTimeout(() => controller.abort(), 120000);
1004
-
1005
- const response = await fetch('/api/generate', {
1006
- method: 'POST',
1007
- headers: { 'Content-Type': 'application/json' },
1008
- body: JSON.stringify({
1009
- prompt: prompt,
1010
- images: AppState.currentImages
1011
- }),
1012
- signal: controller.signal
1013
- });
1014
- clearTimeout(timeoutId);
1015
-
1016
- const data = await response.json();
1017
-
1018
- if (!response.ok || !data.success) {
1019
- throw new Error(data.message || '生成失败');
1020
- }
1021
-
1022
- // 验证返回的图片数据
1023
- if (!data.image || !data.image.startsWith('data:image')) {
1024
- throw new Error('返回的图片数据无效');
1025
- }
1026
-
1027
- // 保存到数据库
1028
- await Database.save({
1029
- prompt: prompt,
1030
- image: data.image,
1031
- inputImages: [...AppState.currentImages]
1032
- });
1033
-
1034
- // 刷新画廊
1035
- await GalleryManager.load();
1036
-
1037
- // 清空输入
1038
- this.textarea.value = '';
1039
- this.textarea.style.height = 'auto';
1040
- ImageHandler.clear();
1041
- this.statusBar.textContent = '✅ 生成成功';
1042
-
1043
- } catch (err) {
1044
- console.error('生成失败:', err);
1045
-
1046
- let errorMessage = '生成失败,请重试';
1047
- if (err.message.includes('API认证失败')) {
1048
- errorMessage = 'API认证失败,请联系管理员检查API配置';
1049
- } else if (err.message.includes('无法连接到API服务器')) {
1050
- errorMessage = '无法连接到API服务器,请检查网络连接';
1051
- } else if (err.message.includes('API请求超时')) {
1052
- errorMessage = 'API请求超时,请稍后重试';
1053
- } else if (err.message) {
1054
- errorMessage = err.message;
1055
- }
1056
-
1057
- if (err.name === 'AbortError') {
1058
- this.statusBar.textContent = '⚠️ 生成超时';
1059
- alert('生成超时,请检查网络后重试');
1060
- } else {
1061
- this.statusBar.textContent = '❌ 生成失败';
1062
- alert(errorMessage);
1063
- }
1064
- } finally {
1065
- this.setLoading(false);
1066
- setTimeout(() => {
1067
- if (this.statusBar.textContent !== 'Ready') {
1068
- this.statusBar.textContent = 'Ready';
1069
- }
1070
- }, 3000);
1071
- }
1072
- }
1073
- };
1074
-
1075
- // ============================================
1076
- // 认证模块
1077
- // ============================================
1078
- const Auth = {
1079
- async check() {
1080
- try {
1081
- const res = await fetch('/api/check-auth');
1082
- const data = await res.json();
1083
- return data.authenticated;
1084
- } catch {
1085
- return false;
1086
- }
1087
- },
1088
-
1089
- async login(password) {
1090
- const res = await fetch('/api/login', {
1091
- method: 'POST',
1092
- headers: { 'Content-Type': 'application/json' },
1093
- body: JSON.stringify({ password })
1094
- });
1095
- const data = await res.json();
1096
- return data.success;
1097
- },
1098
-
1099
- unlock() {
1100
- document.getElementById('login-overlay').style.display = 'none';
1101
- const app = document.getElementById('app');
1102
- app.style.filter = 'none';
1103
- app.style.pointerEvents = 'all';
1104
- }
1105
- };
1106
-
1107
- // ============================================
1108
- // 拖拽上传模块
1109
- // ============================================
1110
- const DragDrop = {
1111
- dropZone: null,
1112
-
1113
- init() {
1114
- this.dropZone = document.getElementById('drop-zone');
1115
-
1116
- // 阻止默认行为
1117
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
1118
- this.dropZone.addEventListener(event, (e) => {
1119
- e.preventDefault();
1120
- e.stopPropagation();
1121
- });
1122
- });
1123
-
1124
- // 拖入高亮
1125
- ['dragenter', 'dragover'].forEach(event => {
1126
- this.dropZone.addEventListener(event, () => {
1127
- this.dropZone.style.borderColor = 'var(--accent-color)';
1128
- this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
1129
- });
1130
- });
1131
-
1132
- // 拖出恢复
1133
- ['dragleave', 'drop'].forEach(event => {
1134
- this.dropZone.addEventListener(event, () => {
1135
- this.dropZone.style.borderColor = '';
1136
- this.dropZone.style.background = '';
1137
- });
1138
- });
1139
-
1140
- // 放下处理
1141
- this.dropZone.addEventListener('drop', async (e) => {
1142
- const files = e.dataTransfer.files;
1143
- if (files.length > 0) {
1144
- await ImageHandler.processFiles(files);
1145
- }
1146
- });
1147
- }
1148
- };
1149
-
1150
- // ============================================
1151
- // 文件选择模块
1152
- // ============================================
1153
- const FileSelector = {
1154
- input: null,
1155
- btn: null,
1156
-
1157
- init() {
1158
- this.input = document.getElementById('file-input');
1159
- this.btn = document.getElementById('upload-btn');
1160
-
1161
- this.btn.onclick = () => this.input.click();
1162
-
1163
- this.input.onchange = async () => {
1164
- if (this.input.files.length > 0) {
1165
- await ImageHandler.processFiles(this.input.files);
1166
- }
1167
- this.input.value = ''; // 重置以便重复选择
1168
- };
1169
- }
1170
- };
1171
-
1172
- // ============================================
1173
- // 全局函数(供 HTML onclick 调用)
1174
- // ============================================
1175
- async function loadWorkspaceData() {
1176
- try {
1177
- await Promise.all([
1178
- GalleryManager.load(),
1179
- PublicGalleryManager.fetch()
1180
- ]);
1181
- } catch (error) {
1182
- console.error('加载画廊数据失败:', error);
1183
- }
1184
- }
1185
-
1186
- async function doLogin() {
1187
- const pwd = document.getElementById('pwd').value;
1188
- if (!pwd) return;
1189
-
1190
- const success = await Auth.login(pwd);
1191
- if (success) {
1192
- Auth.unlock();
1193
- await loadWorkspaceData();
1194
- } else {
1195
- alert('密码错误');
1196
- }
1197
- }
1198
-
1199
- function closeModal() {
1200
- ModalManager.close();
1201
- }
1202
-
1203
- // ============================================
1204
- // 应用初始化
1205
- // ============================================
1206
- async function initApp() {
1207
- try {
1208
- // 初始化数据库
1209
- await Database.init();
1210
-
1211
- // 初始化各模块
1212
- PreviewManager.init();
1213
- GalleryManager.init();
1214
- ModalManager.init();
1215
- PublicGalleryManager.init();
1216
- Generator.init();
1217
- DragDrop.init();
1218
- FileSelector.init();
1219
-
1220
- // 检查认证状态
1221
- const isAuth = await Auth.check();
1222
- if (isAuth) {
1223
- Auth.unlock();
1224
- await loadWorkspaceData();
1225
- }
1226
-
1227
- console.log('App initialized successfully');
1228
- } catch (err) {
1229
- console.error('App initialization failed:', err);
1230
- }
1231
- }
1232
-
1233
- // 启动应用
1234
- if (document.readyState === 'loading') {
1235
- document.addEventListener('DOMContentLoaded', initApp);
1236
- } else {
1237
- initApp();
1238
- }
1239
- </script>
1240
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1241
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Banana Pro AI</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <style>
9
+ /* 顶部固定布局样式 */
10
+ .input-section {
11
+ position: fixed !important;
12
+ top: 0 !important;
13
+ bottom: auto !important;
14
+ left: 0 !important;
15
+ right: 0 !important;
16
+ transform: none !important;
17
+ width: 100% !important;
18
+ max-width: 100% !important;
19
+ border-radius: 0 !important;
20
+ z-index: 1000 !important;
21
+ box-shadow: 0 2px 20px rgba(0,0,0,0.3) !important;
22
+ }
23
+
24
+ header {
25
+ margin-top: 80px; /* 为输入区留空间 */
26
+ }
27
+
28
+ .history-container {
29
+ padding-bottom: 20px !important;
30
+ }
31
+
32
+ /* 调整输入区内部布局 */
33
+ .control-bar {
34
+ max-width: 1200px;
35
+ margin: 0 auto;
36
+ padding: 12px 20px !important;
37
+ }
38
+
39
+ .preview-bar {
40
+ max-width: 1200px;
41
+ margin: 0 auto;
42
+ padding: 0 20px;
43
+ }
44
+
45
+ .preview-bar.visible {
46
+ padding: 12px 20px;
47
+ }
48
+
49
+ /* 动态调整header间距 */
50
+ .has-preview header {
51
+ margin-top: 180px;
52
+ }
53
+
54
+ @media (max-width: 768px) {
55
+ header {
56
+ margin-top: 70px;
57
+ }
58
+ .has-preview header {
59
+ margin-top: 150px;
60
+ }
61
+ }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <!-- 在预览栏显示时添加 has-preview 类 -->
66
+ <script>
67
+ // 监听预览栏变化
68
+ const observePreviewBar = () => {
69
+ const previewBar = document.getElementById('preview-bar');
70
+ const observer = new MutationObserver(() => {
71
+ if (previewBar.classList.contains('visible')) {
72
+ document.body.classList.add('has-preview');
73
+ } else {
74
+ document.body.classList.remove('has-preview');
75
+ }
76
+ });
77
+ observer.observe(previewBar, { attributes: true, attributeFilter: ['class'] });
78
+ };
79
+ document.addEventListener('DOMContentLoaded', observePreviewBar);
80
+ </script>
81
+
82
+ <!-- 登录界面 -->
83
+ <div id="login-overlay">
84
+ <div class="login-card glass-panel">
85
+ <h1 style="font-size: 2rem;">🍌</h1>
86
+ <h3 style="margin: 10px 0; color: #cbd5e1;">Banana Pro</h3>
87
+ <input type="password" id="pwd" placeholder="Password" onkeypress="if(event.key==='Enter')doLogin()">
88
+ <button class="btn-3d" onclick="doLogin()" style="width: 100%; padding: 10px;">Unlock</button>
89
+ </div>
90
+ </div>
91
+
92
+ <!-- 主应用 -->
93
+ <div class="app-container" id="app" style="filter: blur(10px); pointer-events: none;">
94
+ <!-- 顶部固定输入区 -->
95
+ <div class="input-section glass-panel" id="drop-zone">
96
+ <!-- 上部分:预览条 -->
97
+ <div class="preview-bar" id="preview-bar"></div>
98
+
99
+ <!-- 下部分:操作栏 -->
100
+ <div class="control-bar">
101
+ <button class="upload-trigger" id="upload-btn" title="上传参考图">📷</button>
102
+ <textarea id="prompt" class="main-input" rows="1" placeholder="描述画面... (支持拖拽图片)"></textarea>
103
+ <button class="ghost-btn" id="clear-btn" title="清空输入" aria-label="清空输入">清空</button>
104
+ <button class="btn-3d send-btn" id="send-btn">
105
+ <span>生成</span>
106
+ <div class="loader"></div>
107
+ </button>
108
+ </div>
109
+ </div>
110
+
111
+ <header>
112
+ <div class="header-stack">
113
+ <div class="title-row">
114
+ <h2>创意画板</h2>
115
+ <a class="prompt-link" href="https://nanobananamaker.com/" target="_blank" rel="noopener">提示词用法库</a>
116
+ </div>
117
+ <div class="status-bar" id="status-bar" aria-live="polite">
118
+ <span class="status-text" id="status-text">Ready 生成较慢,放后台等待即可</span>
119
+ <div class="status-meter" id="status-meter" aria-hidden="true">
120
+ <div class="status-meter-fill" id="status-meter-fill"></div>
121
+ </div>
122
+ <span class="status-timer" id="status-timer"></span>
123
+ </div>
124
+ </div>
125
+ </header>
126
+
127
+ <!-- 历史画廊 -->
128
+ <div class="history-container">
129
+ <div class="grid-layout" id="gallery"></div>
130
+
131
+ <section class="public-gallery-section glass-panel">
132
+ <div class="section-title">
133
+ <div>
134
+ <h3>✨ 社区创意画廊</h3>
135
+ <p class="section-subtitle" id="public-gallery-hint">分享你的作品,欣赏社区灵感</p>
136
+ </div>
137
+ <div class="section-actions">
138
+ <button class="icon-btn" id="refresh-public-gallery" title="刷新公共画廊" aria-label="刷新公共画廊">⟳</button>
139
+ </div>
140
+ </div>
141
+ <div class="grid-layout public-grid" id="public-gallery"></div>
142
+ </section>
143
+ </div>
144
+
145
+ <input type="file" id="file-input" multiple accept="image/*" hidden>
146
+ </div>
147
+
148
+ <!-- 详情弹窗 -->
149
+ <div class="modal" id="modal">
150
+ <div class="modal-content glass-panel">
151
+ <button class="close-modal" onclick="closeModal()">×</button>
152
+ <div class="modal-img-area">
153
+ <img id="m-img" src="">
154
+ </div>
155
+ <div class="modal-footer">
156
+ <div class="input-refs" id="m-refs"></div>
157
+ <div class="prompt-display" id="m-prompt"></div>
158
+ <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;">
159
+ 🎨 复用参数与图片
160
+ </button>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <script>
166
+ // ============================================
167
+ // 全局状态管理
168
+ // ============================================
169
+ const AppState = {
170
+ db: null,
171
+ currentImages: [], // 当前上传的图片 Base64 数组
172
+ galleryData: [], // 个人画廊数据缓存
173
+ publicGalleryData: [], // 公共画廊数据缓存
174
+ currentModalItem: null // 当前弹窗显示的项目
175
+ };
176
+
177
+ const STORAGE_KEYS = {
178
+ publicGalleryTokens: 'BananaPro_PublicGallery_Tokens_v1'
179
+ };
180
+
181
+ const DEFAULT_PUBLIC_HINT = '分享你的作品,欣赏社区灵感';
182
+
183
+ const DB_NAME = 'BananaProDB_v3';
184
+ const DB_VERSION = 1;
185
+
186
+ const STORE_NAME = 'artworks';
187
+
188
+ // ============================================
189
+ // IndexedDB 模块(使用 Blob 存储避免 Base64 问题)
190
+ // ============================================
191
+ const Database = {
192
+ async init() {
193
+ return new Promise((resolve, reject) => {
194
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
195
+
196
+ request.onerror = () => reject(request.error);
197
+
198
+ request.onsuccess = () => {
199
+ AppState.db = request.result;
200
+ resolve();
201
+ };
202
+
203
+ request.onupgradeneeded = (event) => {
204
+ const db = event.target.result;
205
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
206
+ const store = db.createObjectStore(STORE_NAME, {
207
+ keyPath: 'id',
208
+ autoIncrement: true
209
+ });
210
+ store.createIndex('timestamp', 'timestamp', { unique: false });
211
+ }
212
+ };
213
+ });
214
+ },
215
+
216
+ async save(item) {
217
+ return new Promise((resolve, reject) => {
218
+ const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
219
+ const store = tx.objectStore(STORE_NAME);
220
+
221
+ // 直接存储完整对象,不做任何转换
222
+ const record = {
223
+ prompt: item.prompt,
224
+ image: item.image, // 完整的 data:image/... 字符串
225
+ inputImages: item.inputImages || [],
226
+ timestamp: Date.now()
227
+ };
228
+
229
+ const request = store.add(record);
230
+ request.onsuccess = () => resolve(request.result);
231
+ request.onerror = () => reject(request.error);
232
+ });
233
+ },
234
+
235
+ async getAll() {
236
+ return new Promise((resolve, reject) => {
237
+ const tx = AppState.db.transaction([STORE_NAME], 'readonly');
238
+ const store = tx.objectStore(STORE_NAME);
239
+ const request = store.getAll();
240
+
241
+ request.onsuccess = () => {
242
+ // 按时间戳倒序
243
+ const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
244
+ resolve(results);
245
+ };
246
+ request.onerror = () => reject(request.error);
247
+ });
248
+ },
249
+
250
+ async delete(id) {
251
+ return new Promise((resolve, reject) => {
252
+ const tx = AppState.db.transaction([STORE_NAME], 'readwrite');
253
+ const store = tx.objectStore(STORE_NAME);
254
+ const request = store.delete(id);
255
+ request.onsuccess = () => resolve();
256
+ request.onerror = () => reject(request.error);
257
+ });
258
+ },
259
+
260
+ async getById(id) {
261
+ return new Promise((resolve, reject) => {
262
+ const tx = AppState.db.transaction([STORE_NAME], 'readonly');
263
+ const store = tx.objectStore(STORE_NAME);
264
+ const request = store.get(id);
265
+ request.onsuccess = () => resolve(request.result);
266
+ request.onerror = () => reject(request.error);
267
+ });
268
+ }
269
+ };
270
+
271
+ // ============================================
272
+ // ��片处理模块
273
+ // ============================================
274
+ const ImageHandler = {
275
+ // 文件转 Base64
276
+ fileToBase64(file) {
277
+ return new Promise((resolve, reject) => {
278
+ const reader = new FileReader();
279
+ reader.onload = () => resolve(reader.result);
280
+ reader.onerror = () => reject(reader.error);
281
+ reader.readAsDataURL(file);
282
+ });
283
+ },
284
+
285
+ // 压缩图片(手机端优化)
286
+ async compressImage(base64Data, maxWidth = 1280) {
287
+ return new Promise((resolve) => {
288
+ const img = new Image();
289
+ img.onload = () => {
290
+ if (img.width <= maxWidth && base64Data.length < 500000) {
291
+ resolve(base64Data);
292
+ return;
293
+ }
294
+
295
+ const canvas = document.createElement('canvas');
296
+ const ctx = canvas.getContext('2d');
297
+ const scale = Math.min(1, maxWidth / img.width);
298
+ canvas.width = img.width * scale;
299
+ canvas.height = img.height * scale;
300
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
301
+
302
+ resolve(canvas.toDataURL('image/jpeg', 0.8));
303
+ };
304
+ img.onerror = () => resolve(base64Data);
305
+ img.src = base64Data;
306
+ });
307
+ },
308
+
309
+ // 处理上传的文件
310
+ async processFiles(files) {
311
+ const maxImages = 16;
312
+ const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
313
+
314
+ for (const file of imageFiles) {
315
+ if (AppState.currentImages.length >= maxImages) {
316
+ alert(`最多只能上传 ${maxImages} 张图片`);
317
+ break;
318
+ }
319
+
320
+ try {
321
+ let base64 = await this.fileToBase64(file);
322
+ base64 = await this.compressImage(base64);
323
+ AppState.currentImages.push(base64);
324
+ } catch (err) {
325
+ console.error('图片读取失败:', err);
326
+ }
327
+ }
328
+
329
+ PreviewManager.render();
330
+ },
331
+
332
+ // 移除指定索引的图片
333
+ removeAt(index) {
334
+ AppState.currentImages.splice(index, 1);
335
+ PreviewManager.render();
336
+ },
337
+
338
+ // 清空所有上传的图片
339
+ clear() {
340
+ AppState.currentImages = [];
341
+ PreviewManager.render();
342
+ },
343
+
344
+ // 设置图片(用于复用功能)
345
+ setImages(images) {
346
+ AppState.currentImages = images ? [...images] : [];
347
+ PreviewManager.render();
348
+ }
349
+ };
350
+
351
+ // ============================================
352
+ // 预览条管理模块
353
+ // ============================================
354
+ const PreviewManager = {
355
+ container: null,
356
+ uploadBtn: null,
357
+
358
+ init() {
359
+ this.container = document.getElementById('preview-bar');
360
+ this.uploadBtn = document.getElementById('upload-btn');
361
+ },
362
+
363
+ render() {
364
+ this.container.innerHTML = '';
365
+
366
+ const images = AppState.currentImages;
367
+
368
+ if (images.length === 0) {
369
+ this.container.classList.remove('visible');
370
+ this.uploadBtn.classList.remove('active');
371
+ StatusBar.resetToContext();
372
+ return;
373
+ }
374
+
375
+ this.container.classList.add('visible');
376
+ this.uploadBtn.classList.add('active');
377
+ StatusBar.setText("\u5df2\u9009\u62e9 " + images.length + '/16 ' + "\u5f20\u56fe\u7247");
378
+
379
+ images.forEach((imgData, index) => {
380
+ const wrapper = document.createElement('div');
381
+ wrapper.className = 'thumb-wrapper';
382
+
383
+ const img = document.createElement('img');
384
+ img.src = imgData;
385
+ img.decoding = 'async';
386
+
387
+ const removeBtn = document.createElement('div');
388
+ removeBtn.className = 'thumb-remove';
389
+ removeBtn.textContent = '?';
390
+ removeBtn.onclick = (e) => {
391
+ e.stopPropagation();
392
+ ImageHandler.removeAt(index);
393
+ };
394
+
395
+ wrapper.appendChild(img);
396
+ wrapper.appendChild(removeBtn);
397
+ this.container.appendChild(wrapper);
398
+ });
399
+ }
400
+ };
401
+
402
+ wrapper.appendChild(img);
403
+ wrapper.appendChild(removeBtn);
404
+ this.container.appendChild(wrapper);
405
+ });
406
+ }
407
+ };
408
+
409
+ // ============================================
410
+ // Status bar
411
+ // ============================================
412
+ const StatusBar = {
413
+ container: null,
414
+ textEl: null,
415
+ meterEl: null,
416
+ meterFill: null,
417
+ timerEl: null,
418
+ defaultText: "Ready \u751f\u6210\u8f83\u6162\uff0c\u653e\u540e\u53f0\u7b49\u5f85\u5373\u53ef",
419
+ locked: false,
420
+ countdownTimer: null,
421
+ flashTimer: null,
422
+ countdownTotal: 60,
423
+
424
+ init() {
425
+ this.container = document.getElementById('status-bar');
426
+ this.textEl = document.getElementById('status-text');
427
+ this.meterEl = document.getElementById('status-meter');
428
+ this.meterFill = document.getElementById('status-meter-fill');
429
+ this.timerEl = document.getElementById('status-timer');
430
+
431
+ if (this.textEl && this.textEl.textContent) {
432
+ this.defaultText = this.textEl.textContent.trim();
433
+ }
434
+
435
+ this.resetMeter();
436
+ },
437
+
438
+ resetMeter() {
439
+ if (this.meterFill) {
440
+ this.meterFill.style.width = '0%';
441
+ }
442
+ if (this.timerEl) {
443
+ this.timerEl.textContent = '';
444
+ }
445
+ if (this.container) {
446
+ this.container.classList.remove('is-generating');
447
+ }
448
+ },
449
+
450
+ lock() {
451
+ this.locked = true;
452
+ },
453
+
454
+ unlock() {
455
+ this.locked = false;
456
+ },
457
+
458
+ setText(text, options = {}) {
459
+ const { force = false } = options;
460
+ if (this.locked && !force) {
461
+ return;
462
+ }
463
+ if (this.textEl) {
464
+ this.textEl.textContent = text;
465
+ }
466
+ },
467
+
468
+ resetToContext() {
469
+ if (this.locked) {
470
+ return;
471
+ }
472
+ if (AppState.currentImages.length > 0) {
473
+ this.setText("\u5df2\u9009\u62e9 " + AppState.currentImages.length + '/16 ' + "\u5f20\u56fe\u7247");
474
+ } else {
475
+ this.setText(this.defaultText);
476
+ }
477
+ },
478
+
479
+ startCountdown(totalSeconds = 60) {
480
+ this.countdownTotal = totalSeconds;
481
+ clearInterval(this.countdownTimer);
482
+ clearTimeout(this.flashTimer);
483
+ this.lock();
484
+
485
+ if (this.container) {
486
+ this.container.classList.add('is-generating');
487
+ }
488
+
489
+ const startTime = Date.now();
490
+ const endTime = startTime + totalSeconds * 1000;
491
+
492
+ const tick = () => {
493
+ const now = Date.now();
494
+ const remainingMs = endTime - now;
495
+ const remaining = Math.max(0, Math.ceil(remainingMs / 1000));
496
+ const progress = Math.min(1, (totalSeconds - remaining) / totalSeconds);
497
+
498
+ if (this.meterFill) {
499
+ this.meterFill.style.width = `${progress * 100}%`;
500
+ }
501
+ if (this.timerEl) {
502
+ this.timerEl.textContent = remaining > 0 ? `${remaining}s` : '60s+';
503
+ }
504
+
505
+ const text = remaining > 0
506
+ ? "\u751f\u6210\u4e2d\uff0c\u9884\u8ba1\u7b49\u5f85 " + remaining + 's'
507
+ : "\u751f\u6210\u4e2d\uff0c\u7b49\u5f85\u7ed3\u679c...";
508
+
509
+ if (this.textEl) {
510
+ this.textEl.textContent = text;
511
+ }
512
+ };
513
+
514
+ tick();
515
+ this.countdownTimer = setInterval(tick, 1000);
516
+ },
517
+
518
+ stopCountdown() {
519
+ clearInterval(this.countdownTimer);
520
+ this.countdownTimer = null;
521
+ this.unlock();
522
+ this.resetMeter();
523
+ },
524
+
525
+ flash(text, duration = 3000) {
526
+ this.setText(text, { force: true });
527
+ clearTimeout(this.flashTimer);
528
+ this.flashTimer = setTimeout(() => {
529
+ this.resetToContext();
530
+ }, duration);
531
+ }
532
+ };
533
+
534
+ // ============================================
535
+ // 画廊管理模块
536
+ // ============================================
537
+ const GalleryManager = {
538
+ container: null,
539
+
540
+ init() {
541
+ this.container = document.getElementById('gallery');
542
+ },
543
+
544
+ async load() {
545
+ try {
546
+ AppState.galleryData = await Database.getAll();
547
+ this.render();
548
+ } catch (err) {
549
+ console.error('加载画廊失败:', err);
550
+ }
551
+ },
552
+
553
+ render() {
554
+ this.container.innerHTML = '';
555
+
556
+ if (AppState.galleryData.length === 0) {
557
+ this.container.innerHTML = `
558
+ <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
559
+ <div style="font-size: 48px; margin-bottom: 10px;">🎨</div>
560
+ <div>暂无作品,开始创作吧!</div>
561
+ </div>
562
+ `;
563
+ return;
564
+ }
565
+
566
+ const fragment = document.createDocumentFragment();
567
+ AppState.galleryData.forEach(item => {
568
+ const card = this.createCard(item);
569
+ fragment.appendChild(card);
570
+ });
571
+ this.container.appendChild(fragment);
572
+ },
573
+
574
+ createCard(item) {
575
+ const el = document.createElement('div');
576
+ el.className = 'history-item';
577
+ el.dataset.id = item.id;
578
+
579
+ // 参考图标记
580
+ if (item.inputImages && item.inputImages.length > 0) {
581
+ const badge = document.createElement('div');
582
+ badge.className = 'item-badge';
583
+ badge.textContent = `📎 ${item.inputImages.length}`;
584
+ el.appendChild(badge);
585
+ }
586
+
587
+ // 主图片 - 使用 DOM API 设置 src
588
+ const img = document.createElement('img');
589
+ img.loading = 'lazy';
590
+ img.decoding = 'async';
591
+ img.src = item.image;
592
+ img.alt = `作品 ${item.id}`;
593
+ img.onerror = () => {
594
+ console.error('图片加载失败, ID:', item.id);
595
+ img.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect fill="%23333" width="100" height="100"/><text fill="%23666" x="50%" y="50%" text-anchor="middle" dy=".3em">Error</text></svg>';
596
+ };
597
+ el.appendChild(img);
598
+
599
+ // 操作按钮
600
+ const actions = document.createElement('div');
601
+ actions.className = 'item-actions';
602
+
603
+ const shareBtn = document.createElement('button');
604
+ shareBtn.className = 'icon-btn share-btn';
605
+ shareBtn.textContent = '🌐';
606
+ shareBtn.title = '发布到公共画廊';
607
+ shareBtn.setAttribute('aria-label', '发布到公共画廊');
608
+ shareBtn.onclick = (e) => {
609
+ e.stopPropagation();
610
+ PublicGalleryManager.share(item, shareBtn);
611
+ };
612
+
613
+ const downloadBtn = document.createElement('button');
614
+ downloadBtn.className = 'icon-btn';
615
+ downloadBtn.textContent = '⬇';
616
+ downloadBtn.title = '下载图片';
617
+ downloadBtn.onclick = (e) => {
618
+ e.stopPropagation();
619
+ this.downloadImage(item);
620
+ };
621
+
622
+ const deleteBtn = document.createElement('button');
623
+ deleteBtn.className = 'icon-btn';
624
+ deleteBtn.style.background = 'rgba(239,68,68,0.8)';
625
+ deleteBtn.textContent = '🗑';
626
+ deleteBtn.title = '删除图片';
627
+ deleteBtn.onclick = (e) => {
628
+ e.stopPropagation();
629
+ this.deleteItem(item.id);
630
+ };
631
+
632
+ actions.appendChild(shareBtn);
633
+ actions.appendChild(downloadBtn);
634
+ actions.appendChild(deleteBtn);
635
+ el.appendChild(actions);
636
+
637
+ // 点击打开弹窗
638
+ el.onclick = () => ModalManager.open(item);
639
+
640
+ return el;
641
+ },
642
+
643
+ downloadImage(item) {
644
+ const link = document.createElement('a');
645
+ link.href = item.image;
646
+ link.download = `banana-pro-${item.id}-${Date.now()}.png`;
647
+ document.body.appendChild(link);
648
+ link.click();
649
+ document.body.removeChild(link);
650
+ },
651
+
652
+ async deleteItem(id) {
653
+ if (!confirm('确定要删除这张图片吗?')) return;
654
+
655
+ try {
656
+ await Database.delete(id);
657
+ await this.load();
658
+ } catch (err) {
659
+ console.error('删除失败:', err);
660
+ alert('删除失败');
661
+ }
662
+ }
663
+ };
664
+
665
+ // ============================================
666
+ // 弹窗管理模块
667
+ // ============================================
668
+ const ModalManager = {
669
+ modal: null,
670
+ imgEl: null,
671
+ promptEl: null,
672
+ refsEl: null,
673
+ reuseBtn: null,
674
+
675
+ init() {
676
+ this.modal = document.getElementById('modal');
677
+ this.imgEl = document.getElementById('m-img');
678
+ this.promptEl = document.getElementById('m-prompt');
679
+ this.refsEl = document.getElementById('m-refs');
680
+ this.reuseBtn = document.getElementById('m-reuse');
681
+
682
+ // 点击背景关闭
683
+ this.modal.onclick = (e) => {
684
+ if (e.target === this.modal) this.close();
685
+ };
686
+
687
+ // ESC 关闭
688
+ document.addEventListener('keydown', (e) => {
689
+ if (e.key === 'Escape') this.close();
690
+ });
691
+ },
692
+
693
+ open(item) {
694
+ AppState.currentModalItem = item;
695
+
696
+ // 设置主图 - 直接赋值
697
+ this.imgEl.src = item.image;
698
+
699
+ // 设置提示词
700
+ this.promptEl.textContent = item.prompt;
701
+
702
+ // 渲染参考图
703
+ this.refsEl.innerHTML = '';
704
+ if (item.inputImages && item.inputImages.length > 0) {
705
+ item.inputImages.forEach(imgData => {
706
+ const thumb = document.createElement('img');
707
+ thumb.className = 'ref-thumb';
708
+ thumb.src = imgData; // 直接赋值
709
+ thumb.onclick = () => window.open(imgData, '_blank');
710
+ this.refsEl.appendChild(thumb);
711
+ });
712
+ }
713
+
714
+ // 绑定复用按钮
715
+ this.reuseBtn.onclick = () => this.reuse();
716
+
717
+ this.modal.style.display = 'flex';
718
+ },
719
+
720
+ close() {
721
+ this.modal.style.display = 'none';
722
+ AppState.currentModalItem = null;
723
+ },
724
+
725
+ reuse() {
726
+ const item = AppState.currentModalItem;
727
+ if (!item) return;
728
+
729
+ // 复用提示词
730
+ const textarea = document.getElementById('prompt');
731
+ textarea.value = item.prompt;
732
+ textarea.style.height = 'auto';
733
+ textarea.style.height = textarea.scrollHeight + 'px';
734
+
735
+ // 复用参考图
736
+ if (item.inputImages && item.inputImages.length > 0) {
737
+ ImageHandler.setImages(item.inputImages);
738
+ } else {
739
+ ImageHandler.clear();
740
+ }
741
+
742
+ this.close();
743
+ textarea.focus();
744
+ }
745
+ };
746
+
747
+ // ============================================
748
+ // 公共画廊管理模块
749
+ // ============================================
750
+ const PublicGalleryManager = {
751
+ container: null,
752
+ refreshBtn: null,
753
+ hintEl: null,
754
+ tokens: {},
755
+ isLoading: false,
756
+ defaultHint: DEFAULT_PUBLIC_HINT,
757
+ hintTimer: null,
758
+ syncTimer: null,
759
+ syncInterval: 0,
760
+ loadTimeout: null,
761
+ lastFetchTime: 0,
762
+ minFetchInterval: 5000,
763
+
764
+ init() {
765
+ this.container = document.getElementById('public-gallery');
766
+ this.refreshBtn = document.getElementById('refresh-public-gallery');
767
+ this.hintEl = document.getElementById('public-gallery-hint');
768
+ this.tokens = this.loadTokens();
769
+ if (this.hintEl && this.hintEl.textContent) {
770
+ this.defaultHint = this.hintEl.textContent;
771
+ }
772
+ this.setHint(this.defaultHint);
773
+
774
+ if (this.refreshBtn) {
775
+ this.refreshBtn.onclick = () => this.fetch();
776
+ }
777
+
778
+ this.initialFetch();
779
+ },
780
+
781
+ async initialFetch() {
782
+ await this.fetch();
783
+ },
784
+
785
+ startRealtimeSync() {
786
+ // 实时同步已禁用 - 改为手动刷新提高性能
787
+ },
788
+
789
+ stopRealtimeSync() {
790
+ if (this.syncTimer) {
791
+ clearInterval(this.syncTimer);
792
+ this.syncTimer = null;
793
+ }
794
+ },
795
+
796
+ loadTokens() {
797
+ try {
798
+ const raw = localStorage.getItem(STORAGE_KEYS.publicGalleryTokens);
799
+ return raw ? JSON.parse(raw) : {};
800
+ } catch (error) {
801
+ console.warn('无法读取公共画廊令牌:', error);
802
+ return {};
803
+ }
804
+ },
805
+
806
+ saveTokens() {
807
+ try {
808
+ localStorage.setItem(STORAGE_KEYS.publicGalleryTokens, JSON.stringify(this.tokens));
809
+ } catch (error) {
810
+ console.warn('无法保存公共画廊令牌:', error);
811
+ }
812
+ },
813
+
814
+ setHint(message, mode = 'default') {
815
+ if (!this.hintEl) return;
816
+ this.hintEl.textContent = message;
817
+ this.hintEl.classList.remove('is-error', 'is-success');
818
+ if (mode === 'error') {
819
+ this.hintEl.classList.add('is-error');
820
+ } else if (mode === 'success') {
821
+ this.hintEl.classList.add('is-success');
822
+ }
823
+ clearTimeout(this.hintTimer);
824
+ if (mode !== 'default') {
825
+ this.hintTimer = setTimeout(() => {
826
+ this.setHint(this.defaultHint);
827
+ }, 4000);
828
+ }
829
+ },
830
+
831
+ setLoading(loading) {
832
+ this.isLoading = loading;
833
+ if (this.refreshBtn) {
834
+ this.refreshBtn.classList.toggle('spinning', loading);
835
+ this.refreshBtn.disabled = loading;
836
+ }
837
+ // 防止加载超时 - 30秒后强制停止
838
+ if (loading) {
839
+ clearTimeout(this.loadTimeout);
840
+ this.loadTimeout = setTimeout(() => {
841
+ if (this.isLoading) {
842
+ this.setLoading(false);
843
+ this.setHint('加载超时,请稍后重试', 'error');
844
+ }
845
+ }, 30000);
846
+ } else {
847
+ clearTimeout(this.loadTimeout);
848
+ }
849
+ },
850
+
851
+ async fetch() {
852
+ if (!this.container) return;
853
+
854
+ // 防止过于频繁的请求
855
+ const now = Date.now();
856
+ if (now - this.lastFetchTime < this.minFetchInterval) {
857
+ return;
858
+ }
859
+ this.lastFetchTime = now;
860
+
861
+ this.setLoading(true);
862
+ try {
863
+ const controller = new AbortController();
864
+ const timeoutId = setTimeout(() => controller.abort(), 15000);
865
+
866
+ const res = await fetch('/api/public-gallery', {
867
+ signal: controller.signal
868
+ });
869
+ clearTimeout(timeoutId);
870
+ const data = await res.json();
871
+
872
+ if (!res.ok || !data.success) {
873
+ throw new Error(data.message || '无法加载公共画廊');
874
+ }
875
+
876
+ AppState.publicGalleryData = data.items || [];
877
+ this.render();
878
+ this.setHint(this.defaultHint);
879
+ } catch (error) {
880
+ console.error('公共画廊加载失败:', error);
881
+
882
+ let errorMessage = '加载失败,请稍后重试';
883
+ if (error.name === 'AbortError') {
884
+ errorMessage = '加载超时,请检查网络';
885
+ this.setHint('加载超时,请检查网络', 'error');
886
+ } else {
887
+ this.setHint(errorMessage, 'error');
888
+ }
889
+
890
+ if (AppState.publicGalleryData.length === 0) {
891
+ this.showEmpty(errorMessage);
892
+ }
893
+ } finally {
894
+ this.setLoading(false);
895
+ }
896
+ },
897
+
898
+ showEmpty(message) {
899
+ if (!this.container) return;
900
+ this.container.innerHTML = `
901
+ <div class="public-gallery-empty">
902
+ <div>🌌</div>
903
+ <p>${message}</p>
904
+ </div>
905
+ `;
906
+ },
907
+
908
+ render() {
909
+ if (!this.container) return;
910
+
911
+ if (!AppState.publicGalleryData || AppState.publicGalleryData.length === 0) {
912
+ this.showEmpty('还没有人分享作品,成为第一个吧!');
913
+ return;
914
+ }
915
+
916
+ this.container.innerHTML = '';
917
+ const fragment = document.createDocumentFragment();
918
+ AppState.publicGalleryData.forEach(item => {
919
+ const card = this.createCard(item);
920
+ fragment.appendChild(card);
921
+ });
922
+ this.container.appendChild(fragment);
923
+ },
924
+
925
+ createCard(item) {
926
+ const card = document.createElement('div');
927
+ card.className = 'public-card';
928
+ card.dataset.id = item.id;
929
+
930
+ const img = document.createElement('img');
931
+ img.loading = 'lazy';
932
+ img.decoding = 'async';
933
+ img.src = item.image;
934
+ img.alt = item.prompt || '创意作品';
935
+ card.appendChild(img);
936
+
937
+ const info = document.createElement('div');
938
+ info.className = 'public-card-info';
939
+
940
+ const promptText = document.createElement('p');
941
+ promptText.className = 'public-card-prompt';
942
+ promptText.textContent = item.prompt || '未提供提示词';
943
+ promptText.title = item.prompt;
944
+ info.appendChild(promptText);
945
+
946
+ const footer = document.createElement('div');
947
+ footer.className = 'public-card-footer';
948
+
949
+ const timeText = document.createElement('span');
950
+ timeText.textContent = this.formatTimestamp(item.timestamp);
951
+ footer.appendChild(timeText);
952
+
953
+ if (this.canDelete(item.id)) {
954
+ const deleteBtn = document.createElement('button');
955
+ deleteBtn.className = 'icon-btn small public-delete-btn';
956
+ deleteBtn.textContent = '🗑';
957
+ deleteBtn.title = '删除这张作品';
958
+ deleteBtn.onclick = (e) => {
959
+ e.stopPropagation();
960
+ this.delete(item.id);
961
+ };
962
+ footer.appendChild(deleteBtn);
963
+ }
964
+
965
+ info.appendChild(footer);
966
+ card.appendChild(info);
967
+
968
+ card.onclick = () => ModalManager.open(item);
969
+
970
+ return card;
971
+ },
972
+
973
+ formatTimestamp(timestamp) {
974
+ if (!timestamp) return '';
975
+ const date = new Date(timestamp);
976
+ if (Number.isNaN(date.getTime())) {
977
+ return '';
978
+ }
979
+ return date.toLocaleString();
980
+ },
981
+
982
+ canDelete(id) {
983
+ return true;
984
+ },
985
+
986
+ markOwned(id, token) {
987
+ if (!id || !token) return;
988
+ this.tokens[id] = token;
989
+ this.saveTokens();
990
+ },
991
+
992
+ removeOwnership(id) {
993
+ if (this.tokens[id]) {
994
+ delete this.tokens[id];
995
+ this.saveTokens();
996
+ }
997
+ },
998
+
999
+ async share(item, triggerBtn) {
1000
+ if (!item) return;
1001
+
1002
+ const confirmShare = confirm('确认将这张作品发布到公共画廊?提示词将对所有人可见。');
1003
+ if (!confirmShare) {
1004
+ return;
1005
+ }
1006
+
1007
+ if (triggerBtn) {
1008
+ triggerBtn.disabled = true;
1009
+ }
1010
+
1011
+ try {
1012
+ const res = await fetch('/api/public-gallery', {
1013
+ method: 'POST',
1014
+ headers: { 'Content-Type': 'application/json' },
1015
+ body: JSON.stringify({
1016
+ prompt: item.prompt,
1017
+ image: item.image,
1018
+ inputImages: item.inputImages || []
1019
+ })
1020
+ });
1021
+
1022
+ const data = await res.json();
1023
+
1024
+ if (!res.ok || !data.success) {
1025
+ throw new Error(data.message || '发布失败');
1026
+ }
1027
+
1028
+ AppState.publicGalleryData = [data.item, ...(AppState.publicGalleryData || [])];
1029
+ this.markOwned(data.item.id, data.deleteToken);
1030
+ this.render();
1031
+ this.setHint('作品已发布至公共画廊', 'success');
1032
+ } catch (error) {
1033
+ console.error('发布到公共画廊失败:', error);
1034
+ alert('发布失败: ' + error.message);
1035
+ this.setHint('发布失败,请稍后重试', 'error');
1036
+ } finally {
1037
+ if (triggerBtn) {
1038
+ triggerBtn.disabled = false;
1039
+ }
1040
+ }
1041
+ },
1042
+
1043
+ async delete(id) {
1044
+ const confirmDelete = confirm('确定要删除这张公共画廊作品吗?');
1045
+ if (!confirmDelete) {
1046
+ return;
1047
+ }
1048
+
1049
+ try {
1050
+ const res = await fetch(`/api/public-gallery/${id}`, {
1051
+ method: 'DELETE',
1052
+ headers: { 'Content-Type': 'application/json' }
1053
+ });
1054
+
1055
+ const data = await res.json();
1056
+
1057
+ if (!res.ok || !data.success) {
1058
+ throw new Error(data.message || '删除失败');
1059
+ }
1060
+
1061
+ AppState.publicGalleryData = AppState.publicGalleryData.filter(item => item.id !== id);
1062
+ this.removeOwnership(id);
1063
+ this.render();
1064
+ this.setHint('作品已删除', 'success');
1065
+ } catch (error) {
1066
+ console.error('删除公共画廊作品失败:', error);
1067
+ alert('删除失败: ' + error.message);
1068
+ this.setHint('删除失败,请稍后再试', 'error');
1069
+ }
1070
+ }
1071
+ };
1072
+
1073
+ // ============================================
1074
+ // 生成请求模块
1075
+ // ============================================
1076
+ const Generator = {
1077
+ sendBtn: null,
1078
+ uploadBtn: null,
1079
+ clearBtn: null,
1080
+ textarea: null,
1081
+ isGenerating: false,
1082
+ generateTimeout: null,
1083
+
1084
+ init() {
1085
+ this.sendBtn = document.getElementById('send-btn');
1086
+ this.uploadBtn = document.getElementById('upload-btn');
1087
+ this.clearBtn = document.getElementById('clear-btn');
1088
+ this.textarea = document.getElementById('prompt');
1089
+
1090
+ this.sendBtn.onclick = () => this.generate();
1091
+
1092
+ if (this.clearBtn) {
1093
+ this.clearBtn.onclick = () => {
1094
+ const hasPrompt = this.textarea.value.trim().length > 0;
1095
+ const hasImages = AppState.currentImages.length > 0;
1096
+ if (!hasPrompt && !hasImages) {
1097
+ return;
1098
+ }
1099
+ this.textarea.value = '';
1100
+ this.textarea.style.height = 'auto';
1101
+ ImageHandler.clear();
1102
+ StatusBar.resetToContext();
1103
+ };
1104
+ }
1105
+
1106
+ this.textarea.addEventListener('input', () => {
1107
+ this.textarea.style.height = 'auto';
1108
+ this.textarea.style.height = this.textarea.scrollHeight + 'px';
1109
+ });
1110
+
1111
+ this.textarea.addEventListener('keydown', (e) => {
1112
+ if (e.key === 'Enter' && !e.shiftKey) {
1113
+ e.preventDefault();
1114
+ this.generate();
1115
+ }
1116
+ });
1117
+ },
1118
+
1119
+ setLoading(loading) {
1120
+ this.isGenerating = loading;
1121
+ if (loading) {
1122
+ this.sendBtn.classList.add('loading');
1123
+ this.sendBtn.disabled = true;
1124
+ if (this.clearBtn) {
1125
+ this.clearBtn.disabled = true;
1126
+ }
1127
+ if (this.uploadBtn) {
1128
+ this.uploadBtn.disabled = true;
1129
+ }
1130
+ StatusBar.startCountdown(60);
1131
+
1132
+ clearTimeout(this.generateTimeout);
1133
+ this.generateTimeout = setTimeout(() => {
1134
+ if (this.isGenerating) {
1135
+ this.setLoading(false);
1136
+ StatusBar.flash("\u751f\u6210\u8d85\u65f6");
1137
+ alert("\u751f\u6210\u8d85\u65f6\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u540e\u91cd\u8bd5");
1138
+ }
1139
+ }, 180000);
1140
+ } else {
1141
+ this.sendBtn.classList.remove('loading');
1142
+ this.sendBtn.disabled = false;
1143
+ if (this.clearBtn) {
1144
+ this.clearBtn.disabled = false;
1145
+ }
1146
+ if (this.uploadBtn) {
1147
+ this.uploadBtn.disabled = false;
1148
+ }
1149
+ clearTimeout(this.generateTimeout);
1150
+ StatusBar.stopCountdown();
1151
+ }
1152
+ },
1153
+
1154
+ async generate() {
1155
+ if (this.isGenerating) return;
1156
+
1157
+ const prompt = this.textarea.value.trim();
1158
+ if (!prompt) {
1159
+ alert("\u8bf7\u8f93\u5165\u63d0\u793a\u8bcd");
1160
+ return;
1161
+ }
1162
+
1163
+ this.setLoading(true);
1164
+ let statusMessage = null;
1165
+
1166
+ try {
1167
+ const controller = new AbortController();
1168
+ const timeoutId = setTimeout(() => controller.abort(), 120000);
1169
+
1170
+ const response = await fetch('/api/generate', {
1171
+ method: 'POST',
1172
+ headers: { 'Content-Type': 'application/json' },
1173
+ body: JSON.stringify({
1174
+ prompt: prompt,
1175
+ images: AppState.currentImages
1176
+ }),
1177
+ signal: controller.signal
1178
+ });
1179
+ clearTimeout(timeoutId);
1180
+
1181
+ const data = await response.json();
1182
+
1183
+ if (!response.ok || !data.success) {
1184
+ throw new Error(data.message || "\u751f\u6210\u5931\u8d25");
1185
+ }
1186
+
1187
+ if (!data.image || !data.image.startsWith('data:image')) {
1188
+ throw new Error("\u8fd4\u56de\u7684\u56fe\u7247\u6570\u636e\u65e0\u6548");
1189
+ }
1190
+
1191
+ await Database.save({
1192
+ prompt: prompt,
1193
+ image: data.image,
1194
+ inputImages: [...AppState.currentImages]
1195
+ });
1196
+
1197
+ await GalleryManager.load();
1198
+
1199
+ this.textarea.value = '';
1200
+ this.textarea.style.height = 'auto';
1201
+ ImageHandler.clear();
1202
+
1203
+ statusMessage = "\u751f\u6210\u6210\u529f";
1204
+ } catch (err) {
1205
+ console.error('Generate failed:', err);
1206
+
1207
+ let errorMessage = "\u751f\u6210\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5";
1208
+ if (err.message.includes("API\u8ba4\u8bc1\u5931\u8d25")) {
1209
+ errorMessage = "API\u8ba4\u8bc1\u5931\u8d25\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458\u68c0\u67e5API\u914d\u7f6e";
1210
+ } else if (err.message.includes("\u65e0\u6cd5\u8fde\u63a5\u5230API\u670d\u52a1\u5668")) {
1211
+ errorMessage = "\u65e0\u6cd5\u8fde\u63a5\u5230API\u670d\u52a1\u5668\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u8fde\u63a5";
1212
+ } else if (err.message.includes("API\u8bf7\u6c42\u8d85\u65f6")) {
1213
+ errorMessage = "API\u8bf7\u6c42\u8d85\u65f6\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5";
1214
+ } else if (err.message) {
1215
+ errorMessage = err.message;
1216
+ }
1217
+
1218
+ if (err.name === 'AbortError') {
1219
+ statusMessage = "\u751f\u6210\u8d85\u65f6";
1220
+ alert("\u751f\u6210\u8d85\u65f6\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u540e\u91cd\u8bd5");
1221
+ } else {
1222
+ statusMessage = "\u751f\u6210\u5931\u8d25";
1223
+ alert(errorMessage);
1224
+ }
1225
+ } finally {
1226
+ this.setLoading(false);
1227
+ if (statusMessage) {
1228
+ StatusBar.flash(statusMessage);
1229
+ } else {
1230
+ StatusBar.resetToContext();
1231
+ }
1232
+ }
1233
+ }
1234
+ };
1235
+
1236
+ // ============================================
1237
+ // 认证模块
1238
+ // ============================================
1239
+ const Auth = {
1240
+ async check() {
1241
+ try {
1242
+ const res = await fetch('/api/check-auth');
1243
+ const data = await res.json();
1244
+ return data.authenticated;
1245
+ } catch {
1246
+ return false;
1247
+ }
1248
+ },
1249
+
1250
+ async login(password) {
1251
+ const res = await fetch('/api/login', {
1252
+ method: 'POST',
1253
+ headers: { 'Content-Type': 'application/json' },
1254
+ body: JSON.stringify({ password })
1255
+ });
1256
+ const data = await res.json();
1257
+ return data.success;
1258
+ },
1259
+
1260
+ unlock() {
1261
+ document.getElementById('login-overlay').style.display = 'none';
1262
+ const app = document.getElementById('app');
1263
+ app.style.filter = 'none';
1264
+ app.style.pointerEvents = 'all';
1265
+ }
1266
+ };
1267
+
1268
+ // ============================================
1269
+ // 拖拽上传模块
1270
+ // ============================================
1271
+ const DragDrop = {
1272
+ dropZone: null,
1273
+
1274
+ init() {
1275
+ this.dropZone = document.getElementById('drop-zone');
1276
+
1277
+ // 阻止默认行为
1278
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
1279
+ this.dropZone.addEventListener(event, (e) => {
1280
+ e.preventDefault();
1281
+ e.stopPropagation();
1282
+ });
1283
+ });
1284
+
1285
+ // 拖入高亮
1286
+ ['dragenter', 'dragover'].forEach(event => {
1287
+ this.dropZone.addEventListener(event, () => {
1288
+ this.dropZone.style.borderColor = 'var(--accent-color)';
1289
+ this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
1290
+ });
1291
+ });
1292
+
1293
+ // 拖出恢复
1294
+ ['dragleave', 'drop'].forEach(event => {
1295
+ this.dropZone.addEventListener(event, () => {
1296
+ this.dropZone.style.borderColor = '';
1297
+ this.dropZone.style.background = '';
1298
+ });
1299
+ });
1300
+
1301
+ // 放下处理
1302
+ this.dropZone.addEventListener('drop', async (e) => {
1303
+ const files = e.dataTransfer.files;
1304
+ if (files.length > 0) {
1305
+ await ImageHandler.processFiles(files);
1306
+ }
1307
+ });
1308
+ }
1309
+ };
1310
+
1311
+ // ============================================
1312
+ // 文件选择模块
1313
+ // ============================================
1314
+ const FileSelector = {
1315
+ input: null,
1316
+ btn: null,
1317
+
1318
+ init() {
1319
+ this.input = document.getElementById('file-input');
1320
+ this.btn = document.getElementById('upload-btn');
1321
+
1322
+ this.btn.onclick = () => this.input.click();
1323
+
1324
+ this.input.onchange = async () => {
1325
+ if (this.input.files.length > 0) {
1326
+ await ImageHandler.processFiles(this.input.files);
1327
+ }
1328
+ this.input.value = ''; // 重置以便重复选择
1329
+ };
1330
+ }
1331
+ };
1332
+
1333
+ // ============================================
1334
+ // 全局函数(供 HTML onclick 调用)
1335
+ // ============================================
1336
+ async function loadWorkspaceData() {
1337
+ try {
1338
+ await GalleryManager.load();
1339
+ if ('requestIdleCallback' in window) {
1340
+ requestIdleCallback(() => PublicGalleryManager.fetch());
1341
+ } else {
1342
+ setTimeout(() => PublicGalleryManager.fetch(), 300);
1343
+ }
1344
+ } catch (error) {
1345
+ console.error('加载画廊数据失败:', error);
1346
+ }
1347
+ }
1348
+
1349
+ async function doLogin() {
1350
+ const pwd = document.getElementById('pwd').value;
1351
+ if (!pwd) return;
1352
+
1353
+ const success = await Auth.login(pwd);
1354
+ if (success) {
1355
+ Auth.unlock();
1356
+ await loadWorkspaceData();
1357
+ } else {
1358
+ alert('密码错误');
1359
+ }
1360
+ }
1361
+
1362
+ function closeModal() {
1363
+ ModalManager.close();
1364
+ }
1365
+
1366
+ // ============================================
1367
+ // 应用初始化
1368
+ // ============================================
1369
+ async function initApp() {
1370
+ try {
1371
+ // 初始化数据库
1372
+ await Database.init();
1373
+ StatusBar.init();
1374
+
1375
+ // 初始化各模块
1376
+ PreviewManager.init();
1377
+ GalleryManager.init();
1378
+ ModalManager.init();
1379
+ PublicGalleryManager.init();
1380
+ Generator.init();
1381
+ DragDrop.init();
1382
+ FileSelector.init();
1383
+
1384
+ // 检查认证状态
1385
+ const isAuth = await Auth.check();
1386
+ if (isAuth) {
1387
+ Auth.unlock();
1388
+ await loadWorkspaceData();
1389
+ }
1390
+
1391
+ console.log('App initialized successfully');
1392
+ } catch (err) {
1393
+ console.error('App initialization failed:', err);
1394
+ }
1395
+ }
1396
+
1397
+ // 启动应用
1398
+ if (document.readyState === 'loading') {
1399
+ document.addEventListener('DOMContentLoaded', initApp);
1400
+ } else {
1401
+ initApp();
1402
+ }
1403
+ </script>
1404
+ </body>
1405
  </html>
package-lock.json ADDED
@@ -0,0 +1,943 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "banana-pro-ai",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "banana-pro-ai",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "cookie-parser": "^1.4.6",
12
+ "dotenv": "^16.3.1",
13
+ "express": "^4.18.2"
14
+ }
15
+ },
16
+ "node_modules/accepts": {
17
+ "version": "1.3.8",
18
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
19
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "mime-types": "~2.1.34",
23
+ "negotiator": "0.6.3"
24
+ },
25
+ "engines": {
26
+ "node": ">= 0.6"
27
+ }
28
+ },
29
+ "node_modules/array-flatten": {
30
+ "version": "1.1.1",
31
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
32
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
33
+ "license": "MIT"
34
+ },
35
+ "node_modules/body-parser": {
36
+ "version": "1.20.4",
37
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
38
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "bytes": "~3.1.2",
42
+ "content-type": "~1.0.5",
43
+ "debug": "2.6.9",
44
+ "depd": "2.0.0",
45
+ "destroy": "~1.2.0",
46
+ "http-errors": "~2.0.1",
47
+ "iconv-lite": "~0.4.24",
48
+ "on-finished": "~2.4.1",
49
+ "qs": "~6.14.0",
50
+ "raw-body": "~2.5.3",
51
+ "type-is": "~1.6.18",
52
+ "unpipe": "~1.0.0"
53
+ },
54
+ "engines": {
55
+ "node": ">= 0.8",
56
+ "npm": "1.2.8000 || >= 1.4.16"
57
+ }
58
+ },
59
+ "node_modules/bytes": {
60
+ "version": "3.1.2",
61
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
62
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
63
+ "license": "MIT",
64
+ "engines": {
65
+ "node": ">= 0.8"
66
+ }
67
+ },
68
+ "node_modules/call-bind-apply-helpers": {
69
+ "version": "1.0.2",
70
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
71
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
72
+ "license": "MIT",
73
+ "dependencies": {
74
+ "es-errors": "^1.3.0",
75
+ "function-bind": "^1.1.2"
76
+ },
77
+ "engines": {
78
+ "node": ">= 0.4"
79
+ }
80
+ },
81
+ "node_modules/call-bound": {
82
+ "version": "1.0.4",
83
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
84
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
85
+ "license": "MIT",
86
+ "dependencies": {
87
+ "call-bind-apply-helpers": "^1.0.2",
88
+ "get-intrinsic": "^1.3.0"
89
+ },
90
+ "engines": {
91
+ "node": ">= 0.4"
92
+ },
93
+ "funding": {
94
+ "url": "https://github.com/sponsors/ljharb"
95
+ }
96
+ },
97
+ "node_modules/content-disposition": {
98
+ "version": "0.5.4",
99
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
100
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
101
+ "license": "MIT",
102
+ "dependencies": {
103
+ "safe-buffer": "5.2.1"
104
+ },
105
+ "engines": {
106
+ "node": ">= 0.6"
107
+ }
108
+ },
109
+ "node_modules/content-type": {
110
+ "version": "1.0.5",
111
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
112
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
113
+ "license": "MIT",
114
+ "engines": {
115
+ "node": ">= 0.6"
116
+ }
117
+ },
118
+ "node_modules/cookie": {
119
+ "version": "0.7.2",
120
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
121
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
122
+ "license": "MIT",
123
+ "engines": {
124
+ "node": ">= 0.6"
125
+ }
126
+ },
127
+ "node_modules/cookie-parser": {
128
+ "version": "1.4.7",
129
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
130
+ "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
131
+ "license": "MIT",
132
+ "dependencies": {
133
+ "cookie": "0.7.2",
134
+ "cookie-signature": "1.0.6"
135
+ },
136
+ "engines": {
137
+ "node": ">= 0.8.0"
138
+ }
139
+ },
140
+ "node_modules/cookie-signature": {
141
+ "version": "1.0.6",
142
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
143
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
144
+ "license": "MIT"
145
+ },
146
+ "node_modules/debug": {
147
+ "version": "2.6.9",
148
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
149
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
150
+ "license": "MIT",
151
+ "dependencies": {
152
+ "ms": "2.0.0"
153
+ }
154
+ },
155
+ "node_modules/depd": {
156
+ "version": "2.0.0",
157
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
158
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
159
+ "license": "MIT",
160
+ "engines": {
161
+ "node": ">= 0.8"
162
+ }
163
+ },
164
+ "node_modules/destroy": {
165
+ "version": "1.2.0",
166
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
167
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
168
+ "license": "MIT",
169
+ "engines": {
170
+ "node": ">= 0.8",
171
+ "npm": "1.2.8000 || >= 1.4.16"
172
+ }
173
+ },
174
+ "node_modules/dotenv": {
175
+ "version": "16.6.1",
176
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
177
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
178
+ "license": "BSD-2-Clause",
179
+ "engines": {
180
+ "node": ">=12"
181
+ },
182
+ "funding": {
183
+ "url": "https://dotenvx.com"
184
+ }
185
+ },
186
+ "node_modules/dunder-proto": {
187
+ "version": "1.0.1",
188
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
189
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
190
+ "license": "MIT",
191
+ "dependencies": {
192
+ "call-bind-apply-helpers": "^1.0.1",
193
+ "es-errors": "^1.3.0",
194
+ "gopd": "^1.2.0"
195
+ },
196
+ "engines": {
197
+ "node": ">= 0.4"
198
+ }
199
+ },
200
+ "node_modules/ee-first": {
201
+ "version": "1.1.1",
202
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
203
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
204
+ "license": "MIT"
205
+ },
206
+ "node_modules/encodeurl": {
207
+ "version": "2.0.0",
208
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
209
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
210
+ "license": "MIT",
211
+ "engines": {
212
+ "node": ">= 0.8"
213
+ }
214
+ },
215
+ "node_modules/es-define-property": {
216
+ "version": "1.0.1",
217
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
218
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
219
+ "license": "MIT",
220
+ "engines": {
221
+ "node": ">= 0.4"
222
+ }
223
+ },
224
+ "node_modules/es-errors": {
225
+ "version": "1.3.0",
226
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
227
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
228
+ "license": "MIT",
229
+ "engines": {
230
+ "node": ">= 0.4"
231
+ }
232
+ },
233
+ "node_modules/es-object-atoms": {
234
+ "version": "1.1.1",
235
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
236
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
237
+ "license": "MIT",
238
+ "dependencies": {
239
+ "es-errors": "^1.3.0"
240
+ },
241
+ "engines": {
242
+ "node": ">= 0.4"
243
+ }
244
+ },
245
+ "node_modules/escape-html": {
246
+ "version": "1.0.3",
247
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
248
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
249
+ "license": "MIT"
250
+ },
251
+ "node_modules/etag": {
252
+ "version": "1.8.1",
253
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
254
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
255
+ "license": "MIT",
256
+ "engines": {
257
+ "node": ">= 0.6"
258
+ }
259
+ },
260
+ "node_modules/express": {
261
+ "version": "4.22.1",
262
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
263
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
264
+ "license": "MIT",
265
+ "dependencies": {
266
+ "accepts": "~1.3.8",
267
+ "array-flatten": "1.1.1",
268
+ "body-parser": "~1.20.3",
269
+ "content-disposition": "~0.5.4",
270
+ "content-type": "~1.0.4",
271
+ "cookie": "~0.7.1",
272
+ "cookie-signature": "~1.0.6",
273
+ "debug": "2.6.9",
274
+ "depd": "2.0.0",
275
+ "encodeurl": "~2.0.0",
276
+ "escape-html": "~1.0.3",
277
+ "etag": "~1.8.1",
278
+ "finalhandler": "~1.3.1",
279
+ "fresh": "~0.5.2",
280
+ "http-errors": "~2.0.0",
281
+ "merge-descriptors": "1.0.3",
282
+ "methods": "~1.1.2",
283
+ "on-finished": "~2.4.1",
284
+ "parseurl": "~1.3.3",
285
+ "path-to-regexp": "~0.1.12",
286
+ "proxy-addr": "~2.0.7",
287
+ "qs": "~6.14.0",
288
+ "range-parser": "~1.2.1",
289
+ "safe-buffer": "5.2.1",
290
+ "send": "~0.19.0",
291
+ "serve-static": "~1.16.2",
292
+ "setprototypeof": "1.2.0",
293
+ "statuses": "~2.0.1",
294
+ "type-is": "~1.6.18",
295
+ "utils-merge": "1.0.1",
296
+ "vary": "~1.1.2"
297
+ },
298
+ "engines": {
299
+ "node": ">= 0.10.0"
300
+ },
301
+ "funding": {
302
+ "type": "opencollective",
303
+ "url": "https://opencollective.com/express"
304
+ }
305
+ },
306
+ "node_modules/finalhandler": {
307
+ "version": "1.3.2",
308
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
309
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
310
+ "license": "MIT",
311
+ "dependencies": {
312
+ "debug": "2.6.9",
313
+ "encodeurl": "~2.0.0",
314
+ "escape-html": "~1.0.3",
315
+ "on-finished": "~2.4.1",
316
+ "parseurl": "~1.3.3",
317
+ "statuses": "~2.0.2",
318
+ "unpipe": "~1.0.0"
319
+ },
320
+ "engines": {
321
+ "node": ">= 0.8"
322
+ }
323
+ },
324
+ "node_modules/forwarded": {
325
+ "version": "0.2.0",
326
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
327
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
328
+ "license": "MIT",
329
+ "engines": {
330
+ "node": ">= 0.6"
331
+ }
332
+ },
333
+ "node_modules/fresh": {
334
+ "version": "0.5.2",
335
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
336
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
337
+ "license": "MIT",
338
+ "engines": {
339
+ "node": ">= 0.6"
340
+ }
341
+ },
342
+ "node_modules/function-bind": {
343
+ "version": "1.1.2",
344
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
345
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
346
+ "license": "MIT",
347
+ "funding": {
348
+ "url": "https://github.com/sponsors/ljharb"
349
+ }
350
+ },
351
+ "node_modules/get-intrinsic": {
352
+ "version": "1.3.0",
353
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
354
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
355
+ "license": "MIT",
356
+ "dependencies": {
357
+ "call-bind-apply-helpers": "^1.0.2",
358
+ "es-define-property": "^1.0.1",
359
+ "es-errors": "^1.3.0",
360
+ "es-object-atoms": "^1.1.1",
361
+ "function-bind": "^1.1.2",
362
+ "get-proto": "^1.0.1",
363
+ "gopd": "^1.2.0",
364
+ "has-symbols": "^1.1.0",
365
+ "hasown": "^2.0.2",
366
+ "math-intrinsics": "^1.1.0"
367
+ },
368
+ "engines": {
369
+ "node": ">= 0.4"
370
+ },
371
+ "funding": {
372
+ "url": "https://github.com/sponsors/ljharb"
373
+ }
374
+ },
375
+ "node_modules/get-proto": {
376
+ "version": "1.0.1",
377
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
378
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
379
+ "license": "MIT",
380
+ "dependencies": {
381
+ "dunder-proto": "^1.0.1",
382
+ "es-object-atoms": "^1.0.0"
383
+ },
384
+ "engines": {
385
+ "node": ">= 0.4"
386
+ }
387
+ },
388
+ "node_modules/gopd": {
389
+ "version": "1.2.0",
390
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
391
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
392
+ "license": "MIT",
393
+ "engines": {
394
+ "node": ">= 0.4"
395
+ },
396
+ "funding": {
397
+ "url": "https://github.com/sponsors/ljharb"
398
+ }
399
+ },
400
+ "node_modules/has-symbols": {
401
+ "version": "1.1.0",
402
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
403
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
404
+ "license": "MIT",
405
+ "engines": {
406
+ "node": ">= 0.4"
407
+ },
408
+ "funding": {
409
+ "url": "https://github.com/sponsors/ljharb"
410
+ }
411
+ },
412
+ "node_modules/hasown": {
413
+ "version": "2.0.2",
414
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
415
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
416
+ "license": "MIT",
417
+ "dependencies": {
418
+ "function-bind": "^1.1.2"
419
+ },
420
+ "engines": {
421
+ "node": ">= 0.4"
422
+ }
423
+ },
424
+ "node_modules/http-errors": {
425
+ "version": "2.0.1",
426
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
427
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
428
+ "license": "MIT",
429
+ "dependencies": {
430
+ "depd": "~2.0.0",
431
+ "inherits": "~2.0.4",
432
+ "setprototypeof": "~1.2.0",
433
+ "statuses": "~2.0.2",
434
+ "toidentifier": "~1.0.1"
435
+ },
436
+ "engines": {
437
+ "node": ">= 0.8"
438
+ },
439
+ "funding": {
440
+ "type": "opencollective",
441
+ "url": "https://opencollective.com/express"
442
+ }
443
+ },
444
+ "node_modules/iconv-lite": {
445
+ "version": "0.4.24",
446
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
447
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
448
+ "license": "MIT",
449
+ "dependencies": {
450
+ "safer-buffer": ">= 2.1.2 < 3"
451
+ },
452
+ "engines": {
453
+ "node": ">=0.10.0"
454
+ }
455
+ },
456
+ "node_modules/inherits": {
457
+ "version": "2.0.4",
458
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
459
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
460
+ "license": "ISC"
461
+ },
462
+ "node_modules/ipaddr.js": {
463
+ "version": "1.9.1",
464
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
465
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
466
+ "license": "MIT",
467
+ "engines": {
468
+ "node": ">= 0.10"
469
+ }
470
+ },
471
+ "node_modules/math-intrinsics": {
472
+ "version": "1.1.0",
473
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
474
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
475
+ "license": "MIT",
476
+ "engines": {
477
+ "node": ">= 0.4"
478
+ }
479
+ },
480
+ "node_modules/media-typer": {
481
+ "version": "0.3.0",
482
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
483
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
484
+ "license": "MIT",
485
+ "engines": {
486
+ "node": ">= 0.6"
487
+ }
488
+ },
489
+ "node_modules/merge-descriptors": {
490
+ "version": "1.0.3",
491
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
492
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
493
+ "license": "MIT",
494
+ "funding": {
495
+ "url": "https://github.com/sponsors/sindresorhus"
496
+ }
497
+ },
498
+ "node_modules/methods": {
499
+ "version": "1.1.2",
500
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
501
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
502
+ "license": "MIT",
503
+ "engines": {
504
+ "node": ">= 0.6"
505
+ }
506
+ },
507
+ "node_modules/mime": {
508
+ "version": "1.6.0",
509
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
510
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
511
+ "license": "MIT",
512
+ "bin": {
513
+ "mime": "cli.js"
514
+ },
515
+ "engines": {
516
+ "node": ">=4"
517
+ }
518
+ },
519
+ "node_modules/mime-db": {
520
+ "version": "1.52.0",
521
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
522
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
523
+ "license": "MIT",
524
+ "engines": {
525
+ "node": ">= 0.6"
526
+ }
527
+ },
528
+ "node_modules/mime-types": {
529
+ "version": "2.1.35",
530
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
531
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
532
+ "license": "MIT",
533
+ "dependencies": {
534
+ "mime-db": "1.52.0"
535
+ },
536
+ "engines": {
537
+ "node": ">= 0.6"
538
+ }
539
+ },
540
+ "node_modules/ms": {
541
+ "version": "2.0.0",
542
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
543
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
544
+ "license": "MIT"
545
+ },
546
+ "node_modules/negotiator": {
547
+ "version": "0.6.3",
548
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
549
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
550
+ "license": "MIT",
551
+ "engines": {
552
+ "node": ">= 0.6"
553
+ }
554
+ },
555
+ "node_modules/object-inspect": {
556
+ "version": "1.13.4",
557
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
558
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
559
+ "license": "MIT",
560
+ "engines": {
561
+ "node": ">= 0.4"
562
+ },
563
+ "funding": {
564
+ "url": "https://github.com/sponsors/ljharb"
565
+ }
566
+ },
567
+ "node_modules/on-finished": {
568
+ "version": "2.4.1",
569
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
570
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
571
+ "license": "MIT",
572
+ "dependencies": {
573
+ "ee-first": "1.1.1"
574
+ },
575
+ "engines": {
576
+ "node": ">= 0.8"
577
+ }
578
+ },
579
+ "node_modules/parseurl": {
580
+ "version": "1.3.3",
581
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
582
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
583
+ "license": "MIT",
584
+ "engines": {
585
+ "node": ">= 0.8"
586
+ }
587
+ },
588
+ "node_modules/path-to-regexp": {
589
+ "version": "0.1.12",
590
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
591
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
592
+ "license": "MIT"
593
+ },
594
+ "node_modules/proxy-addr": {
595
+ "version": "2.0.7",
596
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
597
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
598
+ "license": "MIT",
599
+ "dependencies": {
600
+ "forwarded": "0.2.0",
601
+ "ipaddr.js": "1.9.1"
602
+ },
603
+ "engines": {
604
+ "node": ">= 0.10"
605
+ }
606
+ },
607
+ "node_modules/qs": {
608
+ "version": "6.14.0",
609
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
610
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
611
+ "license": "BSD-3-Clause",
612
+ "dependencies": {
613
+ "side-channel": "^1.1.0"
614
+ },
615
+ "engines": {
616
+ "node": ">=0.6"
617
+ },
618
+ "funding": {
619
+ "url": "https://github.com/sponsors/ljharb"
620
+ }
621
+ },
622
+ "node_modules/range-parser": {
623
+ "version": "1.2.1",
624
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
625
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
626
+ "license": "MIT",
627
+ "engines": {
628
+ "node": ">= 0.6"
629
+ }
630
+ },
631
+ "node_modules/raw-body": {
632
+ "version": "2.5.3",
633
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
634
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
635
+ "license": "MIT",
636
+ "dependencies": {
637
+ "bytes": "~3.1.2",
638
+ "http-errors": "~2.0.1",
639
+ "iconv-lite": "~0.4.24",
640
+ "unpipe": "~1.0.0"
641
+ },
642
+ "engines": {
643
+ "node": ">= 0.8"
644
+ }
645
+ },
646
+ "node_modules/safe-buffer": {
647
+ "version": "5.2.1",
648
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
649
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
650
+ "funding": [
651
+ {
652
+ "type": "github",
653
+ "url": "https://github.com/sponsors/feross"
654
+ },
655
+ {
656
+ "type": "patreon",
657
+ "url": "https://www.patreon.com/feross"
658
+ },
659
+ {
660
+ "type": "consulting",
661
+ "url": "https://feross.org/support"
662
+ }
663
+ ],
664
+ "license": "MIT"
665
+ },
666
+ "node_modules/safer-buffer": {
667
+ "version": "2.1.2",
668
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
669
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
670
+ "license": "MIT"
671
+ },
672
+ "node_modules/send": {
673
+ "version": "0.19.1",
674
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz",
675
+ "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==",
676
+ "license": "MIT",
677
+ "dependencies": {
678
+ "debug": "2.6.9",
679
+ "depd": "2.0.0",
680
+ "destroy": "1.2.0",
681
+ "encodeurl": "~2.0.0",
682
+ "escape-html": "~1.0.3",
683
+ "etag": "~1.8.1",
684
+ "fresh": "0.5.2",
685
+ "http-errors": "2.0.0",
686
+ "mime": "1.6.0",
687
+ "ms": "2.1.3",
688
+ "on-finished": "2.4.1",
689
+ "range-parser": "~1.2.1",
690
+ "statuses": "2.0.1"
691
+ },
692
+ "engines": {
693
+ "node": ">= 0.8.0"
694
+ }
695
+ },
696
+ "node_modules/send/node_modules/http-errors": {
697
+ "version": "2.0.0",
698
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
699
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
700
+ "license": "MIT",
701
+ "dependencies": {
702
+ "depd": "2.0.0",
703
+ "inherits": "2.0.4",
704
+ "setprototypeof": "1.2.0",
705
+ "statuses": "2.0.1",
706
+ "toidentifier": "1.0.1"
707
+ },
708
+ "engines": {
709
+ "node": ">= 0.8"
710
+ }
711
+ },
712
+ "node_modules/send/node_modules/ms": {
713
+ "version": "2.1.3",
714
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
715
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
716
+ "license": "MIT"
717
+ },
718
+ "node_modules/send/node_modules/statuses": {
719
+ "version": "2.0.1",
720
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
721
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
722
+ "license": "MIT",
723
+ "engines": {
724
+ "node": ">= 0.8"
725
+ }
726
+ },
727
+ "node_modules/serve-static": {
728
+ "version": "1.16.2",
729
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
730
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
731
+ "license": "MIT",
732
+ "dependencies": {
733
+ "encodeurl": "~2.0.0",
734
+ "escape-html": "~1.0.3",
735
+ "parseurl": "~1.3.3",
736
+ "send": "0.19.0"
737
+ },
738
+ "engines": {
739
+ "node": ">= 0.8.0"
740
+ }
741
+ },
742
+ "node_modules/serve-static/node_modules/http-errors": {
743
+ "version": "2.0.0",
744
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
745
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
746
+ "license": "MIT",
747
+ "dependencies": {
748
+ "depd": "2.0.0",
749
+ "inherits": "2.0.4",
750
+ "setprototypeof": "1.2.0",
751
+ "statuses": "2.0.1",
752
+ "toidentifier": "1.0.1"
753
+ },
754
+ "engines": {
755
+ "node": ">= 0.8"
756
+ }
757
+ },
758
+ "node_modules/serve-static/node_modules/ms": {
759
+ "version": "2.1.3",
760
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
761
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
762
+ "license": "MIT"
763
+ },
764
+ "node_modules/serve-static/node_modules/send": {
765
+ "version": "0.19.0",
766
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
767
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
768
+ "license": "MIT",
769
+ "dependencies": {
770
+ "debug": "2.6.9",
771
+ "depd": "2.0.0",
772
+ "destroy": "1.2.0",
773
+ "encodeurl": "~1.0.2",
774
+ "escape-html": "~1.0.3",
775
+ "etag": "~1.8.1",
776
+ "fresh": "0.5.2",
777
+ "http-errors": "2.0.0",
778
+ "mime": "1.6.0",
779
+ "ms": "2.1.3",
780
+ "on-finished": "2.4.1",
781
+ "range-parser": "~1.2.1",
782
+ "statuses": "2.0.1"
783
+ },
784
+ "engines": {
785
+ "node": ">= 0.8.0"
786
+ }
787
+ },
788
+ "node_modules/serve-static/node_modules/send/node_modules/encodeurl": {
789
+ "version": "1.0.2",
790
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
791
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
792
+ "license": "MIT",
793
+ "engines": {
794
+ "node": ">= 0.8"
795
+ }
796
+ },
797
+ "node_modules/serve-static/node_modules/statuses": {
798
+ "version": "2.0.1",
799
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
800
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
801
+ "license": "MIT",
802
+ "engines": {
803
+ "node": ">= 0.8"
804
+ }
805
+ },
806
+ "node_modules/setprototypeof": {
807
+ "version": "1.2.0",
808
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
809
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
810
+ "license": "ISC"
811
+ },
812
+ "node_modules/side-channel": {
813
+ "version": "1.1.0",
814
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
815
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
816
+ "license": "MIT",
817
+ "dependencies": {
818
+ "es-errors": "^1.3.0",
819
+ "object-inspect": "^1.13.3",
820
+ "side-channel-list": "^1.0.0",
821
+ "side-channel-map": "^1.0.1",
822
+ "side-channel-weakmap": "^1.0.2"
823
+ },
824
+ "engines": {
825
+ "node": ">= 0.4"
826
+ },
827
+ "funding": {
828
+ "url": "https://github.com/sponsors/ljharb"
829
+ }
830
+ },
831
+ "node_modules/side-channel-list": {
832
+ "version": "1.0.0",
833
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
834
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
835
+ "license": "MIT",
836
+ "dependencies": {
837
+ "es-errors": "^1.3.0",
838
+ "object-inspect": "^1.13.3"
839
+ },
840
+ "engines": {
841
+ "node": ">= 0.4"
842
+ },
843
+ "funding": {
844
+ "url": "https://github.com/sponsors/ljharb"
845
+ }
846
+ },
847
+ "node_modules/side-channel-map": {
848
+ "version": "1.0.1",
849
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
850
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
851
+ "license": "MIT",
852
+ "dependencies": {
853
+ "call-bound": "^1.0.2",
854
+ "es-errors": "^1.3.0",
855
+ "get-intrinsic": "^1.2.5",
856
+ "object-inspect": "^1.13.3"
857
+ },
858
+ "engines": {
859
+ "node": ">= 0.4"
860
+ },
861
+ "funding": {
862
+ "url": "https://github.com/sponsors/ljharb"
863
+ }
864
+ },
865
+ "node_modules/side-channel-weakmap": {
866
+ "version": "1.0.2",
867
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
868
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
869
+ "license": "MIT",
870
+ "dependencies": {
871
+ "call-bound": "^1.0.2",
872
+ "es-errors": "^1.3.0",
873
+ "get-intrinsic": "^1.2.5",
874
+ "object-inspect": "^1.13.3",
875
+ "side-channel-map": "^1.0.1"
876
+ },
877
+ "engines": {
878
+ "node": ">= 0.4"
879
+ },
880
+ "funding": {
881
+ "url": "https://github.com/sponsors/ljharb"
882
+ }
883
+ },
884
+ "node_modules/statuses": {
885
+ "version": "2.0.2",
886
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
887
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
888
+ "license": "MIT",
889
+ "engines": {
890
+ "node": ">= 0.8"
891
+ }
892
+ },
893
+ "node_modules/toidentifier": {
894
+ "version": "1.0.1",
895
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
896
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
897
+ "license": "MIT",
898
+ "engines": {
899
+ "node": ">=0.6"
900
+ }
901
+ },
902
+ "node_modules/type-is": {
903
+ "version": "1.6.18",
904
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
905
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
906
+ "license": "MIT",
907
+ "dependencies": {
908
+ "media-typer": "0.3.0",
909
+ "mime-types": "~2.1.24"
910
+ },
911
+ "engines": {
912
+ "node": ">= 0.6"
913
+ }
914
+ },
915
+ "node_modules/unpipe": {
916
+ "version": "1.0.0",
917
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
918
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
919
+ "license": "MIT",
920
+ "engines": {
921
+ "node": ">= 0.8"
922
+ }
923
+ },
924
+ "node_modules/utils-merge": {
925
+ "version": "1.0.1",
926
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
927
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
928
+ "license": "MIT",
929
+ "engines": {
930
+ "node": ">= 0.4.0"
931
+ }
932
+ },
933
+ "node_modules/vary": {
934
+ "version": "1.1.2",
935
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
936
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
937
+ "license": "MIT",
938
+ "engines": {
939
+ "node": ">= 0.8"
940
+ }
941
+ }
942
+ }
943
+ }
style.css CHANGED
@@ -1,1278 +1,1440 @@
1
- /* style.css - Enhanced Glass Design with Animations */
2
- :root {
3
- --bg-color: #0f172a;
4
- --panel-bg: rgba(30, 41, 59, 0.65);
5
- --panel-bg-strong: rgba(30, 41, 59, 0.85);
6
- --panel-border: rgba(148, 163, 184, 0.15);
7
- --panel-border-strong: rgba(148, 163, 184, 0.3);
8
- --accent-color: #3b82f6;
9
- --accent-hover: #2563eb;
10
- --text-main: #f1f5f9;
11
- --text-sub: #94a3b8;
12
- --btn-shadow: #1e3a8a;
13
- --glass-shine: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 100%);
14
- --transition-fast: 0.2s ease;
15
- --transition-normal: 0.3s ease;
16
- --transition-slow: 0.5s ease;
17
- }
18
-
19
- * {
20
- box-sizing: border-box;
21
- margin: 0;
22
- padding: 0;
23
- -webkit-tap-highlight-color: transparent;
24
- }
25
-
26
- body {
27
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
28
- background: var(--bg-color) radial-gradient(circle at 50% 0%, #1e293b 0%, var(--bg-color) 70%);
29
- color: var(--text-main);
30
- height: 100vh;
31
- display: flex;
32
- flex-direction: column;
33
- overflow: hidden;
34
- animation: fadeIn 0.6s ease-out;
35
- }
36
-
37
- @keyframes fadeIn {
38
- from { opacity: 0; }
39
- to { opacity: 1; }
40
- }
41
-
42
- /* --- Enhanced Glass Components --- */
43
- .glass-panel {
44
- background: var(--panel-bg);
45
- backdrop-filter: blur(20px) saturate(180%);
46
- -webkit-backdrop-filter: blur(20px) saturate(180%);
47
- border: 1px solid var(--panel-border);
48
- position: relative;
49
- transition: all var(--transition-normal);
50
- will-change: transform;
51
- }
52
-
53
- @media (max-width: 768px) {
54
- .glass-panel {
55
- backdrop-filter: blur(10px) saturate(120%);
56
- -webkit-backdrop-filter: blur(10px) saturate(120%);
57
- }
58
- }
59
-
60
- .glass-panel::before {
61
- content: '';
62
- position: absolute;
63
- top: 0;
64
- left: 0;
65
- right: 0;
66
- height: 1px;
67
- background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%);
68
- opacity: 0.5;
69
- }
70
-
71
- .glass-panel:hover {
72
- border-color: var(--panel-border-strong);
73
- transform: translateY(-1px);
74
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
75
- }
76
-
77
- /* --- Enhanced 3D Buttons --- */
78
- .btn-3d {
79
- background: linear-gradient(135deg, var(--accent-color) 0%, #2563eb 100%);
80
- color: white;
81
- border: none;
82
- border-radius: 12px;
83
- font-weight: 600;
84
- box-shadow:
85
- 0 4px 0 var(--btn-shadow),
86
- 0 5px 15px rgba(59, 130, 246, 0.3),
87
- inset 0 1px 0 rgba(255, 255, 255, 0.2);
88
- transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
89
- cursor: pointer;
90
- display: inline-flex;
91
- align-items: center;
92
- justify-content: center;
93
- position: relative;
94
- overflow: hidden;
95
- }
96
-
97
- .btn-3d::before {
98
- content: '';
99
- position: absolute;
100
- top: 0;
101
- left: -100%;
102
- width: 100%;
103
- height: 100%;
104
- background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
105
- transition: left 0.5s;
106
- }
107
-
108
- .btn-3d:hover {
109
- transform: translateY(-2px);
110
- box-shadow:
111
- 0 6px 0 var(--btn-shadow),
112
- 0 8px 20px rgba(59, 130, 246, 0.4),
113
- inset 0 1px 0 rgba(255, 255, 255, 0.2);
114
- }
115
-
116
- .btn-3d:hover::before {
117
- left: 100%;
118
- }
119
-
120
- .btn-3d:active {
121
- transform: translateY(4px);
122
- box-shadow:
123
- 0 0 0 var(--btn-shadow),
124
- 0 2px 5px rgba(59, 130, 246, 0.3);
125
- }
126
-
127
- .btn-3d:disabled {
128
- background: linear-gradient(135deg, #475569 0%, #334155 100%);
129
- box-shadow: none;
130
- transform: none;
131
- opacity: 0.7;
132
- cursor: not-allowed;
133
- }
134
-
135
- /* --- Layout --- */
136
- .app-container {
137
- max-width: 1200px;
138
- margin: 0 auto;
139
- width: 100%;
140
- height: 100%;
141
- display: flex;
142
- flex-direction: column;
143
- position: relative;
144
- animation: slideUp 0.6s ease-out;
145
- }
146
-
147
- @keyframes slideUp {
148
- from {
149
- opacity: 0;
150
- transform: translateY(20px);
151
- }
152
- to {
153
- opacity: 1;
154
- transform: translateY(0);
155
- }
156
- }
157
-
158
- header {
159
- padding: 15px 20px;
160
- display: flex;
161
- justify-content: space-between;
162
- align-items: center;
163
- flex-shrink: 0;
164
- margin-top: 70px;
165
- transition: margin-top var(--transition-normal);
166
- }
167
-
168
- header h2 {
169
- font-size: 1.2rem;
170
- font-weight: 700;
171
- letter-spacing: 0.5px;
172
- background: linear-gradient(135deg, var(--text-main) 0%, var(--text-sub) 100%);
173
- -webkit-background-clip: text;
174
- -webkit-text-fill-color: transparent;
175
- background-clip: text;
176
- }
177
-
178
- body.has-preview header {
179
- margin-top: 170px;
180
- }
181
-
182
- /* --- History Gallery --- */
183
- .history-container {
184
- flex: 1;
185
- overflow-y: auto;
186
- padding: 10px 20px;
187
- padding-bottom: 20px;
188
- scroll-behavior: smooth;
189
- }
190
-
191
- .history-container::-webkit-scrollbar {
192
- width: 8px;
193
- }
194
-
195
- .history-container::-webkit-scrollbar-track {
196
- background: rgba(0, 0, 0, 0.2);
197
- border-radius: 4px;
198
- }
199
-
200
- .history-container::-webkit-scrollbar-thumb {
201
- background: var(--panel-bg);
202
- border-radius: 4px;
203
- transition: background var(--transition-fast);
204
- }
205
-
206
- .history-container::-webkit-scrollbar-thumb:hover {
207
- background: var(--panel-bg-strong);
208
- }
209
-
210
- .grid-layout {
211
- display: grid;
212
- grid-template-columns: repeat(2, 1fr);
213
- gap: 16px;
214
- }
215
-
216
- .public-gallery-section {
217
- margin-top: 40px;
218
- padding: 32px;
219
- border-radius: 24px;
220
- border: 1px solid var(--panel-border);
221
- background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(20, 30, 50, 0.8) 100%);
222
- backdrop-filter: blur(20px) saturate(180%);
223
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
224
- animation: slideUp 0.6s ease-out;
225
- position: relative;
226
- overflow: hidden;
227
- }
228
-
229
- .public-gallery-section::before {
230
- content: '';
231
- position: absolute;
232
- top: 0;
233
- left: 0;
234
- right: 0;
235
- height: 1px;
236
- background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%);
237
- opacity: 0.5;
238
- }
239
-
240
- .section-title {
241
- display: flex;
242
- justify-content: space-between;
243
- align-items: flex-start;
244
- gap: 16px;
245
- flex-wrap: wrap;
246
- position: relative;
247
- }
248
-
249
- .section-title h3 {
250
- margin: 0;
251
- font-size: 1.3rem;
252
- letter-spacing: 0.5px;
253
- background: linear-gradient(135deg, var(--text-main) 0%, var(--text-sub) 100%);
254
- -webkit-background-clip: text;
255
- -webkit-text-fill-color: transparent;
256
- background-clip: text;
257
- font-weight: 700;
258
- }
259
-
260
- .section-subtitle {
261
- margin-top: 8px;
262
- font-size: 0.95rem;
263
- color: var(--text-sub);
264
- transition: color var(--transition-fast);
265
- }
266
-
267
- .section-subtitle.is-success {
268
- color: #34d399;
269
- font-weight: 500;
270
- }
271
-
272
- .section-subtitle.is-error {
273
- color: #f87171;
274
- font-weight: 500;
275
- }
276
-
277
- .section-actions {
278
- display: flex;
279
- align-items: center;
280
- gap: 12px;
281
- }
282
-
283
- .public-grid {
284
- margin-top: 28px;
285
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
286
- gap: 24px;
287
- }
288
-
289
- .public-card {
290
- position: relative;
291
- border-radius: 18px;
292
- overflow: hidden;
293
- border: 1px solid var(--panel-border);
294
- background: rgba(15, 23, 42, 0.85);
295
- aspect-ratio: 16 / 9;
296
- cursor: pointer;
297
- transition: all var(--transition-normal);
298
- box-shadow: 0 10px 35px rgba(0, 0, 0, 0.3);
299
- animation: scaleIn 0.5s ease-out backwards;
300
- will-change: transform, box-shadow;
301
- }
302
-
303
- @media (max-width: 768px) {
304
- .public-card {
305
- animation: fadeIn 0.3s ease-out backwards;
306
- }
307
- }
308
-
309
- .public-card img {
310
- width: 100%;
311
- height: 100%;
312
- object-fit: cover;
313
- display: block;
314
- transition: transform var(--transition-normal);
315
- }
316
-
317
- .public-card:hover {
318
- transform: translateY(-8px);
319
- box-shadow: 0 20px 50px rgba(59, 130, 246, 0.3);
320
- border-color: rgba(59, 130, 246, 0.4);
321
- }
322
-
323
- .public-card:hover img {
324
- transform: scale(1.08);
325
- }
326
-
327
- .public-card:nth-child(1) { animation-delay: 0.05s; }
328
- .public-card:nth-child(2) { animation-delay: 0.1s; }
329
- .public-card:nth-child(3) { animation-delay: 0.15s; }
330
- .public-card:nth-child(4) { animation-delay: 0.2s; }
331
- .public-card:nth-child(5) { animation-delay: 0.25s; }
332
- .public-card:nth-child(6) { animation-delay: 0.3s; }
333
-
334
- .public-card-info {
335
- position: absolute;
336
- left: 0;
337
- right: 0;
338
- bottom: 0;
339
- padding: 16px;
340
- background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.7) 40%, rgba(15, 23, 42, 0.95) 100%);
341
- display: flex;
342
- flex-direction: column;
343
- gap: 10px;
344
- transition: all var(--transition-normal);
345
- }
346
-
347
- .public-card:hover .public-card-info {
348
- padding-bottom: 20px;
349
- background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.85) 30%, rgba(15, 23, 42, 0.98) 100%);
350
- }
351
-
352
- .public-card-prompt {
353
- margin: 0;
354
- font-size: 0.9rem;
355
- color: #f1f5f9;
356
- line-height: 1.4;
357
- display: -webkit-box;
358
- -webkit-line-clamp: 2;
359
- -webkit-box-orient: vertical;
360
- overflow: hidden;
361
- font-weight: 500;
362
- }
363
-
364
- .public-card-footer {
365
- display: flex;
366
- justify-content: space-between;
367
- align-items: center;
368
- font-size: 0.75rem;
369
- color: rgba(148, 163, 184, 0.9);
370
- gap: 10px;
371
- }
372
-
373
- .public-gallery-empty {
374
- grid-column: 1 / -1;
375
- text-align: center;
376
- color: var(--text-sub);
377
- padding: 60px 30px;
378
- background: linear-gradient(135deg, rgba(15, 23, 42, 0.5) 0%, rgba(20, 30, 50, 0.5) 100%);
379
- border-radius: 20px;
380
- border: 2px dashed var(--panel-border);
381
- font-size: 1.1rem;
382
- }
383
-
384
- .public-gallery-empty > div:first-child {
385
- font-size: 48px;
386
- margin-bottom: 16px;
387
- }
388
-
389
- .icon-btn.small {
390
- width: 36px;
391
- height: 36px;
392
- font-size: 14px;
393
- background: rgba(239, 68, 68, 0.9);
394
- border-color: rgba(248, 113, 113, 0.7);
395
- transition: all var(--transition-fast);
396
- }
397
-
398
- .icon-btn.small:hover {
399
- background: rgba(239, 68, 68, 0.95);
400
- transform: scale(1.15);
401
- box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
402
- }
403
-
404
- .icon-btn.share-btn {
405
- background: rgba(59, 130, 246, 0.25);
406
- border-color: rgba(59, 130, 246, 0.4);
407
- }
408
-
409
- .icon-btn.share-btn:hover {
410
- background: rgba(59, 130, 246, 0.4);
411
- }
412
-
413
- .icon-btn.public-delete-btn {
414
- background: rgba(239, 68, 68, 0.9);
415
- border-color: rgba(248, 113, 113, 0.7);
416
- }
417
-
418
- .icon-btn.spinning {
419
- animation: spin 0.8s linear infinite;
420
- }
421
-
422
- @media (max-width: 768px) {
423
- .icon-btn.spinning {
424
- animation: spin 1s linear infinite;
425
- opacity: 0.8;
426
- }
427
- }
428
-
429
- .history-item {
430
- position: relative;
431
- aspect-ratio: 16 / 9;
432
- border-radius: 12px;
433
- overflow: hidden;
434
- border: 1px solid var(--panel-border);
435
- background: rgba(0,0,0,0.2);
436
- cursor: pointer;
437
- transition: all var(--transition-normal);
438
- animation: scaleIn 0.4s ease-out backwards;
439
- }
440
-
441
- @keyframes scaleIn {
442
- from {
443
- opacity: 0;
444
- transform: scale(0.9);
445
- }
446
- to {
447
- opacity: 1;
448
- transform: scale(1);
449
- }
450
- }
451
-
452
- .history-item:nth-child(1) { animation-delay: 0.05s; }
453
- .history-item:nth-child(2) { animation-delay: 0.1s; }
454
- .history-item:nth-child(3) { animation-delay: 0.15s; }
455
- .history-item:nth-child(4) { animation-delay: 0.2s; }
456
- .history-item:nth-child(5) { animation-delay: 0.25s; }
457
- .history-item:nth-child(6) { animation-delay: 0.3s; }
458
-
459
- .history-item img {
460
- width: 100%;
461
- height: 100%;
462
- object-fit: cover;
463
- transition: transform var(--transition-normal);
464
- }
465
-
466
- .history-item:hover {
467
- border-color: var(--panel-border-strong);
468
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
469
- transform: translateY(-4px);
470
- }
471
-
472
- .history-item:hover img {
473
- transform: scale(1.08);
474
- }
475
-
476
- .item-badge {
477
- position: absolute;
478
- top: 8px;
479
- left: 8px;
480
- background: linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(37, 99, 235, 0.95) 100%);
481
- font-size: 10px;
482
- padding: 4px 8px;
483
- border-radius: 6px;
484
- z-index: 2;
485
- backdrop-filter: blur(8px);
486
- border: 1px solid rgba(255, 255, 255, 0.2);
487
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
488
- transition: all var(--transition-fast);
489
- }
490
-
491
- .item-badge:hover {
492
- transform: scale(1.05);
493
- }
494
-
495
- .item-actions {
496
- position: absolute;
497
- bottom: 0;
498
- right: 0;
499
- left: 0;
500
- padding: 12px;
501
- background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 50%, transparent 100%);
502
- backdrop-filter: blur(8px);
503
- display: flex;
504
- justify-content: flex-end;
505
- gap: 8px;
506
- opacity: 0;
507
- transition: opacity var(--transition-normal);
508
- }
509
-
510
- .history-item:hover .item-actions {
511
- opacity: 1;
512
- }
513
-
514
- @media (hover: none) {
515
- .item-actions { opacity: 1; }
516
- }
517
-
518
- .icon-btn {
519
- width: 36px;
520
- height: 36px;
521
- border-radius: 8px;
522
- background: rgba(255,255,255,0.15);
523
- color: white;
524
- border: 1px solid rgba(255,255,255,0.2);
525
- display: flex;
526
- align-items: center;
527
- justify-content: center;
528
- backdrop-filter: blur(8px);
529
- cursor: pointer;
530
- transition: all var(--transition-fast);
531
- font-size: 16px;
532
- }
533
-
534
- .icon-btn:hover {
535
- background: rgba(255,255,255,0.25);
536
- transform: scale(1.1);
537
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
538
- }
539
-
540
- .icon-btn:active {
541
- transform: scale(0.95);
542
- }
543
-
544
- /* --- Fixed Input Section (Top) --- */
545
- .input-section {
546
- position: fixed;
547
- top: 0;
548
- left: 0;
549
- right: 0;
550
- width: 100%;
551
- display: flex;
552
- flex-direction: column;
553
- border-radius: 0;
554
- box-shadow: 0 4px 24px rgba(0,0,0,0.4);
555
- z-index: 100;
556
- border-bottom: 1px solid var(--panel-border);
557
- transition: box-shadow var(--transition-normal);
558
- }
559
-
560
- .input-section:hover {
561
- box-shadow: 0 6px 32px rgba(0,0,0,0.5);
562
- }
563
-
564
- /* Preview Bar */
565
- .preview-bar {
566
- padding: 0;
567
- max-height: 0;
568
- overflow-x: auto;
569
- overflow-y: hidden;
570
- background: rgba(15, 23, 42, 0.6);
571
- backdrop-filter: blur(12px);
572
- display: flex;
573
- gap: 8px;
574
- transition: all var(--transition-normal) cubic-bezier(0.4, 0, 0.2, 1);
575
- white-space: nowrap;
576
- max-width: 1200px;
577
- margin: 0 auto;
578
- width: 100%;
579
- scroll-behavior: smooth;
580
- }
581
-
582
- .preview-bar::-webkit-scrollbar {
583
- height: 4px;
584
- }
585
-
586
- .preview-bar::-webkit-scrollbar-track {
587
- background: rgba(0, 0, 0, 0.2);
588
- }
589
-
590
- .preview-bar::-webkit-scrollbar-thumb {
591
- background: var(--accent-color);
592
- border-radius: 2px;
593
- }
594
-
595
- .preview-bar.visible {
596
- padding: 12px 20px;
597
- max-height: 100px;
598
- }
599
-
600
- .thumb-wrapper {
601
- position: relative;
602
- width: 64px;
603
- height: 64px;
604
- flex-shrink: 0;
605
- border-radius: 8px;
606
- border: 2px solid rgba(255,255,255,0.1);
607
- overflow: hidden;
608
- transition: all var(--transition-fast);
609
- animation: slideInThumb 0.3s ease-out;
610
- }
611
-
612
- @keyframes slideInThumb {
613
- from {
614
- opacity: 0;
615
- transform: scale(0.8) translateY(-10px);
616
- }
617
- to {
618
- opacity: 1;
619
- transform: scale(1) translateY(0);
620
- }
621
- }
622
-
623
- .thumb-wrapper:hover {
624
- border-color: var(--accent-color);
625
- transform: scale(1.05);
626
- }
627
-
628
- .thumb-wrapper img {
629
- width: 100%;
630
- height: 100%;
631
- object-fit: cover;
632
- }
633
-
634
- .thumb-remove {
635
- position: absolute;
636
- top: 2px;
637
- right: 2px;
638
- width: 18px;
639
- height: 18px;
640
- background: linear-gradient(135deg, rgba(239,68,68,0.95) 0%, rgba(220,38,38,0.95) 100%);
641
- border-radius: 50%;
642
- display: flex;
643
- align-items: center;
644
- justify-content: center;
645
- font-size: 12px;
646
- cursor: pointer;
647
- border: 1px solid rgba(255,255,255,0.3);
648
- transition: all var(--transition-fast);
649
- box-shadow: 0 2px 6px rgba(0,0,0,0.3);
650
- }
651
-
652
- .thumb-remove:hover {
653
- transform: scale(1.2);
654
- background: linear-gradient(135deg, rgba(220,38,38,1) 0%, rgba(185,28,28,1) 100%);
655
- }
656
-
657
- .thumb-remove:active {
658
- transform: scale(0.9);
659
- }
660
-
661
- /* Control Bar */
662
- .control-bar {
663
- display: flex;
664
- align-items: flex-end;
665
- padding: 12px 20px;
666
- gap: 10px;
667
- max-width: 1200px;
668
- margin: 0 auto;
669
- width: 100%;
670
- }
671
-
672
- .upload-trigger {
673
- width: 44px;
674
- height: 44px;
675
- border-radius: 12px;
676
- background: rgba(255,255,255,0.05);
677
- border: 2px dashed rgba(255,255,255,0.3);
678
- color: var(--text-sub);
679
- display: flex;
680
- align-items: center;
681
- justify-content: center;
682
- font-size: 20px;
683
- flex-shrink: 0;
684
- transition: all var(--transition-normal);
685
- cursor: pointer;
686
- position: relative;
687
- overflow: hidden;
688
- }
689
-
690
- .upload-trigger::before {
691
- content: '';
692
- position: absolute;
693
- inset: 0;
694
- background: radial-gradient(circle, rgba(59, 130, 246, 0.2) 0%, transparent 70%);
695
- opacity: 0;
696
- transition: opacity var(--transition-fast);
697
- }
698
-
699
- .upload-trigger:hover, .upload-trigger.active {
700
- background: rgba(59, 130, 246, 0.15);
701
- border-color: var(--accent-color);
702
- border-style: solid;
703
- color: var(--accent-color);
704
- transform: scale(1.05);
705
- }
706
-
707
- .upload-trigger:hover::before, .upload-trigger.active::before {
708
- opacity: 1;
709
- }
710
-
711
- .upload-trigger:active {
712
- transform: scale(0.95);
713
- }
714
-
715
- /* Drag Active State */
716
- .input-section.drag-active {
717
- background: rgba(59, 130, 246, 0.1);
718
- border-color: var(--accent-color);
719
- box-shadow:
720
- 0 6px 32px rgba(59, 130, 246, 0.4),
721
- inset 0 0 0 2px var(--accent-color);
722
- animation: pulse 1s ease-in-out infinite;
723
- }
724
-
725
- @keyframes pulse {
726
- 0%, 100% {
727
- box-shadow:
728
- 0 6px 32px rgba(59, 130, 246, 0.4),
729
- inset 0 0 0 2px var(--accent-color);
730
- }
731
- 50% {
732
- box-shadow:
733
- 0 8px 40px rgba(59, 130, 246, 0.6),
734
- inset 0 0 0 3px var(--accent-color);
735
- }
736
- }
737
-
738
- .main-input {
739
- flex: 1;
740
- background: transparent;
741
- border: none;
742
- color: white;
743
- font-size: 16px;
744
- padding: 10px 5px;
745
- resize: none;
746
- max-height: 120px;
747
- outline: none;
748
- line-height: 1.5;
749
- transition: all var(--transition-fast);
750
- }
751
-
752
- .main-input::placeholder {
753
- color: rgba(255,255,255,0.3);
754
- transition: color var(--transition-fast);
755
- }
756
-
757
- .main-input:focus::placeholder {
758
- color: rgba(255,255,255,0.5);
759
- }
760
-
761
- .send-btn {
762
- height: 44px;
763
- padding: 0 20px;
764
- font-size: 15px;
765
- flex-shrink: 0;
766
- min-width: 90px;
767
- }
768
-
769
- /* Enhanced Loading Animation */
770
- .loader {
771
- width: 18px;
772
- height: 18px;
773
- border: 2px solid rgba(255,255,255,0.2);
774
- border-top-color: white;
775
- border-radius: 50%;
776
- animation: spin 0.8s linear infinite;
777
- display: none;
778
- will-change: transform;
779
- }
780
-
781
- .loading .loader { display: block; }
782
- .loading span { display: none; }
783
-
784
- @keyframes spin {
785
- from { transform: rotate(0deg); }
786
- to { transform: rotate(360deg); }
787
- }
788
-
789
- @media (prefers-reduced-motion: reduce) {
790
- .loader,
791
- .icon-btn.spinning {
792
- animation: none;
793
- opacity: 0.7;
794
- }
795
- }
796
-
797
- /* --- Enhanced Modal --- */
798
- .modal {
799
- display: none;
800
- position: fixed;
801
- top: 0;
802
- left: 0;
803
- width: 100%;
804
- height: 100%;
805
- background: rgba(0,0,0,0.85);
806
- z-index: 1000;
807
- justify-content: center;
808
- align-items: center;
809
- backdrop-filter: blur(8px);
810
- animation: fadeIn var(--transition-normal);
811
- }
812
-
813
- .modal.show {
814
- display: flex;
815
- }
816
-
817
- .modal-content {
818
- background: var(--panel-bg-strong);
819
- backdrop-filter: blur(24px) saturate(180%);
820
- width: 95%;
821
- max-width: 1000px;
822
- height: 90%;
823
- max-height: 900px;
824
- border-radius: 20px;
825
- display: flex;
826
- flex-direction: column;
827
- position: relative;
828
- border: 1px solid var(--panel-border-strong);
829
- box-shadow:
830
- 0 20px 60px rgba(0, 0, 0, 0.6),
831
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
832
- animation: modalSlideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
833
- }
834
-
835
- @keyframes modalSlideIn {
836
- from {
837
- opacity: 0;
838
- transform: scale(0.9) translateY(20px);
839
- }
840
- to {
841
- opacity: 1;
842
- transform: scale(1) translateY(0);
843
- }
844
- }
845
-
846
- .close-modal {
847
- position: absolute;
848
- top: 15px;
849
- right: 15px;
850
- width: 40px;
851
- height: 40px;
852
- border-radius: 50%;
853
- background: rgba(0,0,0,0.7);
854
- backdrop-filter: blur(8px);
855
- color: white;
856
- border: 1px solid rgba(255,255,255,0.2);
857
- z-index: 10;
858
- font-size: 24px;
859
- cursor: pointer;
860
- display: flex;
861
- align-items: center;
862
- justify-content: center;
863
- transition: all var(--transition-fast);
864
- }
865
-
866
- .close-modal:hover {
867
- background: rgba(239,68,68,0.9);
868
- transform: rotate(90deg) scale(1.1);
869
- border-color: rgba(255,255,255,0.4);
870
- }
871
-
872
- .close-modal:active {
873
- transform: rotate(90deg) scale(0.95);
874
- }
875
-
876
- .modal-img-area {
877
- flex: 1;
878
- display: flex;
879
- justify-content: center;
880
- align-items: center;
881
- background: rgba(0,0,0,0.5);
882
- overflow: hidden;
883
- border-radius: 20px 20px 0 0;
884
- position: relative;
885
- }
886
-
887
- .modal-img-area::before {
888
- content: '';
889
- position: absolute;
890
- inset: 0;
891
- background: radial-gradient(circle at center, transparent 0%, rgba(0,0,0,0.3) 100%);
892
- pointer-events: none;
893
- }
894
-
895
- .modal-img-area img {
896
- max-width: 100%;
897
- max-height: 100%;
898
- object-fit: contain;
899
- animation: imageZoomIn 0.5s ease-out;
900
- }
901
-
902
- @keyframes imageZoomIn {
903
- from {
904
- opacity: 0;
905
- transform: scale(0.95);
906
- }
907
- to {
908
- opacity: 1;
909
- transform: scale(1);
910
- }
911
- }
912
-
913
- .modal-footer {
914
- padding: 20px;
915
- background: rgba(30, 41, 59, 0.95);
916
- backdrop-filter: blur(16px);
917
- border-top: 1px solid var(--panel-border-strong);
918
- display: flex;
919
- flex-direction: column;
920
- gap: 12px;
921
- border-radius: 0 0 20px 20px;
922
- }
923
-
924
- .input-refs {
925
- display: flex;
926
- gap: 8px;
927
- overflow-x: auto;
928
- padding-bottom: 5px;
929
- scroll-behavior: smooth;
930
- }
931
-
932
- .input-refs::-webkit-scrollbar {
933
- height: 4px;
934
- }
935
-
936
- .input-refs::-webkit-scrollbar-track {
937
- background: rgba(0, 0, 0, 0.2);
938
- border-radius: 2px;
939
- }
940
-
941
- .input-refs::-webkit-scrollbar-thumb {
942
- background: var(--accent-color);
943
- border-radius: 2px;
944
- }
945
-
946
- .ref-thumb {
947
- width: 44px;
948
- height: 44px;
949
- border-radius: 8px;
950
- border: 2px solid rgba(255,255,255,0.2);
951
- cursor: pointer;
952
- transition: all var(--transition-fast);
953
- object-fit: cover;
954
- }
955
-
956
- .ref-thumb:hover {
957
- border-color: var(--accent-color);
958
- transform: scale(1.1);
959
- }
960
-
961
- .prompt-display {
962
- background: rgba(0,0,0,0.3);
963
- padding: 12px;
964
- border-radius: 10px;
965
- color: #cbd5e1;
966
- font-size: 0.9rem;
967
- max-height: 100px;
968
- overflow-y: auto;
969
- line-height: 1.5;
970
- border: 1px solid rgba(255,255,255,0.05);
971
- }
972
-
973
- .prompt-display::-webkit-scrollbar {
974
- width: 6px;
975
- }
976
-
977
- .prompt-display::-webkit-scrollbar-track {
978
- background: rgba(0, 0, 0, 0.2);
979
- border-radius: 3px;
980
- }
981
-
982
- .prompt-display::-webkit-scrollbar-thumb {
983
- background: var(--accent-color);
984
- border-radius: 3px;
985
- }
986
-
987
- /* --- Login Overlay --- */
988
- #login-overlay {
989
- position: fixed;
990
- inset: 0;
991
- z-index: 5000;
992
- display: flex;
993
- justify-content: center;
994
- align-items: center;
995
- background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
996
- animation: fadeIn 0.5s ease-out;
997
- }
998
-
999
- .login-card {
1000
- width: 340px;
1001
- padding: 40px;
1002
- text-align: center;
1003
- border-radius: 20px;
1004
- box-shadow:
1005
- 0 20px 60px rgba(0, 0, 0, 0.5),
1006
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
1007
- animation: modalSlideIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
1008
- }
1009
-
1010
- .login-card h1 {
1011
- font-size: 3rem;
1012
- margin-bottom: 10px;
1013
- animation: bounce 1s ease-in-out infinite;
1014
- }
1015
-
1016
- @keyframes bounce {
1017
- 0%, 100% { transform: translateY(0); }
1018
- 50% { transform: translateY(-10px); }
1019
- }
1020
-
1021
- .login-card h3 {
1022
- margin: 15px 0;
1023
- color: #cbd5e1;
1024
- font-weight: 600;
1025
- }
1026
-
1027
- .login-card input {
1028
- width: 100%;
1029
- padding: 14px;
1030
- margin: 20px 0;
1031
- background: rgba(0,0,0,0.4);
1032
- border: 2px solid rgba(255,255,255,0.1);
1033
- color: white;
1034
- border-radius: 10px;
1035
- font-size: 15px;
1036
- transition: all var(--transition-normal);
1037
- outline: none;
1038
- }
1039
-
1040
- .login-card input:focus {
1041
- border-color: var(--accent-color);
1042
- background: rgba(0,0,0,0.5);
1043
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
1044
- }
1045
-
1046
- .login-card .btn-3d {
1047
- width: 100%;
1048
- padding: 14px;
1049
- font-size: 16px;
1050
- }
1051
-
1052
- /* --- Mobile Responsive Design --- */
1053
- @media (max-width: 768px) {
1054
- .grid-layout {
1055
- grid-template-columns: 1fr;
1056
- gap: 12px;
1057
- }
1058
-
1059
- .public-gallery-section {
1060
- padding: 20px;
1061
- margin-top: 30px;
1062
- border-radius: 18px;
1063
- backdrop-filter: blur(10px) saturate(120%);
1064
- -webkit-backdrop-filter: blur(10px) saturate(120%);
1065
- }
1066
-
1067
- .section-title h3 {
1068
- font-size: 1.1rem;
1069
- }
1070
-
1071
- .section-subtitle {
1072
- font-size: 0.85rem;
1073
- }
1074
-
1075
- .public-grid {
1076
- grid-template-columns: 1fr;
1077
- gap: 14px;
1078
- margin-top: 16px;
1079
- }
1080
-
1081
- .public-card {
1082
- border-radius: 14px;
1083
- transition: none;
1084
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1085
- }
1086
-
1087
- .public-card:hover {
1088
- transform: none;
1089
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1090
- }
1091
-
1092
- .public-card-prompt {
1093
- font-size: 0.8rem;
1094
- -webkit-line-clamp: 2;
1095
- }
1096
-
1097
- .public-card-footer {
1098
- font-size: 0.7rem;
1099
- }
1100
-
1101
- .icon-btn.small {
1102
- width: 32px;
1103
- height: 32px;
1104
- font-size: 12px;
1105
- }
1106
-
1107
- .public-gallery-empty {
1108
- padding: 40px 20px;
1109
- border-radius: 16px;
1110
- font-size: 0.95rem;
1111
- }
1112
-
1113
- .public-gallery-empty > div:first-child {
1114
- font-size: 36px;
1115
- margin-bottom: 12px;
1116
- }
1117
-
1118
- .history-item {
1119
- animation: fadeIn 0.3s ease-out backwards;
1120
- }
1121
-
1122
- .history-item:nth-child(1) { animation-delay: 0s; }
1123
- .history-item:nth-child(2) { animation-delay: 0s; }
1124
- .history-item:nth-child(3) { animation-delay: 0s; }
1125
- .history-item:nth-child(4) { animation-delay: 0s; }
1126
- .history-item:nth-child(5) { animation-delay: 0s; }
1127
- .history-item:nth-child(6) { animation-delay: 0s; }
1128
-
1129
- .history-item:hover {
1130
- transform: none;
1131
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1132
- }
1133
-
1134
- .btn-3d {
1135
- transition: none;
1136
- }
1137
-
1138
- .btn-3d:hover {
1139
- transform: none;
1140
- }
1141
- }
1142
-
1143
- @media (max-width: 600px) {
1144
- .public-gallery-section {
1145
- padding: 16px;
1146
- margin-top: 20px;
1147
- }
1148
-
1149
- .section-title {
1150
- flex-direction: column;
1151
- }
1152
-
1153
- .section-title h3 {
1154
- font-size: 1rem;
1155
- }
1156
-
1157
- .public-grid {
1158
- grid-template-columns: 1fr;
1159
- gap: 12px;
1160
- }
1161
-
1162
- .public-card-info {
1163
- padding: 12px;
1164
- }
1165
-
1166
- .public-card:hover .public-card-info {
1167
- padding-bottom: 12px;
1168
- }
1169
- }
1170
-
1171
- /* --- Tablet Responsive Design --- */
1172
- @media (min-width: 768px) and (max-width: 1024px) {
1173
- .grid-layout {
1174
- grid-template-columns: repeat(2, 1fr);
1175
- gap: 16px;
1176
- }
1177
-
1178
- .public-grid {
1179
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1180
- gap: 18px;
1181
- }
1182
-
1183
- .public-gallery-section {
1184
- padding: 28px;
1185
- margin-top: 35px;
1186
- }
1187
-
1188
- .modal-content {
1189
- width: 90%;
1190
- height: 85%;
1191
- }
1192
-
1193
- header {
1194
- margin-top: 75px;
1195
- }
1196
-
1197
- body.has-preview header {
1198
- margin-top: 160px;
1199
- }
1200
- }
1201
-
1202
- /* --- Large Screen Optimization --- */
1203
- @media (min-width: 1200px) {
1204
- .grid-layout {
1205
- grid-template-columns: repeat(3, 1fr);
1206
- gap: 20px;
1207
- }
1208
-
1209
- .public-grid {
1210
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
1211
- gap: 28px;
1212
- }
1213
-
1214
- .public-gallery-section {
1215
- padding: 40px;
1216
- margin-top: 50px;
1217
- }
1218
-
1219
- .section-title h3 {
1220
- font-size: 1.5rem;
1221
- }
1222
-
1223
- .history-container {
1224
- padding: 15px 30px;
1225
- padding-bottom: 30px;
1226
- }
1227
-
1228
- header {
1229
- padding: 20px 30px;
1230
- }
1231
-
1232
- .control-bar {
1233
- padding: 14px 30px;
1234
- }
1235
-
1236
- .preview-bar.visible {
1237
- padding: 14px 30px;
1238
- }
1239
- }
1240
-
1241
- /* --- Accessibility & Reduced Motion --- */
1242
- @media (prefers-reduced-motion: reduce) {
1243
- *,
1244
- *::before,
1245
- *::after {
1246
- animation-duration: 0.01ms !important;
1247
- animation-iteration-count: 1 !important;
1248
- transition-duration: 0.01ms !important;
1249
- }
1250
- }
1251
-
1252
- /* --- Focus Styles for Accessibility --- */
1253
- button:focus-visible,
1254
- input:focus-visible,
1255
- textarea:focus-visible {
1256
- outline: 2px solid var(--accent-color);
1257
- outline-offset: 2px;
1258
- }
1259
-
1260
- /* --- Print Styles --- */
1261
- @media print {
1262
- .input-section,
1263
- .item-actions,
1264
- header,
1265
- .modal {
1266
- display: none !important;
1267
- }
1268
-
1269
- .history-container {
1270
- overflow: visible;
1271
- padding: 0;
1272
- }
1273
-
1274
- body {
1275
- background: white;
1276
- color: black;
1277
- }
1278
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* style.css - Enhanced Glass Design with Animations */
2
+ :root {
3
+ --bg-color: #0f172a;
4
+ --panel-bg: rgba(30, 41, 59, 0.65);
5
+ --panel-bg-strong: rgba(30, 41, 59, 0.85);
6
+ --panel-border: rgba(148, 163, 184, 0.15);
7
+ --panel-border-strong: rgba(148, 163, 184, 0.3);
8
+ --accent-color: #3b82f6;
9
+ --accent-secondary: #22d3ee;
10
+ --accent-hover: #2563eb;
11
+ --text-main: #f1f5f9;
12
+ --text-sub: #94a3b8;
13
+ --btn-shadow: #1e3a8a;
14
+ --glass-shine: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 100%);
15
+ --transition-fast: 0.2s ease;
16
+ --transition-normal: 0.3s ease;
17
+ --transition-slow: 0.5s ease;
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ margin: 0;
23
+ padding: 0;
24
+ -webkit-tap-highlight-color: transparent;
25
+ }
26
+
27
+ body {
28
+ font-family: 'Avenir Next', 'Trebuchet MS', 'Segoe UI', sans-serif;
29
+ background:
30
+ radial-gradient(700px 360px at 10% -10%, rgba(59, 130, 246, 0.25), transparent 60%),
31
+ radial-gradient(700px 420px at 90% -20%, rgba(14, 165, 233, 0.18), transparent 55%),
32
+ radial-gradient(600px 500px at 50% 20%, rgba(15, 23, 42, 0.2), transparent 60%),
33
+ var(--bg-color);
34
+ color: var(--text-main);
35
+ height: 100vh;
36
+ display: flex;
37
+ flex-direction: column;
38
+ overflow: hidden;
39
+ animation: fadeIn 0.6s ease-out;
40
+ }
41
+
42
+ @keyframes fadeIn {
43
+ from { opacity: 0; }
44
+ to { opacity: 1; }
45
+ }
46
+
47
+ /* --- Enhanced Glass Components --- */
48
+ .glass-panel {
49
+ background: var(--panel-bg);
50
+ backdrop-filter: blur(20px) saturate(180%);
51
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
52
+ border: 1px solid var(--panel-border);
53
+ position: relative;
54
+ transition: all var(--transition-normal);
55
+ will-change: transform;
56
+ }
57
+
58
+ @media (max-width: 768px) {
59
+ .status-meter {
60
+ width: 120px;
61
+ }
62
+
63
+ .prompt-link {
64
+ font-size: 0.75rem;
65
+ padding: 5px 10px;
66
+ }
67
+
68
+ .glass-panel {
69
+ backdrop-filter: blur(10px) saturate(120%);
70
+ -webkit-backdrop-filter: blur(10px) saturate(120%);
71
+ }
72
+ }
73
+
74
+ .glass-panel::before {
75
+ content: '';
76
+ position: absolute;
77
+ top: 0;
78
+ left: 0;
79
+ right: 0;
80
+ height: 1px;
81
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%);
82
+ opacity: 0.5;
83
+ }
84
+
85
+ .glass-panel:hover {
86
+ border-color: var(--panel-border-strong);
87
+ transform: translateY(-1px);
88
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
89
+ }
90
+
91
+ /* --- Enhanced 3D Buttons --- */
92
+ .btn-3d {
93
+ background: linear-gradient(135deg, var(--accent-color) 0%, #2563eb 100%);
94
+ color: white;
95
+ border: none;
96
+ border-radius: 12px;
97
+ font-weight: 600;
98
+ box-shadow:
99
+ 0 4px 0 var(--btn-shadow),
100
+ 0 5px 15px rgba(59, 130, 246, 0.3),
101
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
102
+ transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
103
+ cursor: pointer;
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ position: relative;
108
+ overflow: hidden;
109
+ }
110
+
111
+ .btn-3d::before {
112
+ content: '';
113
+ position: absolute;
114
+ top: 0;
115
+ left: -100%;
116
+ width: 100%;
117
+ height: 100%;
118
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
119
+ transition: left 0.5s;
120
+ }
121
+
122
+ .btn-3d:hover {
123
+ transform: translateY(-2px);
124
+ box-shadow:
125
+ 0 6px 0 var(--btn-shadow),
126
+ 0 8px 20px rgba(59, 130, 246, 0.4),
127
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
128
+ }
129
+
130
+ .btn-3d:hover::before {
131
+ left: 100%;
132
+ }
133
+
134
+ .btn-3d:active {
135
+ transform: translateY(4px);
136
+ box-shadow:
137
+ 0 0 0 var(--btn-shadow),
138
+ 0 2px 5px rgba(59, 130, 246, 0.3);
139
+ }
140
+
141
+ .btn-3d:disabled {
142
+ background: linear-gradient(135deg, #475569 0%, #334155 100%);
143
+ box-shadow: none;
144
+ transform: none;
145
+ opacity: 0.7;
146
+ cursor: not-allowed;
147
+ }
148
+
149
+ /* --- Layout --- */
150
+ .app-container {
151
+ max-width: 1200px;
152
+ margin: 0 auto;
153
+ width: 100%;
154
+ height: 100%;
155
+ display: flex;
156
+ flex-direction: column;
157
+ position: relative;
158
+ animation: slideUp 0.6s ease-out;
159
+ }
160
+
161
+ @keyframes slideUp {
162
+ from {
163
+ opacity: 0;
164
+ transform: translateY(20px);
165
+ }
166
+ to {
167
+ opacity: 1;
168
+ transform: translateY(0);
169
+ }
170
+ }
171
+
172
+ header {
173
+ padding: 15px 20px;
174
+ display: flex;
175
+ justify-content: space-between;
176
+ align-items: center;
177
+ flex-shrink: 0;
178
+ margin-top: 70px;
179
+ transition: margin-top var(--transition-normal);
180
+ }
181
+
182
+ header h2 {
183
+ font-size: 1.2rem;
184
+ font-weight: 700;
185
+ letter-spacing: 0.5px;
186
+ background: linear-gradient(135deg, var(--text-main) 0%, var(--text-sub) 100%);
187
+ -webkit-background-clip: text;
188
+ -webkit-text-fill-color: transparent;
189
+ background-clip: text;
190
+ }
191
+
192
+ .header-stack {
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: 8px;
196
+ width: 100%;
197
+ }
198
+
199
+ .title-row {
200
+ display: flex;
201
+ align-items: center;
202
+ gap: 12px;
203
+ flex-wrap: wrap;
204
+ }
205
+
206
+ .prompt-link {
207
+ font-size: 0.85rem;
208
+ font-weight: 600;
209
+ text-decoration: none;
210
+ color: var(--text-main);
211
+ padding: 6px 12px;
212
+ border-radius: 999px;
213
+ background: rgba(59, 130, 246, 0.15);
214
+ border: 1px solid rgba(59, 130, 246, 0.35);
215
+ transition: all var(--transition-fast);
216
+ }
217
+
218
+ .prompt-link:hover {
219
+ background: rgba(59, 130, 246, 0.3);
220
+ transform: translateY(-1px);
221
+ }
222
+
223
+ .status-bar {
224
+ display: flex;
225
+ align-items: center;
226
+ flex-wrap: wrap;
227
+ gap: 10px;
228
+ font-size: 12px;
229
+ color: var(--text-sub);
230
+ }
231
+
232
+ .status-bar::before {
233
+ content: '';
234
+ width: 6px;
235
+ height: 6px;
236
+ border-radius: 50%;
237
+ background: rgba(148, 163, 184, 0.8);
238
+ box-shadow: 0 0 0 4px rgba(148, 163, 184, 0.08);
239
+ transition: background var(--transition-fast);
240
+ }
241
+
242
+ .status-bar.is-generating::before {
243
+ background: var(--accent-secondary);
244
+ box-shadow: 0 0 0 6px rgba(34, 211, 238, 0.25);
245
+ animation: statusPulse 1.4s ease-in-out infinite;
246
+ }
247
+
248
+ .status-text {
249
+ font-weight: 500;
250
+ }
251
+
252
+ .status-meter {
253
+ position: relative;
254
+ width: 160px;
255
+ height: 6px;
256
+ background: rgba(15, 23, 42, 0.6);
257
+ border-radius: 999px;
258
+ overflow: hidden;
259
+ opacity: 0;
260
+ transform: scaleX(0.85);
261
+ transform-origin: left center;
262
+ transition: all var(--transition-fast);
263
+ }
264
+
265
+ .status-meter-fill {
266
+ height: 100%;
267
+ width: 0%;
268
+ background: linear-gradient(90deg, var(--accent-color), var(--accent-secondary));
269
+ box-shadow: 0 0 10px rgba(34, 211, 238, 0.45);
270
+ transition: width 0.9s linear;
271
+ }
272
+
273
+ .status-timer {
274
+ min-width: 44px;
275
+ text-align: right;
276
+ font-variant-numeric: tabular-nums;
277
+ opacity: 0;
278
+ transition: opacity var(--transition-fast);
279
+ }
280
+
281
+ .status-bar.is-generating .status-meter,
282
+ .status-bar.is-generating .status-timer {
283
+ opacity: 1;
284
+ transform: scaleX(1);
285
+ }
286
+
287
+ @keyframes statusPulse {
288
+ 0%, 100% {
289
+ transform: scale(1);
290
+ opacity: 0.8;
291
+ }
292
+ 50% {
293
+ transform: scale(1.25);
294
+ opacity: 1;
295
+ }
296
+ }
297
+
298
+
299
+ body.has-preview header {
300
+ margin-top: 170px;
301
+ }
302
+
303
+ /* --- History Gallery --- */
304
+ .history-container {
305
+ flex: 1;
306
+ overflow-y: auto;
307
+ padding: 10px 20px;
308
+ padding-bottom: 20px;
309
+ scroll-behavior: smooth;
310
+ }
311
+
312
+ .history-container::-webkit-scrollbar {
313
+ width: 8px;
314
+ }
315
+
316
+ .history-container::-webkit-scrollbar-track {
317
+ background: rgba(0, 0, 0, 0.2);
318
+ border-radius: 4px;
319
+ }
320
+
321
+ .history-container::-webkit-scrollbar-thumb {
322
+ background: var(--panel-bg);
323
+ border-radius: 4px;
324
+ transition: background var(--transition-fast);
325
+ }
326
+
327
+ .history-container::-webkit-scrollbar-thumb:hover {
328
+ background: var(--panel-bg-strong);
329
+ }
330
+
331
+ .grid-layout {
332
+ display: grid;
333
+ grid-template-columns: repeat(2, 1fr);
334
+ gap: 16px;
335
+ }
336
+
337
+ .public-gallery-section {
338
+ margin-top: 40px;
339
+ padding: 32px;
340
+ border-radius: 24px;
341
+ border: 1px solid var(--panel-border);
342
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(20, 30, 50, 0.8) 100%);
343
+ backdrop-filter: blur(20px) saturate(180%);
344
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
345
+ animation: slideUp 0.6s ease-out;
346
+ position: relative;
347
+ overflow: hidden;
348
+ }
349
+
350
+ .public-gallery-section::before {
351
+ content: '';
352
+ position: absolute;
353
+ top: 0;
354
+ left: 0;
355
+ right: 0;
356
+ height: 1px;
357
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%);
358
+ opacity: 0.5;
359
+ }
360
+
361
+ .section-title {
362
+ display: flex;
363
+ justify-content: space-between;
364
+ align-items: flex-start;
365
+ gap: 16px;
366
+ flex-wrap: wrap;
367
+ position: relative;
368
+ }
369
+
370
+ .section-title h3 {
371
+ margin: 0;
372
+ font-size: 1.3rem;
373
+ letter-spacing: 0.5px;
374
+ background: linear-gradient(135deg, var(--text-main) 0%, var(--text-sub) 100%);
375
+ -webkit-background-clip: text;
376
+ -webkit-text-fill-color: transparent;
377
+ background-clip: text;
378
+ font-weight: 700;
379
+ }
380
+
381
+ .section-subtitle {
382
+ margin-top: 8px;
383
+ font-size: 0.95rem;
384
+ color: var(--text-sub);
385
+ transition: color var(--transition-fast);
386
+ }
387
+
388
+ .section-subtitle.is-success {
389
+ color: #34d399;
390
+ font-weight: 500;
391
+ }
392
+
393
+ .section-subtitle.is-error {
394
+ color: #f87171;
395
+ font-weight: 500;
396
+ }
397
+
398
+ .section-actions {
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 12px;
402
+ }
403
+
404
+ .public-grid {
405
+ margin-top: 28px;
406
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
407
+ gap: 24px;
408
+ }
409
+
410
+ .public-card {
411
+ position: relative;
412
+ border-radius: 18px;
413
+ overflow: hidden;
414
+ border: 1px solid var(--panel-border);
415
+ background: rgba(15, 23, 42, 0.85);
416
+ aspect-ratio: 16 / 9;
417
+ cursor: pointer;
418
+ transition: all var(--transition-normal);
419
+ box-shadow: 0 10px 35px rgba(0, 0, 0, 0.3);
420
+ animation: scaleIn 0.5s ease-out backwards;
421
+ content-visibility: auto;
422
+ contain-intrinsic-size: 320px 180px;
423
+ will-change: transform, box-shadow;
424
+ }
425
+
426
+ @media (max-width: 768px) {
427
+ .public-card {
428
+ animation: fadeIn 0.3s ease-out backwards;
429
+ }
430
+ }
431
+
432
+ .public-card img {
433
+ width: 100%;
434
+ height: 100%;
435
+ object-fit: cover;
436
+ display: block;
437
+ transition: transform var(--transition-normal);
438
+ }
439
+
440
+ .public-card:hover {
441
+ transform: translateY(-8px);
442
+ box-shadow: 0 20px 50px rgba(59, 130, 246, 0.3);
443
+ border-color: rgba(59, 130, 246, 0.4);
444
+ }
445
+
446
+ .public-card:hover img {
447
+ transform: scale(1.08);
448
+ }
449
+
450
+ .public-card:nth-child(1) { animation-delay: 0.05s; }
451
+ .public-card:nth-child(2) { animation-delay: 0.1s; }
452
+ .public-card:nth-child(3) { animation-delay: 0.15s; }
453
+ .public-card:nth-child(4) { animation-delay: 0.2s; }
454
+ .public-card:nth-child(5) { animation-delay: 0.25s; }
455
+ .public-card:nth-child(6) { animation-delay: 0.3s; }
456
+
457
+ .public-card-info {
458
+ position: absolute;
459
+ left: 0;
460
+ right: 0;
461
+ bottom: 0;
462
+ padding: 16px;
463
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.7) 40%, rgba(15, 23, 42, 0.95) 100%);
464
+ display: flex;
465
+ flex-direction: column;
466
+ gap: 10px;
467
+ transition: all var(--transition-normal);
468
+ }
469
+
470
+ .public-card:hover .public-card-info {
471
+ padding-bottom: 20px;
472
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.85) 30%, rgba(15, 23, 42, 0.98) 100%);
473
+ }
474
+
475
+ .public-card-prompt {
476
+ margin: 0;
477
+ font-size: 0.9rem;
478
+ color: #f1f5f9;
479
+ line-height: 1.4;
480
+ display: -webkit-box;
481
+ -webkit-line-clamp: 2;
482
+ -webkit-box-orient: vertical;
483
+ overflow: hidden;
484
+ font-weight: 500;
485
+ }
486
+
487
+ .public-card-footer {
488
+ display: flex;
489
+ justify-content: space-between;
490
+ align-items: center;
491
+ font-size: 0.75rem;
492
+ color: rgba(148, 163, 184, 0.9);
493
+ gap: 10px;
494
+ }
495
+
496
+ .public-gallery-empty {
497
+ grid-column: 1 / -1;
498
+ text-align: center;
499
+ color: var(--text-sub);
500
+ padding: 60px 30px;
501
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.5) 0%, rgba(20, 30, 50, 0.5) 100%);
502
+ border-radius: 20px;
503
+ border: 2px dashed var(--panel-border);
504
+ font-size: 1.1rem;
505
+ }
506
+
507
+ .public-gallery-empty > div:first-child {
508
+ font-size: 48px;
509
+ margin-bottom: 16px;
510
+ }
511
+
512
+ .icon-btn.small {
513
+ width: 36px;
514
+ height: 36px;
515
+ font-size: 14px;
516
+ background: rgba(239, 68, 68, 0.9);
517
+ border-color: rgba(248, 113, 113, 0.7);
518
+ transition: all var(--transition-fast);
519
+ }
520
+
521
+ .icon-btn.small:hover {
522
+ background: rgba(239, 68, 68, 0.95);
523
+ transform: scale(1.15);
524
+ box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
525
+ }
526
+
527
+ .icon-btn.share-btn {
528
+ background: rgba(59, 130, 246, 0.25);
529
+ border-color: rgba(59, 130, 246, 0.4);
530
+ }
531
+
532
+ .icon-btn.share-btn:hover {
533
+ background: rgba(59, 130, 246, 0.4);
534
+ }
535
+
536
+ .icon-btn.public-delete-btn {
537
+ background: rgba(239, 68, 68, 0.9);
538
+ border-color: rgba(248, 113, 113, 0.7);
539
+ }
540
+
541
+ .icon-btn.spinning {
542
+ animation: spin 0.8s linear infinite;
543
+ }
544
+
545
+ @media (max-width: 768px) {
546
+ .icon-btn.spinning {
547
+ animation: spin 1s linear infinite;
548
+ opacity: 0.8;
549
+ }
550
+ }
551
+
552
+ .history-item {
553
+ position: relative;
554
+ aspect-ratio: 16 / 9;
555
+ border-radius: 12px;
556
+ overflow: hidden;
557
+ border: 1px solid var(--panel-border);
558
+ background: rgba(0,0,0,0.2);
559
+ cursor: pointer;
560
+ transition: all var(--transition-normal);
561
+ animation: scaleIn 0.4s ease-out backwards;
562
+ content-visibility: auto;
563
+ contain-intrinsic-size: 320px 180px;
564
+ }
565
+
566
+ @keyframes scaleIn {
567
+ from {
568
+ opacity: 0;
569
+ transform: scale(0.9);
570
+ }
571
+ to {
572
+ opacity: 1;
573
+ transform: scale(1);
574
+ }
575
+ }
576
+
577
+ .history-item:nth-child(1) { animation-delay: 0.05s; }
578
+ .history-item:nth-child(2) { animation-delay: 0.1s; }
579
+ .history-item:nth-child(3) { animation-delay: 0.15s; }
580
+ .history-item:nth-child(4) { animation-delay: 0.2s; }
581
+ .history-item:nth-child(5) { animation-delay: 0.25s; }
582
+ .history-item:nth-child(6) { animation-delay: 0.3s; }
583
+
584
+ .history-item img {
585
+ width: 100%;
586
+ height: 100%;
587
+ object-fit: cover;
588
+ transition: transform var(--transition-normal);
589
+ }
590
+
591
+ .history-item:hover {
592
+ border-color: var(--panel-border-strong);
593
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
594
+ transform: translateY(-4px);
595
+ }
596
+
597
+ .history-item:hover img {
598
+ transform: scale(1.08);
599
+ }
600
+
601
+ .item-badge {
602
+ position: absolute;
603
+ top: 8px;
604
+ left: 8px;
605
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(37, 99, 235, 0.95) 100%);
606
+ font-size: 10px;
607
+ padding: 4px 8px;
608
+ border-radius: 6px;
609
+ z-index: 2;
610
+ backdrop-filter: blur(8px);
611
+ border: 1px solid rgba(255, 255, 255, 0.2);
612
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
613
+ transition: all var(--transition-fast);
614
+ }
615
+
616
+ .item-badge:hover {
617
+ transform: scale(1.05);
618
+ }
619
+
620
+ .item-actions {
621
+ position: absolute;
622
+ bottom: 0;
623
+ right: 0;
624
+ left: 0;
625
+ padding: 12px;
626
+ background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 50%, transparent 100%);
627
+ backdrop-filter: blur(8px);
628
+ display: flex;
629
+ justify-content: flex-end;
630
+ gap: 8px;
631
+ opacity: 0;
632
+ transition: opacity var(--transition-normal);
633
+ }
634
+
635
+ .history-item:hover .item-actions {
636
+ opacity: 1;
637
+ }
638
+
639
+ @media (hover: none) {
640
+ .item-actions { opacity: 1; }
641
+ }
642
+
643
+ .icon-btn {
644
+ width: 36px;
645
+ height: 36px;
646
+ border-radius: 8px;
647
+ background: rgba(255,255,255,0.15);
648
+ color: white;
649
+ border: 1px solid rgba(255,255,255,0.2);
650
+ display: flex;
651
+ align-items: center;
652
+ justify-content: center;
653
+ backdrop-filter: blur(8px);
654
+ cursor: pointer;
655
+ transition: all var(--transition-fast);
656
+ font-size: 16px;
657
+ }
658
+
659
+ .icon-btn:hover {
660
+ background: rgba(255,255,255,0.25);
661
+ transform: scale(1.1);
662
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
663
+ }
664
+
665
+ .icon-btn:active {
666
+ transform: scale(0.95);
667
+ }
668
+
669
+ /* --- Fixed Input Section (Top) --- */
670
+ .input-section {
671
+ position: fixed;
672
+ top: 0;
673
+ left: 0;
674
+ right: 0;
675
+ width: 100%;
676
+ display: flex;
677
+ flex-direction: column;
678
+ border-radius: 0;
679
+ box-shadow: 0 4px 24px rgba(0,0,0,0.4);
680
+ z-index: 100;
681
+ border-bottom: 1px solid var(--panel-border);
682
+ transition: box-shadow var(--transition-normal);
683
+ }
684
+
685
+ .input-section:hover {
686
+ box-shadow: 0 6px 32px rgba(0,0,0,0.5);
687
+ }
688
+
689
+ /* Preview Bar */
690
+ .preview-bar {
691
+ padding: 0;
692
+ max-height: 0;
693
+ overflow-x: auto;
694
+ overflow-y: hidden;
695
+ background: rgba(15, 23, 42, 0.6);
696
+ backdrop-filter: blur(12px);
697
+ display: flex;
698
+ gap: 8px;
699
+ transition: all var(--transition-normal) cubic-bezier(0.4, 0, 0.2, 1);
700
+ white-space: nowrap;
701
+ max-width: 1200px;
702
+ margin: 0 auto;
703
+ width: 100%;
704
+ scroll-behavior: smooth;
705
+ }
706
+
707
+ .preview-bar::-webkit-scrollbar {
708
+ height: 4px;
709
+ }
710
+
711
+ .preview-bar::-webkit-scrollbar-track {
712
+ background: rgba(0, 0, 0, 0.2);
713
+ }
714
+
715
+ .preview-bar::-webkit-scrollbar-thumb {
716
+ background: var(--accent-color);
717
+ border-radius: 2px;
718
+ }
719
+
720
+ .preview-bar.visible {
721
+ padding: 12px 20px;
722
+ max-height: 100px;
723
+ }
724
+
725
+ .thumb-wrapper {
726
+ position: relative;
727
+ width: 64px;
728
+ height: 64px;
729
+ flex-shrink: 0;
730
+ border-radius: 8px;
731
+ border: 2px solid rgba(255,255,255,0.1);
732
+ overflow: hidden;
733
+ transition: all var(--transition-fast);
734
+ animation: slideInThumb 0.3s ease-out;
735
+ }
736
+
737
+ @keyframes slideInThumb {
738
+ from {
739
+ opacity: 0;
740
+ transform: scale(0.8) translateY(-10px);
741
+ }
742
+ to {
743
+ opacity: 1;
744
+ transform: scale(1) translateY(0);
745
+ }
746
+ }
747
+
748
+ .thumb-wrapper:hover {
749
+ border-color: var(--accent-color);
750
+ transform: scale(1.05);
751
+ }
752
+
753
+ .thumb-wrapper img {
754
+ width: 100%;
755
+ height: 100%;
756
+ object-fit: cover;
757
+ }
758
+
759
+ .thumb-remove {
760
+ position: absolute;
761
+ top: 2px;
762
+ right: 2px;
763
+ width: 18px;
764
+ height: 18px;
765
+ background: linear-gradient(135deg, rgba(239,68,68,0.95) 0%, rgba(220,38,38,0.95) 100%);
766
+ border-radius: 50%;
767
+ display: flex;
768
+ align-items: center;
769
+ justify-content: center;
770
+ font-size: 12px;
771
+ cursor: pointer;
772
+ border: 1px solid rgba(255,255,255,0.3);
773
+ transition: all var(--transition-fast);
774
+ box-shadow: 0 2px 6px rgba(0,0,0,0.3);
775
+ }
776
+
777
+ .thumb-remove:hover {
778
+ transform: scale(1.2);
779
+ background: linear-gradient(135deg, rgba(220,38,38,1) 0%, rgba(185,28,28,1) 100%);
780
+ }
781
+
782
+ .thumb-remove:active {
783
+ transform: scale(0.9);
784
+ }
785
+
786
+ /* Control Bar */
787
+ .control-bar {
788
+ display: flex;
789
+ align-items: flex-end;
790
+ padding: 12px 20px;
791
+ gap: 10px;
792
+ max-width: 1200px;
793
+ margin: 0 auto;
794
+ width: 100%;
795
+ }
796
+
797
+ .upload-trigger {
798
+ width: 44px;
799
+ height: 44px;
800
+ border-radius: 12px;
801
+ background: rgba(255,255,255,0.05);
802
+ border: 2px dashed rgba(255,255,255,0.3);
803
+ color: var(--text-sub);
804
+ display: flex;
805
+ align-items: center;
806
+ justify-content: center;
807
+ font-size: 20px;
808
+ flex-shrink: 0;
809
+ transition: all var(--transition-normal);
810
+ cursor: pointer;
811
+ position: relative;
812
+ overflow: hidden;
813
+ }
814
+
815
+ .upload-trigger::before {
816
+ content: '';
817
+ position: absolute;
818
+ inset: 0;
819
+ background: radial-gradient(circle, rgba(59, 130, 246, 0.2) 0%, transparent 70%);
820
+ opacity: 0;
821
+ transition: opacity var(--transition-fast);
822
+ }
823
+
824
+ .upload-trigger:hover, .upload-trigger.active {
825
+ background: rgba(59, 130, 246, 0.15);
826
+ border-color: var(--accent-color);
827
+ border-style: solid;
828
+ color: var(--accent-color);
829
+ transform: scale(1.05);
830
+ }
831
+
832
+ .upload-trigger:hover::before, .upload-trigger.active::before {
833
+ opacity: 1;
834
+ }
835
+
836
+ .upload-trigger:active {
837
+ transform: scale(0.95);
838
+ }
839
+
840
+ .upload-trigger:disabled {
841
+ opacity: 0.5;
842
+ cursor: not-allowed;
843
+ transform: none;
844
+ }
845
+
846
+ /* Drag Active State */
847
+ .input-section.drag-active {
848
+ background: rgba(59, 130, 246, 0.1);
849
+ border-color: var(--accent-color);
850
+ box-shadow:
851
+ 0 6px 32px rgba(59, 130, 246, 0.4),
852
+ inset 0 0 0 2px var(--accent-color);
853
+ animation: pulse 1s ease-in-out infinite;
854
+ }
855
+
856
+ @keyframes pulse {
857
+ 0%, 100% {
858
+ box-shadow:
859
+ 0 6px 32px rgba(59, 130, 246, 0.4),
860
+ inset 0 0 0 2px var(--accent-color);
861
+ }
862
+ 50% {
863
+ box-shadow:
864
+ 0 8px 40px rgba(59, 130, 246, 0.6),
865
+ inset 0 0 0 3px var(--accent-color);
866
+ }
867
+ }
868
+
869
+ .main-input {
870
+ flex: 1;
871
+ background: rgba(15, 23, 42, 0.35);
872
+ border: 1px solid rgba(148, 163, 184, 0.2);
873
+ border-radius: 12px;
874
+ color: white;
875
+ font-size: 16px;
876
+ padding: 10px 12px;
877
+ resize: none;
878
+ max-height: 120px;
879
+ outline: none;
880
+ line-height: 1.5;
881
+ transition: all var(--transition-fast);
882
+ }
883
+
884
+ .main-input::placeholder {
885
+ color: rgba(255,255,255,0.3);
886
+ transition: color var(--transition-fast);
887
+ }
888
+
889
+ .main-input:focus::placeholder {
890
+ color: rgba(255,255,255,0.5);
891
+ }
892
+
893
+ .main-input:focus {
894
+ border-color: rgba(59, 130, 246, 0.6);
895
+ background: rgba(15, 23, 42, 0.55);
896
+ }
897
+
898
+
899
+ .send-btn {
900
+ height: 44px;
901
+ padding: 0 20px;
902
+ font-size: 15px;
903
+ flex-shrink: 0;
904
+ min-width: 90px;
905
+ }
906
+
907
+ .ghost-btn {
908
+ height: 44px;
909
+ padding: 0 14px;
910
+ border-radius: 12px;
911
+ border: 1px solid rgba(148, 163, 184, 0.35);
912
+ background: rgba(15, 23, 42, 0.4);
913
+ color: var(--text-main);
914
+ font-weight: 600;
915
+ cursor: pointer;
916
+ transition: all var(--transition-fast);
917
+ }
918
+
919
+ .ghost-btn:hover {
920
+ border-color: rgba(59, 130, 246, 0.6);
921
+ color: white;
922
+ transform: translateY(-1px);
923
+ }
924
+
925
+ .ghost-btn:disabled {
926
+ opacity: 0.5;
927
+ cursor: not-allowed;
928
+ transform: none;
929
+ }
930
+
931
+ /* Enhanced Loading Animation */
932
+ .loader {
933
+ width: 18px;
934
+ height: 18px;
935
+ border: 2px solid rgba(255,255,255,0.2);
936
+ border-top-color: white;
937
+ border-radius: 50%;
938
+ animation: spin 0.8s linear infinite;
939
+ display: none;
940
+ will-change: transform;
941
+ }
942
+
943
+ .loading .loader { display: block; }
944
+ .loading span { display: none; }
945
+
946
+ @keyframes spin {
947
+ from { transform: rotate(0deg); }
948
+ to { transform: rotate(360deg); }
949
+ }
950
+
951
+ @media (prefers-reduced-motion: reduce) {
952
+ .loader,
953
+ .icon-btn.spinning {
954
+ animation: none;
955
+ opacity: 0.7;
956
+ }
957
+ }
958
+
959
+ /* --- Enhanced Modal --- */
960
+ .modal {
961
+ display: none;
962
+ position: fixed;
963
+ top: 0;
964
+ left: 0;
965
+ width: 100%;
966
+ height: 100%;
967
+ background: rgba(0,0,0,0.85);
968
+ z-index: 1000;
969
+ justify-content: center;
970
+ align-items: center;
971
+ backdrop-filter: blur(8px);
972
+ animation: fadeIn var(--transition-normal);
973
+ }
974
+
975
+ .modal.show {
976
+ display: flex;
977
+ }
978
+
979
+ .modal-content {
980
+ background: var(--panel-bg-strong);
981
+ backdrop-filter: blur(24px) saturate(180%);
982
+ width: 95%;
983
+ max-width: 1000px;
984
+ height: 90%;
985
+ max-height: 900px;
986
+ border-radius: 20px;
987
+ display: flex;
988
+ flex-direction: column;
989
+ position: relative;
990
+ border: 1px solid var(--panel-border-strong);
991
+ box-shadow:
992
+ 0 20px 60px rgba(0, 0, 0, 0.6),
993
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
994
+ animation: modalSlideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
995
+ }
996
+
997
+ @keyframes modalSlideIn {
998
+ from {
999
+ opacity: 0;
1000
+ transform: scale(0.9) translateY(20px);
1001
+ }
1002
+ to {
1003
+ opacity: 1;
1004
+ transform: scale(1) translateY(0);
1005
+ }
1006
+ }
1007
+
1008
+ .close-modal {
1009
+ position: absolute;
1010
+ top: 15px;
1011
+ right: 15px;
1012
+ width: 40px;
1013
+ height: 40px;
1014
+ border-radius: 50%;
1015
+ background: rgba(0,0,0,0.7);
1016
+ backdrop-filter: blur(8px);
1017
+ color: white;
1018
+ border: 1px solid rgba(255,255,255,0.2);
1019
+ z-index: 10;
1020
+ font-size: 24px;
1021
+ cursor: pointer;
1022
+ display: flex;
1023
+ align-items: center;
1024
+ justify-content: center;
1025
+ transition: all var(--transition-fast);
1026
+ }
1027
+
1028
+ .close-modal:hover {
1029
+ background: rgba(239,68,68,0.9);
1030
+ transform: rotate(90deg) scale(1.1);
1031
+ border-color: rgba(255,255,255,0.4);
1032
+ }
1033
+
1034
+ .close-modal:active {
1035
+ transform: rotate(90deg) scale(0.95);
1036
+ }
1037
+
1038
+ .modal-img-area {
1039
+ flex: 1;
1040
+ display: flex;
1041
+ justify-content: center;
1042
+ align-items: center;
1043
+ background: rgba(0,0,0,0.5);
1044
+ overflow: hidden;
1045
+ border-radius: 20px 20px 0 0;
1046
+ position: relative;
1047
+ }
1048
+
1049
+ .modal-img-area::before {
1050
+ content: '';
1051
+ position: absolute;
1052
+ inset: 0;
1053
+ background: radial-gradient(circle at center, transparent 0%, rgba(0,0,0,0.3) 100%);
1054
+ pointer-events: none;
1055
+ }
1056
+
1057
+ .modal-img-area img {
1058
+ max-width: 100%;
1059
+ max-height: 100%;
1060
+ object-fit: contain;
1061
+ animation: imageZoomIn 0.5s ease-out;
1062
+ }
1063
+
1064
+ @keyframes imageZoomIn {
1065
+ from {
1066
+ opacity: 0;
1067
+ transform: scale(0.95);
1068
+ }
1069
+ to {
1070
+ opacity: 1;
1071
+ transform: scale(1);
1072
+ }
1073
+ }
1074
+
1075
+ .modal-footer {
1076
+ padding: 20px;
1077
+ background: rgba(30, 41, 59, 0.95);
1078
+ backdrop-filter: blur(16px);
1079
+ border-top: 1px solid var(--panel-border-strong);
1080
+ display: flex;
1081
+ flex-direction: column;
1082
+ gap: 12px;
1083
+ border-radius: 0 0 20px 20px;
1084
+ }
1085
+
1086
+ .input-refs {
1087
+ display: flex;
1088
+ gap: 8px;
1089
+ overflow-x: auto;
1090
+ padding-bottom: 5px;
1091
+ scroll-behavior: smooth;
1092
+ }
1093
+
1094
+ .input-refs::-webkit-scrollbar {
1095
+ height: 4px;
1096
+ }
1097
+
1098
+ .input-refs::-webkit-scrollbar-track {
1099
+ background: rgba(0, 0, 0, 0.2);
1100
+ border-radius: 2px;
1101
+ }
1102
+
1103
+ .input-refs::-webkit-scrollbar-thumb {
1104
+ background: var(--accent-color);
1105
+ border-radius: 2px;
1106
+ }
1107
+
1108
+ .ref-thumb {
1109
+ width: 44px;
1110
+ height: 44px;
1111
+ border-radius: 8px;
1112
+ border: 2px solid rgba(255,255,255,0.2);
1113
+ cursor: pointer;
1114
+ transition: all var(--transition-fast);
1115
+ object-fit: cover;
1116
+ }
1117
+
1118
+ .ref-thumb:hover {
1119
+ border-color: var(--accent-color);
1120
+ transform: scale(1.1);
1121
+ }
1122
+
1123
+ .prompt-display {
1124
+ background: rgba(0,0,0,0.3);
1125
+ padding: 12px;
1126
+ border-radius: 10px;
1127
+ color: #cbd5e1;
1128
+ font-size: 0.9rem;
1129
+ max-height: 100px;
1130
+ overflow-y: auto;
1131
+ line-height: 1.5;
1132
+ border: 1px solid rgba(255,255,255,0.05);
1133
+ }
1134
+
1135
+ .prompt-display::-webkit-scrollbar {
1136
+ width: 6px;
1137
+ }
1138
+
1139
+ .prompt-display::-webkit-scrollbar-track {
1140
+ background: rgba(0, 0, 0, 0.2);
1141
+ border-radius: 3px;
1142
+ }
1143
+
1144
+ .prompt-display::-webkit-scrollbar-thumb {
1145
+ background: var(--accent-color);
1146
+ border-radius: 3px;
1147
+ }
1148
+
1149
+ /* --- Login Overlay --- */
1150
+ #login-overlay {
1151
+ position: fixed;
1152
+ inset: 0;
1153
+ z-index: 5000;
1154
+ display: flex;
1155
+ justify-content: center;
1156
+ align-items: center;
1157
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
1158
+ animation: fadeIn 0.5s ease-out;
1159
+ }
1160
+
1161
+ .login-card {
1162
+ width: 340px;
1163
+ padding: 40px;
1164
+ text-align: center;
1165
+ border-radius: 20px;
1166
+ box-shadow:
1167
+ 0 20px 60px rgba(0, 0, 0, 0.5),
1168
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
1169
+ animation: modalSlideIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
1170
+ }
1171
+
1172
+ .login-card h1 {
1173
+ font-size: 3rem;
1174
+ margin-bottom: 10px;
1175
+ animation: bounce 1s ease-in-out infinite;
1176
+ }
1177
+
1178
+ @keyframes bounce {
1179
+ 0%, 100% { transform: translateY(0); }
1180
+ 50% { transform: translateY(-10px); }
1181
+ }
1182
+
1183
+ .login-card h3 {
1184
+ margin: 15px 0;
1185
+ color: #cbd5e1;
1186
+ font-weight: 600;
1187
+ }
1188
+
1189
+ .login-card input {
1190
+ width: 100%;
1191
+ padding: 14px;
1192
+ margin: 20px 0;
1193
+ background: rgba(0,0,0,0.4);
1194
+ border: 2px solid rgba(255,255,255,0.1);
1195
+ color: white;
1196
+ border-radius: 10px;
1197
+ font-size: 15px;
1198
+ transition: all var(--transition-normal);
1199
+ outline: none;
1200
+ }
1201
+
1202
+ .login-card input:focus {
1203
+ border-color: var(--accent-color);
1204
+ background: rgba(0,0,0,0.5);
1205
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
1206
+ }
1207
+
1208
+ .login-card .btn-3d {
1209
+ width: 100%;
1210
+ padding: 14px;
1211
+ font-size: 16px;
1212
+ }
1213
+
1214
+ /* --- Mobile Responsive Design --- */
1215
+ @media (max-width: 768px) {
1216
+ .grid-layout {
1217
+ grid-template-columns: 1fr;
1218
+ gap: 12px;
1219
+ }
1220
+
1221
+ .public-gallery-section {
1222
+ padding: 20px;
1223
+ margin-top: 30px;
1224
+ border-radius: 18px;
1225
+ backdrop-filter: blur(10px) saturate(120%);
1226
+ -webkit-backdrop-filter: blur(10px) saturate(120%);
1227
+ }
1228
+
1229
+ .section-title h3 {
1230
+ font-size: 1.1rem;
1231
+ }
1232
+
1233
+ .section-subtitle {
1234
+ font-size: 0.85rem;
1235
+ }
1236
+
1237
+ .public-grid {
1238
+ grid-template-columns: 1fr;
1239
+ gap: 14px;
1240
+ margin-top: 16px;
1241
+ }
1242
+
1243
+ .public-card {
1244
+ border-radius: 14px;
1245
+ transition: none;
1246
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1247
+ }
1248
+
1249
+ .public-card:hover {
1250
+ transform: none;
1251
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1252
+ }
1253
+
1254
+ .public-card-prompt {
1255
+ font-size: 0.8rem;
1256
+ -webkit-line-clamp: 2;
1257
+ }
1258
+
1259
+ .public-card-footer {
1260
+ font-size: 0.7rem;
1261
+ }
1262
+
1263
+ .icon-btn.small {
1264
+ width: 32px;
1265
+ height: 32px;
1266
+ font-size: 12px;
1267
+ }
1268
+
1269
+ .public-gallery-empty {
1270
+ padding: 40px 20px;
1271
+ border-radius: 16px;
1272
+ font-size: 0.95rem;
1273
+ }
1274
+
1275
+ .public-gallery-empty > div:first-child {
1276
+ font-size: 36px;
1277
+ margin-bottom: 12px;
1278
+ }
1279
+
1280
+ .history-item {
1281
+ animation: fadeIn 0.3s ease-out backwards;
1282
+ }
1283
+
1284
+ .history-item:nth-child(1) { animation-delay: 0s; }
1285
+ .history-item:nth-child(2) { animation-delay: 0s; }
1286
+ .history-item:nth-child(3) { animation-delay: 0s; }
1287
+ .history-item:nth-child(4) { animation-delay: 0s; }
1288
+ .history-item:nth-child(5) { animation-delay: 0s; }
1289
+ .history-item:nth-child(6) { animation-delay: 0s; }
1290
+
1291
+ .history-item:hover {
1292
+ transform: none;
1293
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1294
+ }
1295
+
1296
+ .btn-3d {
1297
+ transition: none;
1298
+ }
1299
+
1300
+ .btn-3d:hover {
1301
+ transform: none;
1302
+ }
1303
+ }
1304
+
1305
+ @media (max-width: 600px) {
1306
+ .public-gallery-section {
1307
+ padding: 16px;
1308
+ margin-top: 20px;
1309
+ }
1310
+
1311
+ .section-title {
1312
+ flex-direction: column;
1313
+ }
1314
+
1315
+ .section-title h3 {
1316
+ font-size: 1rem;
1317
+ }
1318
+
1319
+ .public-grid {
1320
+ grid-template-columns: 1fr;
1321
+ gap: 12px;
1322
+ }
1323
+
1324
+ .public-card-info {
1325
+ padding: 12px;
1326
+ }
1327
+
1328
+ .public-card:hover .public-card-info {
1329
+ padding-bottom: 12px;
1330
+ }
1331
+ }
1332
+
1333
+ /* --- Tablet Responsive Design --- */
1334
+ @media (min-width: 768px) and (max-width: 1024px) {
1335
+ .grid-layout {
1336
+ grid-template-columns: repeat(2, 1fr);
1337
+ gap: 16px;
1338
+ }
1339
+
1340
+ .public-grid {
1341
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1342
+ gap: 18px;
1343
+ }
1344
+
1345
+ .public-gallery-section {
1346
+ padding: 28px;
1347
+ margin-top: 35px;
1348
+ }
1349
+
1350
+ .modal-content {
1351
+ width: 90%;
1352
+ height: 85%;
1353
+ }
1354
+
1355
+ header {
1356
+ margin-top: 75px;
1357
+ }
1358
+
1359
+ body.has-preview header {
1360
+ margin-top: 160px;
1361
+ }
1362
+ }
1363
+
1364
+ /* --- Large Screen Optimization --- */
1365
+ @media (min-width: 1200px) {
1366
+ .grid-layout {
1367
+ grid-template-columns: repeat(3, 1fr);
1368
+ gap: 20px;
1369
+ }
1370
+
1371
+ .public-grid {
1372
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
1373
+ gap: 28px;
1374
+ }
1375
+
1376
+ .public-gallery-section {
1377
+ padding: 40px;
1378
+ margin-top: 50px;
1379
+ }
1380
+
1381
+ .section-title h3 {
1382
+ font-size: 1.5rem;
1383
+ }
1384
+
1385
+ .history-container {
1386
+ padding: 15px 30px;
1387
+ padding-bottom: 30px;
1388
+ }
1389
+
1390
+ header {
1391
+ padding: 20px 30px;
1392
+ }
1393
+
1394
+ .control-bar {
1395
+ padding: 14px 30px;
1396
+ }
1397
+
1398
+ .preview-bar.visible {
1399
+ padding: 14px 30px;
1400
+ }
1401
+ }
1402
+
1403
+ /* --- Accessibility & Reduced Motion --- */
1404
+ @media (prefers-reduced-motion: reduce) {
1405
+ *,
1406
+ *::before,
1407
+ *::after {
1408
+ animation-duration: 0.01ms !important;
1409
+ animation-iteration-count: 1 !important;
1410
+ transition-duration: 0.01ms !important;
1411
+ }
1412
+ }
1413
+
1414
+ /* --- Focus Styles for Accessibility --- */
1415
+ button:focus-visible,
1416
+ input:focus-visible,
1417
+ textarea:focus-visible {
1418
+ outline: 2px solid var(--accent-color);
1419
+ outline-offset: 2px;
1420
+ }
1421
+
1422
+ /* --- Print Styles --- */
1423
+ @media print {
1424
+ .input-section,
1425
+ .item-actions,
1426
+ header,
1427
+ .modal {
1428
+ display: none !important;
1429
+ }
1430
+
1431
+ .history-container {
1432
+ overflow: visible;
1433
+ padding: 0;
1434
+ }
1435
+
1436
+ body {
1437
+ background: white;
1438
+ color: black;
1439
+ }
1440
+ }