jorisvaneyghen commited on
Commit
30f8f43
·
1 Parent(s): a77e892

Recent Files

Browse files
Files changed (3) hide show
  1. README.md +2 -1
  2. css/styles.css +53 -0
  3. index.html +418 -60
README.md CHANGED
@@ -25,4 +25,5 @@ open in browser: http://localhost:5000/
25
  - Save loop ro file
26
  - Put
27
  - Custom Bar fix (half time, double time, time measure, force constant tempo/time measure)
28
- - Save bars in app storage browser
 
 
25
  - Save loop ro file
26
  - Put
27
  - Custom Bar fix (half time, double time, time measure, force constant tempo/time measure)
28
+ - Save bars in app storage browser
29
+ - Add custom labels to bars (intro, chorus1)
css/styles.css CHANGED
@@ -810,3 +810,56 @@ input[type="checkbox"] {
810
  #prevBar:hover, #nextBar:hover {
811
  background: #1976D2 !important;
812
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
810
  #prevBar:hover, #nextBar:hover {
811
  background: #1976D2 !important;
812
  }
813
+
814
+ /* Info Menu Styles */
815
+ .info-menu {
816
+ animation: menuFadeIn 0.2s ease-out;
817
+ }
818
+
819
+ @keyframes menuFadeIn {
820
+ from {
821
+ opacity: 0;
822
+ transform: translateY(-10px);
823
+ }
824
+ to {
825
+ opacity: 1;
826
+ transform: translateY(0);
827
+ }
828
+ }
829
+
830
+ /* Recent Files Menu Styles */
831
+ .recent-files-menu {
832
+ animation: popupFadeIn 0.3s ease-out;
833
+ }
834
+
835
+ .recent-file-item:hover {
836
+ background: rgba(255, 255, 255, 0.2) !important;
837
+ }
838
+
839
+ .remove-recent-file:hover {
840
+ background: rgba(244, 67, 54, 0.5) !important;
841
+ }
842
+
843
+ .close-recent-menu:hover {
844
+ background: rgba(255, 255, 255, 0.1) !important;
845
+ border-radius: 50%;
846
+ }
847
+
848
+ /* Ensure header has proper positioning for the menu */
849
+ .header-top {
850
+ position: relative;
851
+ }
852
+
853
+ /* Responsive design for menus */
854
+ @media (max-width: 600px) {
855
+ .info-menu {
856
+ right: 10px !important;
857
+ left: 10px !important;
858
+ min-width: auto !important;
859
+ }
860
+
861
+ .recent-files-menu {
862
+ width: 95% !important;
863
+ margin: 10px;
864
+ }
865
+ }
index.html CHANGED
@@ -187,7 +187,6 @@
187
  </div>
188
 
189
  <script type="module">
190
-
191
  // Check if the browser supports service workers
192
  if ('serviceWorker' in navigator) {
193
  // Wait for the window to load before registering
@@ -230,12 +229,18 @@
230
  this.CACHE_STORAGE_KEY = 'beatDetectionCache';
231
  this.cache = null;
232
 
 
 
 
 
 
233
  this.init();
234
  }
235
 
236
  async init() {
237
- // Load cache first
238
  await this.loadCache();
 
239
 
240
  // Show initialization progress
241
  this.showInitProgress();
@@ -255,7 +260,130 @@
255
  }
256
  }
257
 
258
- // Cache management methods
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  async loadCache() {
260
  try {
261
  const cached = localStorage.getItem(this.CACHE_STORAGE_KEY);
@@ -300,7 +428,6 @@
300
 
301
  // Clean up old cache entries (keep only last 100 files)
302
  this.cleanupCache();
303
-
304
  await this.saveCache();
305
  console.log('Results cached for file:', file.name);
306
  }
@@ -311,11 +438,9 @@
311
  // Sort by timestamp and remove oldest entries
312
  const sorted = entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
313
  const toRemove = sorted.slice(0, sorted.length - 20);
314
-
315
  toRemove.forEach(([key]) => {
316
  delete this.cache[key];
317
  });
318
-
319
  console.log('Cleaned up cache, removed', toRemove.length, 'old entries');
320
  }
