Phoe2004 commited on
Commit
84d1aad
·
verified ·
1 Parent(s): 8ce2809

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +281 -3
index.html CHANGED
@@ -448,6 +448,85 @@
448
  color:var(--ink);
449
  border:1px solid var(--line);
450
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  </style>
452
  </head>
453
  <body>
@@ -523,7 +602,7 @@
523
  <div class="output" id="output">
524
  <audio id="player" controls></audio>
525
  <div class="actions">
526
- <a class="btn-secondary btn-download" id="downloadLink" download="voice.mp3">
527
  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
528
  <path d="M12 3v12m0 0l-4.5-4.5M12 15l4.5-4.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
529
  <path d="M5 19.5h14" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
@@ -556,6 +635,14 @@
556
  </div>
557
  </div>
558
 
 
 
 
 
 
 
 
 
559
  <div class="modal-section">
560
  <div class="modal-row">
561
  <span class="modal-row-label" data-i18n="developer">ဖန်တီးသူ</span>
@@ -614,7 +701,11 @@
614
  statusRewardDownload: 'ဒေါင်းလုဒ် အခမဲ့ ရယူရန် ကြော်ငြာ ပြသပါမည်...',
615
  errSkippedDownload: 'ဒေါင်းလုဒ်ရန် ကြော်ငြာကို အပြည့်အစုံ ကြည့်ပေးပါ။',
616
  statusSaved: 'Downloads ဖိုလ်ဒါသို့ သိမ်းပြီးပါပြီ။',
617
- errSaveFailed: 'ဒေါင်းလုဒ် မအောင်မြင်ပါ။'
 
 
 
 
618
  },
619
  en: {
620
  eyebrow: 'Voice Studio · Free Text-to-Speech',
@@ -648,7 +739,11 @@
648
  statusRewardDownload: 'Showing a quick ad to unlock the free download...',
649
  errSkippedDownload: 'Please watch the full ad to download for free.',
650
  statusSaved: 'Saved to your Downloads folder.',
651
- errSaveFailed: 'Download failed.'
 
 
 
 
652
  }
653
  };
654
 
@@ -675,6 +770,7 @@
675
  uiLang = lang;
676
  localStorage.setItem('vs_ui_lang', lang);
677
  applyTranslations();
 
678
  }
679
 
680
  applyTranslations();
@@ -691,6 +787,167 @@
691
  btn.addEventListener('click', () => setUiLang(btn.dataset.lang));
692
  });
693
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  // ---- Build signature waveform ----
695
  const wave = document.getElementById('wave');
696
  const BAR_COUNT = 20;
@@ -926,6 +1183,27 @@
926
  status.textContent = t('statusDone');
927
  player.play().catch(() => {});
928
  notifyInterstitialCheckpoint();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
929
  } catch (e) {
930
  status.textContent = e.message;
931
  status.classList.add('error');
 
448
  color:var(--ink);
449
  border:1px solid var(--line);
450
  }
