Spaces:
Sleeping
Sleeping
Commit ·
30f8f43
1
Parent(s): a77e892
Recent Files
Browse files- README.md +2 -1
- css/styles.css +53 -0
- 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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',
|
| 540 |
-
|
| 541 |
-
|
| 542 |
});
|
| 543 |
|
| 544 |
// Close popup
|
|
@@ -555,36 +672,272 @@
|
|
| 555 |
}
|
| 556 |
});
|
| 557 |
|
| 558 |
-
// Close popup with Escape key
|
| 559 |
-
document.addEventListener('keydown',
|
| 560 |
-
if (e.key === 'Escape'
|
| 561 |
infoPopup.classList.remove('active');
|
| 562 |
document.body.style.overflow = '';
|
|
|
|
| 563 |
}
|
| 564 |
});
|
| 565 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
}
|
| 567 |
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
|
| 574 |
-
|
| 575 |
-
|
| 576 |
|
| 577 |
-
|
| 578 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
}
|
| 585 |
}
|
| 586 |
|
| 587 |
-
async
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)})
|
| 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 {
|