321
  }
@@ -358,31 +483,12 @@
358
 
359
  enableUploadComponent() {
360
  const uploadArea = document.getElementById('uploadArea');
361
- const fileInput = document.getElementById('audioFile');
362
-
363
  uploadArea.classList.remove('disabled');
364
  uploadArea.querySelector('p').textContent = 'Click to upload or drag and drop an audio file';
365
- fileInput.disabled = false;
366
- }
367
-
368
- async checkServerStatus() {
369
- const serverStatus = document.getElementById('serverStatus');
370
- serverStatus.style.display = 'block';
371
-
372
- const isOnline = await this.detector.checkServerStatus();
373
-
374
- if (isOnline) {
375
- serverStatus.textContent = "✓ Server is online - using server postprocessing";
376
- serverStatus.className = 'server-status server-online';
377
- } else {
378
- serverStatus.textContent = "⚠ Server is offline - using client-side fallback";
379
- serverStatus.className = 'server-status server-offline';
380
- }
381
  }
382
 
383
  setupEventListeners() {
384
  const uploadArea = document.getElementById('uploadArea');
385
- const fileInput = document.getElementById('audioFile');
386
  const cancelButton = document.getElementById('cancelButton');
387
  const prevBarButton = document.getElementById('prevBar');
388
  const nextBarButton = document.getElementById('nextBar');
@@ -390,14 +496,33 @@
390
  const stepSizeInput = document.getElementById('stepSize');
391
  const startBarInput = document.getElementById('startBar');
392
 
393
- uploadArea.addEventListener('click', () => fileInput.click());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  uploadArea.addEventListener('dragover', (e) => {
395
  e.preventDefault();
396
  uploadArea.classList.add('dragover');
397
  });
 
398
  uploadArea.addEventListener('dragleave', () => {
399
  uploadArea.classList.remove('dragover');
400
  });
 
401
  uploadArea.addEventListener('drop', (e) => {
402
  e.preventDefault();
403
  uploadArea.classList.remove('dragover');
@@ -407,12 +532,6 @@
407
  }
408
  });
409
 
410
- fileInput.addEventListener('change', (e) => {
411
- if (e.target.files.length > 0) {
412
- this.handleAudioFile(e.target.files[0]);
413
- }
414
- });
415
-
416
  cancelButton.addEventListener('click', () => this.cancelProcessing());
417
 
418
  // Bars to play buttons
@@ -432,7 +551,6 @@
432
  const barsToPlayInput = document.getElementById('barsToPlay');
433
  barsToPlayInput.value = value;
434
  barsToPlayInput.classList.remove('active');
435
-
436
  this.updateAudioPlayer();
437
  });
438
  }
@@ -455,7 +573,6 @@
455
  const stepSizeInput = document.getElementById('stepSize');
456
  stepSizeInput.value = value;
457
  stepSizeInput.classList.remove('active');
458
-
459
  });
460
  }
461
  });
@@ -464,7 +581,6 @@
464
  barsToPlayInput.addEventListener('change', (e) => {
465
  const value = parseInt(e.target.value);
466
  this.barsToPlay = value;
467
-
468
  const buttonGroup = barsToPlayInput.closest('.button-input-group').querySelector('.button-group');
469
  const buttons = buttonGroup.querySelectorAll('.choice-button');
470
 
@@ -498,8 +614,6 @@
498
  buttons.forEach(btn => {
499
  btn.classList.toggle('active', parseInt(btn.dataset.value) === value);
500
  });
501
- console.log(`isCustom ${isCustom}`);
502
-
503
  stepSizeInput.classList.toggle('active', isCustom);
504
  });
