Files changed (2) hide show
  1. index.html +292 -176
  2. style.css +115 -50
index.html CHANGED
@@ -258,41 +258,66 @@
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 processFiles(files) {
277
- const maxImages = 16;
278
- const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
279
-
280
- for (const file of imageFiles) {
281
- if (AppState.currentImages.length >= maxImages) {
282
- alert(`最多只能上传 ${maxImages} 张图片`);
283
- break;
284
- }
285
-
286
- try {
287
- const base64 = await this.fileToBase64(file);
288
- AppState.currentImages.push(base64);
289
- } catch (err) {
290
- console.error('图片读取失败:', err);
291
- }
292
- }
293
-
294
- PreviewManager.render();
295
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
  // 移除指定索引的图片
298
  removeAt(index) {
@@ -386,55 +411,59 @@
386
  }
387
  },
388
 
389
- render() {
390
- this.container.innerHTML = '';
391
-
392
- if (AppState.galleryData.length === 0) {
393
- this.container.innerHTML = `
394
- <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
395
- <div style="font-size: 48px; margin-bottom: 10px;">🎨</div>
396
- <div>暂无作品,开始创作吧!</div>
397
- </div>
398
- `;
399
- return;
400
- }
401
-
402
- AppState.galleryData.forEach(item => {
403
- const card = this.createCard(item);
404
- this.container.appendChild(card);
405
- });
406
- },
 
 
407
 