451
+
452
+ /* ---------- Voice history (inside menu) ---------- */
453
+ .history-list{
454
+ display:flex;
455
+ flex-direction:column;
456
+ gap:8px;
457
+ margin-top:10px;
458
+ max-height:220px;
459
+ overflow-y:auto;
460
+ }
461
+ .history-item{
462
+ display:flex;
463
+ align-items:center;
464
+ gap:10px;
465
+ padding:8px;
466
+ border:1px solid var(--line);
467
+ border-radius:10px;
468
+ background:var(--surface-2);
469
+ }
470
+ .history-play{
471
+ width:30px;
472
+ height:30px;
473
+ border-radius:50%;
474
+ background:var(--accent);
475
+ border:none;
476
+ color:#fff;
477
+ display:flex;
478
+ align-items:center;
479
+ justify-content:center;
480
+ flex-shrink:0;
481
+ cursor:pointer;
482
+ padding:0;
483
+ }
484
+ .history-play svg{ width:13px; height:13px; }
485
+ .history-play[disabled]{ background:#C7CCD6; cursor:not-allowed; }
486
+ .history-meta{ flex:1; min-width:0; }
487
+ .history-text{
488
+ font-family:var(--mm);
489
+ font-size:13px;
490
+ color:var(--ink);
491
+ white-space:nowrap;
492
+ overflow:hidden;
493
+ text-overflow:ellipsis;
494
+ }
495
+ .history-sub{
496
+ font-family:var(--sans);
497
+ font-size:11px;
498
+ color:var(--ink-soft);
499
+ margin-top:2px;
500
+ }
501
+ .history-delete{
502
+ width:26px;
503
+ height:26px;
504
+ border-radius:50%;
505
+ border:none;
506
+ background:transparent;
507
+ color:var(--ink-soft);
508
+ font-size:17px;
509
+ line-height:1;
510
+ cursor:pointer;
511
+ flex-shrink:0;
512
+ }
513
+ .history-empty{
514
+ font-family:var(--sans);
515
+ font-size:13px;
516
+ color:var(--ink-soft);
517
+ padding:8px 0 0;
518
+ }
519
+ .history-clear{
520
+ margin-top:10px;
521
+ font-family:var(--sans);
522
+ font-size:12px;
523
+ font-weight:600;
524
+ color:#D14343;
525
+ background:none;
526
+ border:none;
527
+ cursor:pointer;
528
+ padding:0;
529
+ }
530
  </style>
531
  </head>
532
  <body>
 
602
  <div class="output" id="output">
603
  <audio id="player" controls></audio>
604
  <div class="actions">
605
+ <a class="btn-primary btn-download" id="downloadLink" download="voice.mp3">
606
  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
607
  <path d="M12 3v12m0 0l-4.5-4.5M12 15l4.5-4.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
608
  <path d="M5 19.5h14" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
 
635
  </div>
636
  </div>
637
 
638
+ <div class="modal-section">
639
+ <div class="modal-row-label" data-i18n="historyLabel">Voice History</div>
640
+ <div id="historyList" class="history-list"></div>
641
+ <div id="historyEmpty" class="history-empty" data-i18n="historyEmpty">မှတ်တမ်း မရှိသေးပါ။</div>
642
+ <button id="clearHistoryBtn" class="history-clear" data-i18n="clearHistory">မှတ်တမ်း ရှင်းမည်</button>
643
+ <audio id="historyAudio" style="display:none"></audio>
644
+ </div>
645
+
646
  <div class="modal-section">
647
  <div class="modal-row">
648
  <span class="modal-row-label" data-i18n="developer">ဖန်တီးသူ</span>
 
701
  statusRewardDownload: 'ဒေါင်းလုဒ် အခမဲ့ ရယူရန် ကြော်ငြာ ပြသပါမည်...',
702
  errSkippedDownload: 'ဒေါင်းလုဒ်ရန် ကြော်ငြာကို အပြည့်အစုံ ကြည့်ပေးပါ။',
703
  statusSaved: 'Downloads ဖိုလ်ဒါသို့ သိမ်းပြီးပါပြီ။',
704
+ errSaveFailed: 'ဒေါင်းလုဒ် မအောင်မြင်ပါ။',
705
+ historyLabel: 'အသံ မှတ်တမ်း',
706
+ historyEmpty: 'မှတ်တမ်း မရှိသေးပါ။',
707
+ clearHistory: 'မှတ်တမ်း ရှင်းမည်',
708
+ historyAudioUnavailable: 'ဖိုင်ကြီးလွန်းသောကြောင့် ပြန်နားထောင်ရန် မသိမ်းထားပါ'
709
  },
710
  en: {
711
  eyebrow: 'Voice Studio · Free Text-to-Speech',
 
739
  statusRewardDownload: 'Showing a quick ad to unlock the free download...',
740
  errSkippedDownload: 'Please watch the full ad to download for free.',
741
  statusSaved: 'Saved to your Downloads folder.',
742
+ errSaveFailed: 'Download failed.',
743
+ historyLabel: 'Voice History',
744
+ historyEmpty: 'No history yet.',
745
+ clearHistory: 'Clear history',
746
+ historyAudioUnavailable: 'Too large to keep for replay'
747
  }
748
  };
749
 
 
770
  uiLang = lang;
771
  localStorage.setItem('vs_ui_lang', lang);