505
 
@@ -530,15 +644,18 @@
530
  }
531
  });
532
 
533
- // Info Popup functionality
534
  const infoButton = document.getElementById('infoButton');
535
  const infoPopup = document.getElementById('infoPopup');
536
  const closePopup = document.getElementById('closePopup');
537
 
 
 
 
538
  // Open popup
539
- infoButton.addEventListener('click', function () {
540
- infoPopup.classList.add('active');
541
- document.body.style.overflow = 'hidden'; // Prevent background scrolling
542
  });
543
 
544
  // Close popup
@@ -555,36 +672,272 @@
555
  }
556
  });
557
 
558
- // Close popup with Escape key
559
- document.addEventListener('keydown', function (e) {
560
- if (e.key === 'Escape' && infoPopup.classList.contains('active')) {
561
  infoPopup.classList.remove('active');
562
  document.body.style.overflow = '';
 
563
  }
564
  });
565
 
 
 
 
 
566
  }
567
 
568
- cancelProcessing() {
569
- if (this.isProcessing) {
570
- this.detector.cancel();
571
- this.isProcessing = false;
572
- console.log('Processing cancelled by user');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
 
574
- const progressMessage = document.getElementById('progressMessage');
575
- progressMessage.textContent = "Processing cancelled";
576
 
577
- const progressFill = document.getElementById('progressFill');
578
- progressFill.style.background = '#f44336';
 
 
 
 
 
579
 
580
- // Hide loading after a delay
581
- setTimeout(() => {
582
- document.getElementById('loading').style.display = 'none';
583
- }, 1500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  }
585
  }
586
 
587
- async handleAudioFile(file) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  if (this.isProcessing) {
589
  alert('Already processing a file. Please wait.');
590
  return;
@@ -623,7 +976,6 @@
623
  const updateProgress = async (percent, message) => {
624
  // Clamp percentage to ensure it stays within valid range
625
  const clampedPercent = Math.max(0, Math.min(100, percent));
626
-
627
  const currentTime = Date.now();
628
  const elapsed = (currentTime - this.startTime) / 1000;
629
 
@@ -675,6 +1027,12 @@
675
  detected_beats_per_bar: this.detectedBeatsPerBar
676
  });
677
 
 
 
 
 
 
 
678
  // Update UI with results
679
  this.updateResultsUI();
680
 
@@ -713,7 +1071,7 @@
713
  const results = document.getElementById('results');
714
 
715
  // Show file info
716
- fileInfo.textContent = `File: ${file.name} (${this.formatFileSize(file.size)}) [CACHED]`;
717
 
718
  // Load the audio file for playback (we still need the audio buffer)
719
  try {
 
187
  </div>
188
 
189
  <script type="module">
 
190
  // Check if the browser supports service workers
191
  if ('serviceWorker' in navigator) {
192
  // Wait for the window to load before registering
 
229
  this.CACHE_STORAGE_KEY = 'beatDetectionCache';
230
  this.cache = null;
231
 
232
+ // File System Access API and IndexedDB
233
+ this.RECENT_FILES_KEY = 'recentAudioFiles';
234
+ this.MAX_RECENT_FILES = 10;
235
+ this.recentFiles = [];
236
+
237
  this.init();
238
  }
239
 
240
  async init() {
241
+ // Load cache and recent files first
242
  await this.loadCache();
243
+ await this.loadRecentFiles();
244
 
245
  // Show initialization progress
246
  this.showInitProgress();
 
260
  }
261
  }
262
 
263
+ // IndexedDB for file handles
264
+ async openDB() {
265
+ return new Promise((resolve, reject) => {
266
+ const request = indexedDB.open('LoopMaestroDB', 1);
267
+
268
+ request.onerror = () => reject(request.error);
269
+ request.onsuccess = () => resolve(request.result);
270
+
271
+ request.onupgradeneeded = (event) => {
272
+ const db = event.target.result;
273
+ if (!db.objectStoreNames.contains('fileHandles')) {
274
+ db.createObjectStore('fileHandles', { keyPath: 'id' });
275
+ }
276
+ };
277
+ });
278
+ }
279
+
280
+ async saveFileHandle(fileHandle, fileName, fileSize) {
281
+ try {
282
+ const db = await this.openDB();
283
+ const transaction = db.transaction(['fileHandles'], 'readwrite');
284
+ const store = transaction.objectStore('fileHandles');
285
+
286
+ const fileRecord = {
287
+ id: `${fileName}_${fileSize}_${Date.now()}`,
288
+ handle: fileHandle,
289
+ fileName: fileName,
290
+ fileSize: fileSize,
291
+ lastAccessed: Date.now()
292
+ };
293
+
294
+ await store.put(fileRecord);
295
+
296
+ // Also add to recent files list
297
+ await this.addToRecentFiles(fileRecord);
298
+
299
+ return fileRecord.id;
300
+ } catch (error) {
301
+ console.error('Error saving file handle:', error);
302
+ throw error;
303
+ }
304
+ }
305
+
306
+ async getFileHandle(id) {
307
+ try {
308
+ const db = await this.openDB();
309
+ const transaction = db.transaction(['fileHandles'], 'readonly');
310
+ const store = transaction.objectStore('fileHandles');
311
+
312
+ return new Promise((resolve, reject) => {
313
+ const request = store.get(id);
314
+ request.onerror = () => reject(request.error);
315
+ request.onsuccess = () => resolve(request.result);
316
+ });
317
+ } catch (error) {
318
+ console.error('Error getting file handle:', error);
319
+ throw error;
320
+ }
321
+ }
322
+
323
+ async getAllFileHandles() {
324
+ try {
325
+ const db = await this.openDB();
326
+ const transaction = db.transaction(['fileHandles'], 'readonly');
327
+ const store = transaction.objectStore('fileHandles');
328
+
329
+ return new Promise((resolve, reject) => {
330
+ const request = store.getAll();
331
+ request.onerror = () => reject(request.error);
332
+ request.onsuccess = () => resolve(request.result);
333
+ });
334
+ } catch (error) {
335
+ console.error('Error getting all file handles:', error);
336
+ throw error;
337
+ }
338
+ }
339
+
340
+ // Recent files management
341
+ async loadRecentFiles() {
342
+ try {
343
+ const recent = localStorage.getItem(this.RECENT_FILES_KEY);
344
+ this.recentFiles = recent ? JSON.parse(recent) : [];
345
+ } catch (error) {
346
+ console.error('Error loading recent files:', error);
347
+ this.recentFiles = [];
348
+ }
349
+ }
350
+
351
+ async saveRecentFiles() {
352
+ try {
353
+ localStorage.setItem(this.RECENT_FILES_KEY, JSON.stringify(this.recentFiles));
354
+ } catch (error) {
355
+ console.error('Error saving recent files:', error);
356
+ }
357
+ }
358
+
359
+ async addToRecentFiles(fileRecord) {
360
+ // Remove if already exists
361
+ this.recentFiles = this.recentFiles.filter(file =>
362
+ file.id !== fileRecord.id
363
+ );
364
+
365
+ // Add to beginning
366
+ this.recentFiles.unshift({
367
+ id: fileRecord.id,
368
+ fileName: fileRecord.fileName,
369
+ fileSize: fileRecord.fileSize,
370
+ lastAccessed: fileRecord.lastAccessed
371
+ });
372
+
373
+ // Keep only recent files
374
+ if (this.recentFiles.length > this.MAX_RECENT_FILES) {
375
+ this.recentFiles = this.recentFiles.slice(0, this.MAX_RECENT_FILES);
376
+ }
377
+
378
+ await this.saveRecentFiles();
379
+ }
380
+
381
+ async removeFromRecentFiles(fileId) {
382
+ this.recentFiles = this.recentFiles.filter(file => file.id !== fileId);
383
+ await this.saveRecentFiles();
384
+ }
385
+
386
+ // Cache management methods (existing, keep as is)
387
  async loadCache() {
388
  try {
389
  const cached = localStorage.getItem(this.CACHE_STORAGE_KEY);
 
428
 
429
  // Clean up old cache entries (keep only last 100 files)
430
  this.cleanupCache();
 
431
  await this.saveCache();
432
  console.log('Results cached for file:', file.name);
433
  }
 
438
  // Sort by timestamp and remove oldest entries
439
  const sorted = entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
440
  const toRemove = sorted.slice(0, sorted.length - 20);
 
441
  toRemove.forEach(([key]) => {
442
  delete this.cache[key];
443
  });
 
444
  console.log('Cleaned up cache, removed', toRemove.length, 'old entries');
445
  }
446
  }
 