408
- createCard(item) {
409
- const el = document.createElement('div');
410
- el.className = 'history-item';
411
- el.dataset.id = item.id;
412
-
413
- // 参考图标记
414
- if (item.inputImages && item.inputImages.length > 0) {
415
- const badge = document.createElement('div');
416
- badge.className = 'item-badge';
417
- badge.textContent = `📎 ${item.inputImages.length}`;
418
- el.appendChild(badge);
419
- }
420
-
421
- // 主图片 - 使用 DOM API 设置 src
422
- const img = document.createElement('img');
423
- img.loading = 'lazy';
424
- img.src = item.image; // 关键:直接赋值,不用模板字符串
425
- img.onerror = () => {
426
- console.error('图片加载失败, ID:', item.id);
427
- 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>';
428
- };
429
- el.appendChild(img);
430
-
 
 
431
  // 操作按钮
432
  const actions = document.createElement('div');
433
  actions.className = 'item-actions';
434
 
435
  const shareBtn = document.createElement('button');
436
  shareBtn.className = 'icon-btn share-btn';
437
- shareBtn.innerHTML = '🌐';
438
  shareBtn.title = '发布到公共画廊';
439
  shareBtn.setAttribute('aria-label', '发布到公共画廊');
440
  shareBtn.onclick = (e) => {
@@ -444,7 +473,8 @@
444
 
445
  const downloadBtn = document.createElement('button');
446
  downloadBtn.className = 'icon-btn';
447
- downloadBtn.innerHTML = '⬇';
 
448
  downloadBtn.onclick = (e) => {
449
  e.stopPropagation();
450
  this.downloadImage(item);
@@ -453,7 +483,8 @@
453
  const deleteBtn = document.createElement('button');
454
  deleteBtn.className = 'icon-btn';
455
  deleteBtn.style.background = 'rgba(239,68,68,0.8)';
456
- deleteBtn.innerHTML = '🗑';
 
457
  deleteBtn.onclick = (e) => {
458
  e.stopPropagation();
459
  this.deleteItem(item.id);
@@ -464,11 +495,11 @@
464
  actions.appendChild(deleteBtn);
465
  el.appendChild(actions);
466
 
467
- // 点击打开弹窗
468
- el.onclick = () => ModalManager.open(item);
469
-
470
- return el;
471
- },
472
 
473
  downloadImage(item) {
474
  const link = document.createElement('a');
@@ -586,7 +617,10 @@
586
  defaultHint: DEFAULT_PUBLIC_HINT,
587
  hintTimer: null,
588
  syncTimer: null,
589
- syncInterval: 10000,
 
 
 
590
 
591
  init() {
592
  this.container = document.getElementById('public-gallery');
@@ -602,12 +636,15 @@
602
  this.refreshBtn.onclick = () => this.fetch();
603
  }
604
 
605
- this.startRealtimeSync();
 
 
 
 
606
  },
607
 
608
  startRealtimeSync() {
609
- this.fetch();
610
- this.syncTimer = setInterval(() => this.fetch(), this.syncInterval);
611
  },
612
 
613
  stopRealtimeSync() {
@@ -658,13 +695,39 @@
658
  this.refreshBtn.classList.toggle('spinning', loading);
659
  this.refreshBtn.disabled = loading;
660
  }
 
 
 
 
 
 
 
 
 
 
 
 
661
  },
662
 
663
  async fetch() {
664
  if (!this.container) return;
 
 
 
 
 
 
 
 
665
  this.setLoading(true);
666
  try {
667
- const res = await fetch('/api/public-gallery');
 
 
 
 
 
 
668
  const data = await res.json();
669
 
670
  if (!res.ok || !data.success) {
@@ -676,8 +739,14 @@
676
  this.setHint(this.defaultHint);
677
  } catch (error) {
678
  console.error('公共画廊加载失败:', error);
679
- this.setHint('公共画廊加载失败,请稍后重试', 'error');
680
- this.showEmpty('无法加载公共画廊,请稍后重试');
 
 
 
 
 
 
681
  } finally {
682
  this.setLoading(false);
683
  }
@@ -702,19 +771,24 @@
702
  }
703
 
704
  this.container.innerHTML = '';
 
705
  AppState.publicGalleryData.forEach(item => {
706
  const card = this.createCard(item);
707
- this.container.appendChild(card);
708
  });
 
709
  },
710
 
711
  createCard(item) {
712
  const card = document.createElement('div');
713
  card.className = 'public-card';
 
714
 
715
  const img = document.createElement('img');
716
  img.loading = 'lazy';
 
717
  img.src = item.image;
 
718
  card.appendChild(img);
719
 
720
  const info = document.createElement('div');
@@ -723,6 +797,7 @@
723
  const promptText = document.createElement('p');
724
  promptText.className = 'public-card-prompt';
725
  promptText.textContent = item.prompt || '未提供提示词';
 
726
  info.appendChild(promptText);
727
 
728
  const footer = document.createElement('div');
@@ -735,7 +810,7 @@
735
  if (this.canDelete(item.id)) {
736
  const deleteBtn = document.createElement('button');
737
  deleteBtn.className = 'icon-btn small public-delete-btn';
738
- deleteBtn.innerHTML = '🗑';
739
  deleteBtn.title = '删除这张作品';
740
  deleteBtn.onclick = (e) => {
741
  e.stopPropagation();
@@ -858,91 +933,128 @@
858
  const Generator = {
859
  sendBtn: null,
860
  textarea: null,
 
 
 
861
 
862
  init() {
863
  this.sendBtn = document.getElementById('send-btn');
864
  this.textarea = document.getElementById('prompt');
 
865
 
866
- this.sendBtn.onclick = () => this.generate();
867
-
868
- // 输入框自动高度
869
- this.textarea.addEventListener('input', () => {
870
- this.textarea.style.height = 'auto';
871
- this.textarea.style.height = this.textarea.scrollHeight + 'px';
872
- });
873
-
874
- // 回车发送(Shift+Enter 换行)
875
- this.textarea.addEventListener('keydown', (e) => {
876
- if (e.key === 'Enter' && !e.shiftKey) {
877
- e.preventDefault();
878
- this.generate();
879
- }
880
- });
881
- },
882
-
883
- setLoading(loading) {
884
- if (loading) {
885
- this.sendBtn.classList.add('loading');
886
- this.sendBtn.disabled = true;
887
- } else {
888
- this.sendBtn.classList.remove('loading');
889
- this.sendBtn.disabled = false;
890
- }
891
- },
892
-
893
- async generate() {
894
- const prompt = this.textarea.value.trim();
895
- if (!prompt) {
896
- alert('请输入提示词');
897
- return;
898
- }
899
-
900
- this.setLoading(true);
901
-
902
- try {
903
- const response = await fetch('/api/generate', {
904
- method: 'POST',
905
- headers: { 'Content-Type': 'application/json' },
906
- body: JSON.stringify({
907
- prompt: prompt,
908
- images: AppState.currentImages
909
- })
910
- });
911
-
912
- const data = await response.json();
913
-
914
- if (!response.ok || !data.success) {
915
- throw new Error(data.message || '生成失败');
916
- }
917
-
918
- // 验证返回的图片数据
919
- if (!data.image || !data.image.startsWith('data:image')) {
920
- throw new Error('返回的图片数据无效');
921
- }
922
-
923
- // 保存到数据库
924
- await Database.save({
925
- prompt: prompt,
926
- image: data.image,
927
- inputImages: [...AppState.currentImages]
928
- });
929
-
930
- // 刷新画廊
931
- await GalleryManager.load();
932
-
933
- // 清空输入
934
- this.textarea.value = '';
935
- this.textarea.style.height = 'auto';
936
- ImageHandler.clear();
937
-
938
- } catch (err) {
939
- console.error('生成失败:', err);
940
- alert('生成失败: ' + err.message);
941
- } finally {
942
- this.setLoading(false);
943
- }
944
- }
945
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
946
 
947
  // ============================================
948
  // 认证模块
@@ -1102,8 +1214,12 @@
1102
  }
1103
  }
1104
 
1105
- // 启动应用
1106
- document.addEventListener('DOMContentLoaded', initApp);
1107
- </script>
 
 
 
 
1108
  </body>
1109
  </html>
 
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) {
 
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) => {
 
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);
 
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);
 
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');
 
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');
 
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() {
 
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) {
 
739
  this.setHint(this.defaultHint);
740
  } catch (error) {
741
  console.error('公共画廊加载失败:', error);
742
+ if (error.name === 'AbortError') {
743
+ this.setHint('加载超时,请检查网络', 'error');
744
+ } else {
745
+ this.setHint('加载失败,请稍后重试', 'error');
746
+ }
747
+ if (AppState.publicGalleryData.length === 0) {
748
+ this.showEmpty('无法加载公共画廊,请稍后重试');
749
+ }
750
  } finally {
751
  this.setLoading(false);
752
  }
 
771
  }
772
 
773
  this.container.innerHTML = '';
774
+ const fragment = document.createDocumentFragment();
775
  AppState.publicGalleryData.forEach(item => {
776
  const card = this.createCard(item);
777
+ fragment.appendChild(card);
778
  });
779
+ this.container.appendChild(fragment);
780
  },