772
  applyTranslations();
773
+ renderHistory();
774
  }
775
 
776
  applyTranslations();
 
787
  btn.addEventListener('click', () => setUiLang(btn.dataset.lang));
788
  });
789
 
790
+ // ---- Voice history (stored in localStorage, played back from the menu) ----
791
+ const HISTORY_KEY = 'vs_history_v1';
792
+ const MAX_HISTORY = 8;
793
+ const MAX_HISTORY_AUDIO_BYTES = 1.5 * 1024 * 1024; // ~1.5MB cap per stored clip
794
+
795
+ const TIME_STR = {
796
+ my: {
797
+ justNow: () => 'ခုလေးတင်',
798
+ minAgo: (n) => `${n} မိနစ်အကြာ`,
799
+ hourAgo: (n) => `${n} နာရီအကြာ`,
800
+ dayAgo: (n) => `${n} ရက်အကြာ`
801
+ },
802
+ en: {
803
+ justNow: () => 'just now',
804
+ minAgo: (n) => `${n}m ago`,
805
+ hourAgo: (n) => `${n}h ago`,
806
+ dayAgo: (n) => `${n}d ago`
807
+ }
808
+ };
809
+
810
+ function relativeTime(ts) {
811
+ const diffSec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
812
+ const dict = TIME_STR[uiLang] || TIME_STR.my;
813
+ if (diffSec < 60) return dict.justNow();
814
+ const mins = Math.floor(diffSec / 60);
815
+ if (mins < 60) return dict.minAgo(mins);
816
+ const hours = Math.floor(mins / 60);
817
+ if (hours < 24) return dict.hourAgo(hours);
818
+ return dict.dayAgo(Math.floor(hours / 24));
819
+ }
820
+
821
+ function loadHistory() {
822
+ try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); }
823
+ catch (e) { return []; }
824
+ }
825
+
826
+ function saveHistory(list) {
827
+ try {
828
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(list));
829
+ } catch (e) {
830
+ // Quota exceeded (likely too many large audio clips) — drop the oldest
831
+ // half and retry once before giving up silently.
832
+ try {
833
+ const trimmed = list.slice(0, Math.ceil(list.length / 2));
834
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(trimmed));
835
+ } catch (e2) { /* give up quietly, history just won't persist */ }
836
+ }
837
+ }
838
+
839
+ function addHistoryEntry(entry) {
840
+ const list = loadHistory();
841
+ list.unshift(entry);
842
+ while (list.length > MAX_HISTORY) list.pop();
843
+ saveHistory(list);
844
+ renderHistory();
845
+ }
846
+
847
+ function deleteHistoryEntry(id) {
848
+ const list = loadHistory().filter(e => e.id !== id);
849
+ saveHistory(list);
850
+ renderHistory();
851
+ }
852
+
853
+ function clearHistory() {
854
+ saveHistory([]);
855
+ renderHistory();
856
+ }
857
+
858
+ const historyListEl = document.getElementById('historyList');
859
+ const historyEmptyEl = document.getElementById('historyEmpty');
860
+ const historyAudio = document.getElementById('historyAudio');
861
+ let activeHistoryPlayBtn = null;
862
+
863
+ function playIconSvg() {
864
+ return '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 5.5v13l11-6.5-11-6.5z"/></svg>';
865
+ }
866
+ function pauseIconSvg() {
867
+ return '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 5h3.5v14H7V5zm6.5 0H17v14h-3.5V5z"/></svg>';
868
+ }
869
+
870
+ function resetActivePlayButton() {
871
+ if (activeHistoryPlayBtn) {
872
+ activeHistoryPlayBtn.innerHTML = playIconSvg();
873
+ activeHistoryPlayBtn = null;
874
+ }
875
+ }
876
+
877
+ historyAudio.addEventListener('ended', resetActivePlayButton);
878
+ historyAudio.addEventListener('pause', () => {
879
+ if (activeHistoryPlayBtn) activeHistoryPlayBtn.innerHTML = playIconSvg();
880
+ });
881
+
882
+ function renderHistory() {
883
+ const list = loadHistory();
884
+ historyListEl.innerHTML = '';
885
+ activeHistoryPlayBtn = null;
886
+
887
+ if (!list.length) {
888
+ historyEmptyEl.style.display = 'block';
889
+ return;
890
+ }
891
+ historyEmptyEl.style.display = 'none';
892
+
893
+ list.forEach(entry => {
894
+ const row = document.createElement('div');
895
+ row.className = 'history-item';
896
+
897
+ const playBtn = document.createElement('button');
898
+ playBtn.className = 'history-play';
899
+ playBtn.innerHTML = playIconSvg();
900
+ if (!entry.audio) {
901
+ playBtn.disabled = true;
902
+ playBtn.title = t('historyAudioUnavailable');
903
+ } else {
904
+ playBtn.addEventListener('click', () => {
905
+ const isThisPlaying = activeHistoryPlayBtn === playBtn && !historyAudio.paused;
906
+ resetActivePlayButton();
907
+ if (isThisPlaying) return; // was playing -> just stop
908
+ historyAudio.src = entry.audio;
909
+ historyAudio.play().catch(() => {});
910
+ playBtn.innerHTML = pauseIconSvg();
911
+ activeHistoryPlayBtn = playBtn;
912
+ });
913
+ }
914
+
915
+ const meta = document.createElement('div');
916
+ meta.className = 'history-meta';
917
+ const textEl = document.createElement('div');
918
+ textEl.className = 'history-text';
919
+ textEl.textContent = entry.textPreview;
920
+ const subEl = document.createElement('div');
921
+ subEl.className = 'history-sub';
922
+ subEl.textContent = `${entry.langLabel || ''} · ${entry.voiceLabel || ''} · ${relativeTime(entry.ts)}`;
923
+ meta.appendChild(textEl);
924
+ meta.appendChild(subEl);
925
+
926
+ const delBtn = document.createElement('button');
927
+ delBtn.className = 'history-delete';
928
+ delBtn.textContent = '\u00d7';
929
+ delBtn.addEventListener('click', () => {
930
+ if (activeHistoryPlayBtn === playBtn) {
931
+ historyAudio.pause();
932
+ }
933
+ deleteHistoryEntry(entry.id);
934
+ });
935
+
936
+ row.appendChild(playBtn);
937
+ row.appendChild(meta);
938
+ row.appendChild(delBtn);
939
+ historyListEl.appendChild(row);
940
+ });
941
+ }
942
+
943
+ document.getElementById('clearHistoryBtn').addEventListener('click', () => {
944
+ historyAudio.pause();
945
+ clearHistory();
946
+ });
947
+
948
+ renderHistory();
949
+
950
+
951
  // ---- Build signature waveform ----