483
 
484
  enableUploadComponent() {
485
  const uploadArea = document.getElementById('uploadArea');
 
 
486
  uploadArea.classList.remove('disabled');
487
  uploadArea.querySelector('p').textContent = 'Click to upload or drag and drop an audio file';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  }
489
 
490
  setupEventListeners() {
491
  const uploadArea = document.getElementById('uploadArea');
 
492
  const cancelButton = document.getElementById('cancelButton');
493
  const prevBarButton = document.getElementById('prevBar');
494
  const nextBarButton = document.getElementById('nextBar');
 
496
  const stepSizeInput = document.getElementById('stepSize');
497
  const startBarInput = document.getElementById('startBar');
498
 
499
+ // File System Access API for file selection
500
+ uploadArea.addEventListener('click', async () => {
501
+ if ('showOpenFilePicker' in window) {
502
+ await this.openFileWithFileSystemAPI();
503
+ } else {
504
+ // Fallback to traditional file input
505
+ const fileInput = document.createElement('input');
506
+ fileInput.type = 'file';
507
+ fileInput.accept = 'audio/*';
508
+ fileInput.onchange = (e) => {
509
+ if (e.target.files.length > 0) {
510
+ this.handleAudioFile(e.target.files[0]);
511
+ }
512
+ };
513
+ fileInput.click();
514
+ }
515
+ });
516
+
517
  uploadArea.addEventListener('dragover', (e) => {
518
  e.preventDefault();
519
  uploadArea.classList.add('dragover');
520
  });
521
+
522
  uploadArea.addEventListener('dragleave', () => {
523
  uploadArea.classList.remove('dragover');
524
  });
525
+
526
  uploadArea.addEventListener('drop', (e) => {
527
  e.preventDefault();
528
  uploadArea.classList.remove('dragover');
 
532
  }
533
  });