781
 
782
  createCard(item) {
783
  const card = document.createElement('div');
784
  card.className = 'public-card';
785
+ card.dataset.id = item.id;
786
 
787
  const img = document.createElement('img');
788
  img.loading = 'lazy';
789
+ img.decoding = 'async';
790
  img.src = item.image;
791
+ img.alt = item.prompt || '创意作品';
792
  card.appendChild(img);
793
 
794
  const info = document.createElement('div');
 
797
  const promptText = document.createElement('p');
798
  promptText.className = 'public-card-prompt';
799
  promptText.textContent = item.prompt || '未提供提示词';
800
+ promptText.title = item.prompt;
801
  info.appendChild(promptText);
802
 
803
  const footer = document.createElement('div');
 
810
  if (this.canDelete(item.id)) {
811
  const deleteBtn = document.createElement('button');
812
  deleteBtn.className = 'icon-btn small public-delete-btn';
813
+ deleteBtn.textContent = '🗑';
814
  deleteBtn.title = '删除这张作品';
815
  deleteBtn.onclick = (e) => {
816
  e.stopPropagation();
 
933
  const Generator = {
934
  sendBtn: null,
935
  textarea: null,
936
+ statusBar: null,
937
+ isGenerating: false,
938
+ generateTimeout: null,
939
 
940
  init() {
941
  this.sendBtn = document.getElementById('send-btn');
942
  this.textarea = document.getElementById('prompt');
943
+ this.statusBar = document.getElementById('status-bar');
944
 
945
+ this.sendBtn.onclick = () => this.generate();
946
+
947
+ // 输入框自动高度
948
+ this.textarea.addEventListener('input', () => {
949
+ this.textarea.style.height = 'auto';
950
+ this.textarea.style.height = this.textarea.scrollHeight + 'px';
951
+ });
952
+
953
+ // 回车发送(Shift+Enter 换行)
954
+ this.textarea.addEventListener('keydown', (e) => {
955
+ if (e.key === 'Enter' && !e.shiftKey) {
956
+ e.preventDefault();
957
+ this.generate();
958
+ }
959
+ });
960
+ },
961
+
962
+ setLoading(loading) {
963
+ this.isGenerating = loading;
964
+ if (loading) {
965
+ this.sendBtn.classList.add('loading');
966
+ this.sendBtn.disabled = true;
967
+ this.statusBar.textContent = '⏳ 生成中...';
968
+
969
+ // 防止生成超时 - 3分钟后强制停止
970
+ clearTimeout(this.generateTimeout);
971
+ this.generateTimeout = setTimeout(() => {
972
+ if (this.isGenerating) {
973
+ this.setLoading(false);
974
+ this.statusBar.textContent = '生成超时,请重试';
975
+ alert('生成超时,请检查网络后重试');
976
+ }
977
+ }, 180000);
978
+ } else {
979
+ this.sendBtn.classList.remove('loading');
980
+ this.sendBtn.disabled = false;
981
+ this.statusBar.textContent = 'Ready';
982
+ clearTimeout(this.generateTimeout);
983
+ }
984
+ },
985
+
986
+ async generate() {
987
+ if (this.isGenerating) return;
988
+
989
+ const prompt = this.textarea.value.trim();
990
+ if (!prompt) {
991
+ alert('请输入提示词');
992
+ return;
993
+ }
994
+
995
+ this.setLoading(true);
996
+
997
+ try {
998
+ const controller = new AbortController();
999
+ const timeoutId = setTimeout(() => controller.abort(), 120000);
1000
+
1001
+ const response = await fetch('/api/generate', {
1002
+ method: 'POST',
1003
+ headers: { 'Content-Type': 'application/json' },
1004
+ body: JSON.stringify({
1005
+ prompt: prompt,
1006
+ images: AppState.currentImages
1007
+ }),
1008
+ signal: controller.signal
1009
+ });
1010
+ clearTimeout(timeoutId);
1011
+
1012
+ const data = await response.json();
1013
+
1014
+ if (!response.ok || !data.success) {
1015
+ throw new Error(data.message || '生成失败');
1016
+ }
1017
+
1018
+ // 验证返回的图片数据
1019
+ if (!data.image || !data.image.startsWith('data:image')) {
1020
+ throw new Error('返回的图片数据无效');
1021
+ }
1022
+
1023
+ // 保存到数据库
1024
+ await Database.save({
1025
+ prompt: prompt,
1026
+ image: data.image,
1027
+ inputImages: [...AppState.currentImages]
1028
+ });
1029
+
1030
+ // 刷新画廊
1031
+ await GalleryManager.load();
1032
+
1033
+ // 清空输入
1034
+ this.textarea.value = '';
1035
+ this.textarea.style.height = 'auto';
1036
+ ImageHandler.clear();
1037
+ this.statusBar.textContent = '✅ 生成成功';
1038
+
1039
+ } catch (err) {
1040
+ console.error('生成失败:', err);
1041
+ if (err.name === 'AbortError') {
1042
+ this.statusBar.textContent = '⚠️ 生成超时';
1043
+ alert('生成超时,请检查网络后重试');
1044
+ } else {
1045
+ this.statusBar.textContent = '❌ 生成失败';
1046
+ alert('生成失败: ' + err.message);
1047
+ }
1048
+ } finally {
1049
+ this.setLoading(false);
1050
+ setTimeout(() => {
1051
+ if (this.statusBar.textContent !== 'Ready') {
1052
+ this.statusBar.textContent = 'Ready';
1053
+ }
1054
+ }, 3000);
1055
+ }
1056
+ }
1057
+ };
1058
 
1059
  // ============================================
1060
  // 认证模块
 
1214
  }
1215
  }
1216
 
1217
+ // 启动应用
1218
+ if (document.readyState === 'loading') {
1219
+ document.addEventListener('DOMContentLoaded', initApp);
1220
+ } else {
1221
+ initApp();
1222
+ }
1223
+ </script>
1224
  </body>
1225
  </html>
style.css CHANGED
@@ -47,6 +47,14 @@ body {
47
  border: 1px solid var(--panel-border);
48
  position: relative;
49
  transition: all var(--transition-normal);
 
 
 
 
 
 
 
 
50
  }
51
 
52
  .glass-panel::before {
@@ -289,6 +297,13 @@ body.has-preview header {
289
  transition: all var(--transition-normal);
290
  box-shadow: 0 10px 35px rgba(0, 0, 0, 0.3);
291
  animation: scaleIn 0.5s ease-out backwards;
 
 
 
 
 
 
 
292
  }
293
 
294
  .public-card img {
@@ -404,6 +419,13 @@ body.has-preview header {
404
  animation: spin 0.8s linear infinite;
405
  }
406
 
 
 
 
 
 
 
 
407
  .history-item {
408
  position: relative;
409
  aspect-ratio: 16 / 9;
@@ -751,17 +773,27 @@ body.has-preview header {
751
  border: 2px solid rgba(255,255,255,0.2);
752
  border-top-color: white;
753
  border-radius: 50%;
754
- animation: spin 0.8s cubic-bezier(0.5, 0.2, 0.5, 0.8) infinite;
755
  display: none;
 
756
  }
757
 
758
  .loading .loader { display: block; }
759
  .loading span { display: none; }
760
 
761
  @keyframes spin {
 
762
  to { transform: rotate(360deg); }
763
  }
764
 
 
 
 
 
 
 
 
 
765
  /* --- Enhanced Modal --- */
766
  .modal {
767
  display: none;
@@ -1019,55 +1051,88 @@ body.has-preview header {
1019
 
1020
  /* --- Mobile Responsive Design --- */
1021
  @media (max-width: 768px) {
1022
- .public-gallery-section {
1023
- padding: 20px;
1024
- margin-top: 30px;
1025
- border-radius: 18px;
1026
- }
1027
-
1028
- .section-title h3 {
1029
- font-size: 1.1rem;
1030
- }
1031
-
1032
- .section-subtitle {
1033
- font-size: 0.85rem;
1034
- }
1035
-
1036
- .public-grid {
1037
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
1038
- gap: 14px;
1039
- margin-top: 16px;
1040
- }
1041
-
1042
- .public-card {
1043
- border-radius: 14px;
1044
- }
1045
-
1046
- .public-card-prompt {
1047
- font-size: 0.8rem;
1048
- -webkit-line-clamp: 2;
1049
- }
1050
-
1051
- .public-card-footer {
1052
- font-size: 0.7rem;
1053
- }
1054
-
1055
- .icon-btn.small {
1056
- width: 32px;
1057
- height: 32px;
1058
- font-size: 12px;
1059
- }
1060
-
1061
- .public-gallery-empty {
1062
- padding: 40px 20px;
1063
- border-radius: 16px;
1064
- font-size: 0.95rem;
1065
- }
1066
-
1067
- .public-gallery-empty > div:first-child {
1068
- font-size: 36px;
1069
- margin-bottom: 12px;
1070
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1071
  }
1072
 
1073
  @media (max-width: 600px) {
 
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 {
 
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 {
 
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;
 
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;
 
1051
 
1052
  /* --- Mobile Responsive Design --- */
1053
  @media (max-width: 768px) {
1054
+ .public-gallery-section {
1055
+ padding: 20px;
1056
+ margin-top: 30px;
1057
+ border-radius: 18px;
1058
+ backdrop-filter: blur(10px) saturate(120%);
1059
+ -webkit-backdrop-filter: blur(10px) saturate(120%);
1060
+ }
1061
+
1062
+ .section-title h3 {
1063
+ font-size: 1.1rem;
1064
+ }
1065
+
1066
+ .section-subtitle {
1067
+ font-size: 0.85rem;
1068
+ }
1069
+
1070
+ .public-grid {
1071
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
1072
+ gap: 14px;
1073
+ margin-top: 16px;
1074
+ }
1075
+
1076
+ .public-card {
1077
+ border-radius: 14px;
1078
+ transition: none;
1079
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1080
+ }
1081
+
1082
+ .public-card:hover {
1083
+ transform: none;
1084
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1085
+ }
1086
+
1087
+ .public-card-prompt {
1088
+ font-size: 0.8rem;
1089
+ -webkit-line-clamp: 2;
1090
+ }
1091
+
1092
+ .public-card-footer {
1093
+ font-size: 0.7rem;
1094
+ }
1095
+
1096
+ .icon-btn.small {
1097
+ width: 32px;
1098
+ height: 32px;
1099
+ font-size: 12px;
1100
+ }
1101
+
1102
+ .public-gallery-empty {
1103
+ padding: 40px 20px;
1104
+ border-radius: 16px;
1105
+ font-size: 0.95rem;
1106
+ }
1107
+
1108
+ .public-gallery-empty > div:first-child {
1109
+ font-size: 36px;
1110
+ margin-bottom: 12px;
1111
+ }
1112
+
1113
+ .history-item {
1114
+ animation: fadeIn 0.3s ease-out backwards;
1115
+ }
1116
+
1117
+ .history-item:nth-child(1) { animation-delay: 0s; }
1118
+ .history-item:nth-child(2) { animation-delay: 0s; }
1119
+ .history-item:nth-child(3) { animation-delay: 0s; }
1120
+ .history-item:nth-child(4) { animation-delay: 0s; }
1121
+ .history-item:nth-child(5) { animation-delay: 0s; }
1122
+ .history-item:nth-child(6) { animation-delay: 0s; }
1123
+
1124
+ .history-item:hover {
1125
+ transform: none;
1126
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1127
+ }
1128
+
1129
+ .btn-3d {
1130
+ transition: none;
1131
+ }
1132
+
1133
+ .btn-3d:hover {
1134
+ transform: none;
1135
+ }
1136
  }
1137
 
1138
  @media (max-width: 600px) {