952
  const wave = document.getElementById('wave');
953
  const BAR_COUNT = 20;
 
1183
  status.textContent = t('statusDone');
1184
  player.play().catch(() => {});
1185
  notifyInterstitialCheckpoint();
1186
+
1187
+ // Save to Voice History (skip storing audio itself if the clip is too
1188
+ // large, to keep localStorage usage reasonable).
1189
+ (async () => {
1190
+ let audioDataUrl = null;
1191
+ if (blob.size <= MAX_HISTORY_AUDIO_BYTES) {
1192
+ try { audioDataUrl = await blobToBase64(blob); } catch (e) { audioDataUrl = null; }
1193
+ }
1194
+ addHistoryEntry({
1195
+ id: Date.now() + '_' + Math.random().toString(36).slice(2, 8),
1196
+ textPreview: value.length > 80 ? value.slice(0, 80) + '…' : value,
1197
+ voiceLabel: voiceSelect.options[voiceSelect.selectedIndex]
1198
+ ? voiceSelect.options[voiceSelect.selectedIndex].textContent.trim()
1199
+ : voice,
1200
+ langLabel: langSelect.options[langSelect.selectedIndex]
1201
+ ? langSelect.options[langSelect.selectedIndex].textContent.trim()
1202
+ : '',
1203
+ ts: Date.now(),
1204
+ audio: audioDataUrl
1205
+ });
1206
+ })();
1207
  } catch (e) {
1208
  status.textContent = e.message;
1209
  status.classList.add('error');