534
 
 
 
 
 
 
 
535
  cancelButton.addEventListener('click', () => this.cancelProcessing());
536
 
537
  // Bars to play buttons
 
551
  const barsToPlayInput = document.getElementById('barsToPlay');
552
  barsToPlayInput.value = value;
553
  barsToPlayInput.classList.remove('active');
 
554
  this.updateAudioPlayer();
555
  });
556
  }
 
573
  const stepSizeInput = document.getElementById('stepSize');
574
  stepSizeInput.value = value;
575
  stepSizeInput.classList.remove('active');
 
576
  });
577
  }
578
  });
 
581
  barsToPlayInput.addEventListener('change', (e) => {
582
  const value = parseInt(e.target.value);
583
  this.barsToPlay = value;
 
584
  const buttonGroup = barsToPlayInput.closest('.button-input-group').querySelector('.button-group');
585
  const buttons = buttonGroup.querySelectorAll('.choice-button');
586
 
 
614
  buttons.forEach(btn => {
615
  btn.classList.toggle('active', parseInt(btn.dataset.value) === value);
616
  });
 
 
617
  stepSizeInput.classList.toggle('active', isCustom);
618
  });
619
 
 
644
  }
645
  });
646
 
647
+ // Enhanced Info Popup functionality with menu
648
  const infoButton = document.getElementById('infoButton');
