Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -497,6 +497,106 @@ const content = document.getElementById('content');
|
|
497 |
let active = "";
|
498 |
let currentPage = 1;
|
499 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
500 |
// Simple utility functions
|
501 |
function loadHTML(url, callback) {
|
502 |
const xhr = new XMLHttpRequest();
|
@@ -635,6 +735,14 @@ function loadManage() {
|
|
635 |
|
636 |
<h2>Manage Saved URLs</h2>
|
637 |
<div id="url-list" class="url-list">Loading...</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
638 |
</div>
|
639 |
`;
|
640 |
|
@@ -643,14 +751,53 @@ function loadManage() {
|
|
643 |
|
644 |
// URL management functions
|
645 |
function loadUrlList() {
|
|
|
|
|
|
|
646 |
makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
|
647 |
const urlList = document.getElementById('url-list');
|
648 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
649 |
if(data.items.length === 0) {
|
650 |
urlList.innerHTML = '<p style="text-align:center;padding:20px">No URLs saved yet.</p>';
|
651 |
return;
|
652 |
}
|
653 |
|
|
|
|
|
|
|
654 |
let html = '';
|
655 |
data.items.forEach(item => {
|
656 |
// Escape the URL to prevent JavaScript injection when used in onclick handlers
|
@@ -670,6 +817,7 @@ function loadUrlList() {
|
|
670 |
urlList.innerHTML = html;
|
671 |
});
|
672 |
}
|
|
|
673 |
|
674 |
function addUrl() {
|
675 |
const url = document.getElementById('new-url').value.trim();
|
@@ -686,6 +834,14 @@ function addUrl() {
|
|
686 |
showStatus('add-status', data.message, data.success);
|
687 |
if(data.success) {
|
688 |
document.getElementById('new-url').value = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
689 |
loadUrlList();
|
690 |
// If currently in Favorites tab, reload to see changes immediately
|
691 |
if(active === 'Favorites') {
|
@@ -708,6 +864,14 @@ function editUrl(url) {
|
|
708 |
|
709 |
makeRequest('/api/url/update', 'POST', formData, function(data) {
|
710 |
if(data.success) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
711 |
loadUrlList();
|
712 |
// If currently in Favorites tab, reload to see changes immediately
|
713 |
if(active === 'Favorites') {
|
@@ -729,6 +893,14 @@ function deleteUrl(url) {
|
|
729 |
|
730 |
makeRequest('/api/url/delete', 'POST', formData, function(data) {
|
731 |
if(data.success) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
732 |
loadUrlList();
|
733 |
// If currently in Favorites tab, reload to see changes immediately
|
734 |
if(active === 'Favorites') {
|
@@ -778,6 +950,52 @@ tabs.appendChild(manageTab);
|
|
778 |
|
779 |
// Start with Favorites tab
|
780 |
loadFavorites(1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
781 |
</script>
|
782 |
</body>
|
783 |
</html>''')
|
|
|
497 |
let active = "";
|
498 |
let currentPage = 1;
|
499 |
|
500 |
+
// LocalStorage functionality for URL persistence
|
501 |
+
function saveToLocalStorage(urls) {
|
502 |
+
try {
|
503 |
+
localStorage.setItem('favoriteUrls', JSON.stringify(urls));
|
504 |
+
console.log('Saved URLs to localStorage:', urls.length);
|
505 |
+
return true;
|
506 |
+
} catch (e) {
|
507 |
+
console.error('Error saving to localStorage:', e);
|
508 |
+
return false;
|
509 |
+
}
|
510 |
+
}
|
511 |
+
|
512 |
+
function loadFromLocalStorage() {
|
513 |
+
try {
|
514 |
+
const data = localStorage.getItem('favoriteUrls');
|
515 |
+
if (data) {
|
516 |
+
const urls = JSON.parse(data);
|
517 |
+
console.log('Loaded URLs from localStorage:', urls.length);
|
518 |
+
return urls;
|
519 |
+
}
|
520 |
+
} catch (e) {
|
521 |
+
console.error('Error loading from localStorage:', e);
|
522 |
+
}
|
523 |
+
return null;
|
524 |
+
}
|
525 |
+
|
526 |
+
// Export/Import functionality
|
527 |
+
function exportUrls() {
|
528 |
+
makeRequest('/api/favorites?per_page=1000', 'GET', null, function(data) {
|
529 |
+
if (data.items && data.items.length > 0) {
|
530 |
+
const urls = data.items.map(item => item.url);
|
531 |
+
|
532 |
+
// Save to localStorage as backup
|
533 |
+
saveToLocalStorage(urls);
|
534 |
+
|
535 |
+
// Create file for download
|
536 |
+
const blob = new Blob([JSON.stringify(urls, null, 2)], { type: 'application/json' });
|
537 |
+
const a = document.createElement('a');
|
538 |
+
a.href = URL.createObjectURL(blob);
|
539 |
+
a.download = 'favorite_urls.json';
|
540 |
+
document.body.appendChild(a);
|
541 |
+
a.click();
|
542 |
+
document.body.removeChild(a);
|
543 |
+
} else {
|
544 |
+
alert('No URLs to export');
|
545 |
+
}
|
546 |
+
});
|
547 |
+
}
|
548 |
+
|
549 |
+
function importUrls() {
|
550 |
+
document.getElementById('import-file').click();
|
551 |
+
}
|
552 |
+
|
553 |
+
function handleImportFile(files) {
|
554 |
+
if (files.length === 0) return;
|
555 |
+
|
556 |
+
const file = files[0];
|
557 |
+
const reader = new FileReader();
|
558 |
+
|
559 |
+
reader.onload = function(e) {
|
560 |
+
try {
|
561 |
+
const urls = JSON.parse(e.target.result);
|
562 |
+
if (Array.isArray(urls)) {
|
563 |
+
// Save to localStorage
|
564 |
+
saveToLocalStorage(urls);
|
565 |
+
|
566 |
+
// Add each URL to the server
|
567 |
+
let processed = 0;
|
568 |
+
|
569 |
+
function addNextUrl(index) {
|
570 |
+
if (index >= urls.length) {
|
571 |
+
alert(`Import complete. Added ${processed} URLs.`);
|
572 |
+
loadUrlList();
|
573 |
+
if (active === 'Favorites') {
|
574 |
+
loadFavorites(currentPage);
|
575 |
+
}
|
576 |
+
return;
|
577 |
+
}
|
578 |
+
|
579 |
+
const formData = new FormData();
|
580 |
+
formData.append('url', urls[index]);
|
581 |
+
|
582 |
+
makeRequest('/api/url/add', 'POST', formData, function(data) {
|
583 |
+
if (data.success) processed++;
|
584 |
+
addNextUrl(index + 1);
|
585 |
+
});
|
586 |
+
}
|
587 |
+
|
588 |
+
addNextUrl(0);
|
589 |
+
} else {
|
590 |
+
alert('Invalid format. File must contain a JSON array of URLs.');
|
591 |
+
}
|
592 |
+
} catch (e) {
|
593 |
+
alert('Error parsing file: ' + e.message);
|
594 |
+
}
|
595 |
+
};
|
596 |
+
|
597 |
+
reader.readAsText(file);
|
598 |
+
}
|
599 |
+
|
600 |
// Simple utility functions
|
601 |
function loadHTML(url, callback) {
|
602 |
const xhr = new XMLHttpRequest();
|
|
|
735 |
|
736 |
<h2>Manage Saved URLs</h2>
|
737 |
<div id="url-list" class="url-list">Loading...</div>
|
738 |
+
|
739 |
+
<div style="margin-top: 30px;">
|
740 |
+
<h3>Backup & Restore</h3>
|
741 |
+
<p>Server storage may not persist across restarts. Use these options to save your data:</p>
|
742 |
+
<button onclick="exportUrls()" class="btn btn-success">Export URLs</button>
|
743 |
+
<button onclick="importUrls()" class="btn btn-primary">Import URLs</button>
|
744 |
+
<input type="file" id="import-file" style="display:none" onchange="handleImportFile(this.files)">
|
745 |
+
</div>
|
746 |
</div>
|
747 |
`;
|
748 |
|
|
|
751 |
|
752 |
// URL management functions
|
753 |
function loadUrlList() {
|
754 |
+
// First try to load from localStorage as a fallback
|
755 |
+
const localUrls = loadFromLocalStorage();
|
756 |
+
|
757 |
makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
|
758 |
const urlList = document.getElementById('url-list');
|
759 |
|
760 |
+
// If server has no URLs but localStorage does, restore from localStorage
|
761 |
+
if (data.items.length === 0 && localUrls && localUrls.length > 0) {
|
762 |
+
showStatus('add-status', 'Restoring URLs from local backup...', true);
|
763 |
+
|
764 |
+
// Add each URL from localStorage to the server
|
765 |
+
let restored = 0;
|
766 |
+
|
767 |
+
function restoreNextUrl(index) {
|
768 |
+
if (index >= localUrls.length) {
|
769 |
+
showStatus('add-status', `Restored ${restored} URLs from local backup`, true);
|
770 |
+
// Reload the list after restoration
|
771 |
+
setTimeout(() => {
|
772 |
+
loadUrlList();
|
773 |
+
if (active === 'Favorites') {
|
774 |
+
loadFavorites(currentPage);
|
775 |
+
}
|
776 |
+
}, 1000);
|
777 |
+
return;
|
778 |
+
}
|
779 |
+
|
780 |
+
const formData = new FormData();
|
781 |
+
formData.append('url', localUrls[index]);
|
782 |
+
|
783 |
+
makeRequest('/api/url/add', 'POST', formData, function(data) {
|
784 |
+
if (data.success) restored++;
|
785 |
+
restoreNextUrl(index + 1);
|
786 |
+
});
|
787 |
+
}
|
788 |
+
|
789 |
+
restoreNextUrl(0);
|
790 |
+
return;
|
791 |
+
}
|
792 |
+
|
793 |
if(data.items.length === 0) {
|
794 |
urlList.innerHTML = '<p style="text-align:center;padding:20px">No URLs saved yet.</p>';
|
795 |
return;
|
796 |
}
|
797 |
|
798 |
+
// Update localStorage with server data
|
799 |
+
saveToLocalStorage(data.items.map(item => item.url));
|
800 |
+
|
801 |
let html = '';
|
802 |
data.items.forEach(item => {
|
803 |
// Escape the URL to prevent JavaScript injection when used in onclick handlers
|
|
|
817 |
urlList.innerHTML = html;
|
818 |
});
|
819 |
}
|
820 |
+
}
|
821 |
|
822 |
function addUrl() {
|
823 |
const url = document.getElementById('new-url').value.trim();
|
|
|
834 |
showStatus('add-status', data.message, data.success);
|
835 |
if(data.success) {
|
836 |
document.getElementById('new-url').value = '';
|
837 |
+
|
838 |
+
// Update localStorage
|
839 |
+
const localUrls = loadFromLocalStorage() || [];
|
840 |
+
if (!localUrls.includes(url)) {
|
841 |
+
localUrls.unshift(url); // Add to beginning
|
842 |
+
saveToLocalStorage(localUrls);
|
843 |
+
}
|
844 |
+
|
845 |
loadUrlList();
|
846 |
// If currently in Favorites tab, reload to see changes immediately
|
847 |
if(active === 'Favorites') {
|
|
|
864 |
|
865 |
makeRequest('/api/url/update', 'POST', formData, function(data) {
|
866 |
if(data.success) {
|
867 |
+
// Update localStorage
|
868 |
+
let localUrls = loadFromLocalStorage() || [];
|
869 |
+
const index = localUrls.indexOf(decodedUrl);
|
870 |
+
if (index !== -1) {
|
871 |
+
localUrls[index] = newUrl;
|
872 |
+
saveToLocalStorage(localUrls);
|
873 |
+
}
|
874 |
+
|
875 |
loadUrlList();
|
876 |
// If currently in Favorites tab, reload to see changes immediately
|
877 |
if(active === 'Favorites') {
|
|
|
893 |
|
894 |
makeRequest('/api/url/delete', 'POST', formData, function(data) {
|
895 |
if(data.success) {
|
896 |
+
// Update localStorage
|
897 |
+
let localUrls = loadFromLocalStorage() || [];
|
898 |
+
const index = localUrls.indexOf(decodedUrl);
|
899 |
+
if (index !== -1) {
|
900 |
+
localUrls.splice(index, 1);
|
901 |
+
saveToLocalStorage(localUrls);
|
902 |
+
}
|
903 |
+
|
904 |
loadUrlList();
|
905 |
// If currently in Favorites tab, reload to see changes immediately
|
906 |
if(active === 'Favorites') {
|
|
|
950 |
|
951 |
// Start with Favorites tab
|
952 |
loadFavorites(1);
|
953 |
+
|
954 |
+
// Check for localStorage on page load
|
955 |
+
window.addEventListener('load', function() {
|
956 |
+
// Try to load URLs from localStorage
|
957 |
+
const localUrls = loadFromLocalStorage();
|
958 |
+
|
959 |
+
// If we have URLs in localStorage, make sure they're on the server
|
960 |
+
if (localUrls && localUrls.length > 0) {
|
961 |
+
console.log(`Found ${localUrls.length} URLs in localStorage`);
|
962 |
+
|
963 |
+
// First get server URLs to compare
|
964 |
+
makeRequest('/api/favorites?per_page=1000', 'GET', null, function(data) {
|
965 |
+
const serverUrls = data.items.map(item => item.url);
|
966 |
+
|
967 |
+
// Find URLs that are in localStorage but not on server
|
968 |
+
const missingUrls = localUrls.filter(url => !serverUrls.includes(url));
|
969 |
+
|
970 |
+
if (missingUrls.length > 0) {
|
971 |
+
console.log(`Found ${missingUrls.length} URLs missing from server, restoring...`);
|
972 |
+
|
973 |
+
// Add missing URLs to server
|
974 |
+
let restored = 0;
|
975 |
+
|
976 |
+
function restoreNextUrl(index) {
|
977 |
+
if (index >= missingUrls.length) {
|
978 |
+
console.log(`Restored ${restored} URLs from localStorage`);
|
979 |
+
if (active === 'Favorites') {
|
980 |
+
loadFavorites(currentPage);
|
981 |
+
}
|
982 |
+
return;
|
983 |
+
}
|
984 |
+
|
985 |
+
const formData = new FormData();
|
986 |
+
formData.append('url', missingUrls[index]);
|
987 |
+
|
988 |
+
makeRequest('/api/url/add', 'POST', formData, function(data) {
|
989 |
+
if (data.success) restored++;
|
990 |
+
restoreNextUrl(index + 1);
|
991 |
+
});
|
992 |
+
}
|
993 |
+
|
994 |
+
restoreNextUrl(0);
|
995 |
+
}
|
996 |
+
});
|
997 |
+
}
|
998 |
+
});
|
999 |
</script>
|
1000 |
</body>
|
1001 |
</html>''')
|