649
  const infoPopup = document.getElementById('infoPopup');
650
  const closePopup = document.getElementById('closePopup');
651
 
652
+ // Create menu for info button
653
+ this.createInfoMenu();
654
+
655
  // Open popup
656
+ infoButton.addEventListener('click', (e) => {
657
+ e.stopPropagation();
658
+ this.toggleInfoMenu();
659
  });
660
 
661
  // Close popup
 
672
  }
673
  });
674
 
675
+ // Close popup and menu with Escape key
676
+ document.addEventListener('keydown', (e) => {
677
+ if (e.key === 'Escape') {
678
  infoPopup.classList.remove('active');
679
  document.body.style.overflow = '';
680
+ this.hideInfoMenu();
681
  }
682
  });
683
 
684
+ // Close menu when clicking outside
685
+ document.addEventListener('click', () => {
686
+ this.hideInfoMenu();
687
+ });
688
  }
689
 
690
+ createInfoMenu() {
691
+ this.infoMenu = document.createElement('div');
692
+ this.infoMenu.className = 'info-menu';
693
+ this.infoMenu.innerHTML = `
694
+ <div class="menu-item" data-action="about">About Loop Maestro</div>
695
+ <div class="menu-item" data-action="recent">Recent Songs</div>
696
+ `;
697
+ this.infoMenu.style.cssText = `
698
+ position: absolute;
699
+ top: 60px;
700
+ right: 0;
701
+ background: rgba(255, 255, 255, 0.95);
702
+ backdrop-filter: blur(10px);
703
+ border-radius: 8px;
704
+ padding: 8px 0;
705
+ min-width: 180px;
706
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
707
+ border: 1px solid rgba(255, 255, 255, 0.2);
708
+ display: none;
709
+ z-index: 1000;
710
+ color: #333;
711
+ `;
712
+
713
+ // Style menu items
714
+ const menuItems = this.infoMenu.querySelectorAll('.menu-item');
715
+ menuItems.forEach(item => {
716
+ item.style.cssText = `
717
+ padding: 12px 16px;
718
+ cursor: pointer;
719
+ transition: background-color 0.2s;
720
+ font-size: 14px;
721
+ border: none;
722
+ background: none;
723
+ width: 100%;
724
+ text-align: left;
725
+ `;
726
+ item.addEventListener('mouseenter', () => {
727
+ item.style.backgroundColor = 'rgba(33, 150, 243, 0.1)';
728
+ });
729
+ item.addEventListener('mouseleave', () => {
730
+ item.style.backgroundColor = 'transparent';
731
+ });
732
+ item.addEventListener('click', (e) => {
733
+ e.stopPropagation();
734
+ this.handleMenuAction(item.dataset.action);
735
+ });
736
+ });
737
 
738
+ document.body.appendChild(this.infoMenu);
739
+ }
740
 
741
+ toggleInfoMenu() {
742
+ if (this.infoMenu.style.display === 'block') {
743
+ this.hideInfoMenu();
744
+ } else {
745
+ this.showInfoMenu();
746
+ }
747
+ }
748
 
749
+ showInfoMenu() {
750
+ this.infoMenu.style.display = 'block';
751
+ }
752
+
753
+ hideInfoMenu() {
754
+ this.infoMenu.style.display = 'none';
755
+ }
756
+
757
+ async handleMenuAction(action) {
758
+ this.hideInfoMenu();
759
+
760
+ switch (action) {
761
+ case 'about':
762
+ document.getElementById('infoPopup').classList.add('active');
763
+ document.body.style.overflow = 'hidden';
764
+ break;
765
+ case 'recent':
766
+ await this.showRecentFilesMenu();
767
+ break;
768
  }
769
  }
770
 
771
+ async showRecentFilesMenu() {
772
+ await this.loadRecentFiles();
773
+
774
+ const recentFilesMenu = document.createElement('div');
775
+ recentFilesMenu.className = 'recent-files-menu';
776
+ recentFilesMenu.style.cssText = `
777
+ position: fixed;
778
+ top: 50%;
779
+ left: 50%;
780
+ transform: translate(-50%, -50%);
781
+ background: rgba(26, 42, 108, 0.95);
782
+ backdrop-filter: blur(10px);
783
+ border-radius: 15px;
784
+ padding: 20px;
785
+ max-width: 500px;
786
+ width: 90%;
787
+ max-height: 70vh;
788
+ overflow-y: auto;
789
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
790
+ border: 1px solid rgba(255, 255, 255, 0.2);
791
+ z-index: 1001;
792
+ color: white;
793
+ `;
794
+
795
+ let menuContent = `
796
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.2);">
797
+ <h3 style="margin: 0;">Recent Songs</h3>
798
+ <button class="close-recent-menu" style="background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;">×</button>
799
+ </div>
800
+ `;
801
+
802
+ if (this.recentFiles.length === 0) {
803
+ menuContent += `<p style="text-align: center; opacity: 0.8;">No recent files</p>`;
804
+ } else {
805
+ menuContent += `<div class="recent-files-list" style="display: flex; flex-direction: column; gap: 8px;">`;
806
+
807
+ for (const file of this.recentFiles) {
808
+ menuContent += `
809
+ <div class="recent-file-item" data-file-id="${file.id}" style="
810
+ display: flex;
811
+ justify-content: space-between;
812
+ align-items: center;
813
+ padding: 12px;
814
+ background: rgba(255, 255, 255, 0.1);
815
+ border-radius: 8px;
816
+ cursor: pointer;
817
+ transition: background-color 0.2s;
818
+ ">
819
+ <div>
820
+ <div style="font-weight: bold;">${file.fileName}</div>
821
+ <div style="font-size: 0.8rem; opacity: 0.8;">${this.formatFileSize(file.fileSize)}</div>
822
+ </div>
823
+ <button class="remove-recent-file" style="
824
+ background: rgba(244, 67, 54, 0.3);
825
+ border: none;
826
+ color: white;
827
+ border-radius: 4px;
828
+ padding: 4px 8px;
829
+ cursor: pointer;
830
+ font-size: 0.8rem;
831
+ ">Remove</button>
832
+ </div>
833
+ `;
834
+ }
835
+
836
+ menuContent += `</div>`;
837
+ }
838
+
839
+ recentFilesMenu.innerHTML = menuContent;
840
+ document.body.appendChild(recentFilesMenu);
841
+
842
+ // Add event listeners
843
+ recentFilesMenu.querySelector('.close-recent-menu').addEventListener('click', () => {
844
+ document.body.removeChild(recentFilesMenu);
845
+ });
846
+
847
+ recentFilesMenu.addEventListener('click', (e) => {
848
+ if (e.target === recentFilesMenu) {
849
+ document.body.removeChild(recentFilesMenu);
850
+ }
851
+ });
852
+
853
+ // Load file when clicked
854
+ recentFilesMenu.querySelectorAll('.recent-file-item').forEach(item => {
855
+ item.addEventListener('click', async (e) => {
856
+ if (!e.target.classList.contains('remove-recent-file')) {
857
+ const fileId = item.dataset.fileId;
858
+ await this.loadFileFromHandle(fileId);
859
+ document.body.removeChild(recentFilesMenu);
860
+ }
861
+ });
862
+ });
863
+
864
+ // Remove file when remove button clicked
865
+ recentFilesMenu.querySelectorAll('.remove-recent-file').forEach(button => {
866
+ button.addEventListener('click', async (e) => {
867
+ e.stopPropagation();
868
+ const fileId = button.closest('.recent-file-item').dataset.fileId;
869
+ await this.removeFromRecentFiles(fileId);
870
+ document.body.removeChild(recentFilesMenu);
871
+ await this.showRecentFilesMenu(); // Refresh the menu
872
+ });
873
+ });
874
+
875
+ // Close with Escape key
876
+ const closeHandler = (e) => {
877
+ if (e.key === 'Escape') {
878
+ document.body.removeChild(recentFilesMenu);
879
+ document.removeEventListener('keydown', closeHandler);
880
+ }
881
+ };
882
+ document.addEventListener('keydown', closeHandler);
883
+ }
884
+
885
+ async openFileWithFileSystemAPI() {
886
+ try {
887
+ const [fileHandle] = await window.showOpenFilePicker({
888
+ types: [{
889
+ description: 'Audio Files',
890
+ accept: {
891
+ 'audio/*': ['.mp3', '.wav', '.aac', '.ogg', '.flac', '.m4a']
892
+ }
893
+ }],
894
+ multiple: false
895
+ });
896
+
897
+ const file = await fileHandle.getFile();
898
+ const fileId = await this.saveFileHandle(fileHandle, file.name, file.size);
899
+ await this.handleAudioFile(file, fileId);
900
+
901
+ } catch (error) {
902
+ if (error.name !== 'AbortError') {
903
+ console.error('Error opening file:', error);
904
+ alert('Error opening file. Please try again.');
905
+ }
906
+ }
907
+ }
908
+
909
+ async loadFileFromHandle(fileId) {
910
+ try {
911
+ const fileRecord = await this.getFileHandle(fileId);
912
+ if (!fileRecord) {
913
+ throw new Error('File not found in database');
914
+ }
915
+
916
+ // Verify we still have permission to read the file
917
+ if (await fileRecord.handle.queryPermission({ mode: 'read' }) !== 'granted') {
918
+ const permission = await fileRecord.handle.requestPermission({ mode: 'read' });
919
+ if (permission !== 'granted') {
920
+ throw new Error('Permission denied to read the file');
921
+ }
922
+ }
923
+
924
+ const file = await fileRecord.handle.getFile();
925
+
926
+ // Update last accessed time
927
+ await this.addToRecentFiles(fileRecord);
928
+
929
+ await this.handleAudioFile(file, fileId);
930
+
931
+ } catch (error) {
932
+ console.error('Error loading file from handle:', error);
933
+ alert('Error loading file. It may have been moved or deleted.');
934
+
935
+ // Remove from recent files if there's an error
936
+ await this.removeFromRecentFiles(fileId);
937
+ }
938
+ }
939
+
940
+ async handleAudioFile(file, fileId = null) {
941
  if (this.isProcessing) {
942
  alert('Already processing a file. Please wait.');
943
  return;
 
976
  const updateProgress = async (percent, message) => {
977
  // Clamp percentage to ensure it stays within valid range
978
  const clampedPercent = Math.max(0, Math.min(100, percent));
 
979
  const currentTime = Date.now();
980
  const elapsed = (currentTime - this.startTime) / 1000;
981
 
 
1027
  detected_beats_per_bar: this.detectedBeatsPerBar
1028
  });
1029
 
1030
+ // Save file handle if using File System API
1031
+ if (!fileId && 'showOpenFilePicker' in window) {
1032
+ // This was a drag/drop or fallback upload, so we can't save a handle
1033
+ console.log('File uploaded via drag/drop or fallback - no file handle to save');
1034
+ }
1035
+
1036
  // Update UI with results
1037
  this.updateResultsUI();
1038
 
 
1071
  const results = document.getElementById('results');
1072
 
1073
  // Show file info
1074
+ fileInfo.textContent = `File: ${file.name} (${this.formatFileSize(file.size)})`;
1075
 
1076
  // Load the audio file for playback (we still need the audio buffer)
1077
  try {