openfree commited on
Commit
d06901f
ยท
verified ยท
1 Parent(s): beaf11d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +673 -48
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, render_template, request, jsonify
2
  import os, re, json, sqlite3, logging
3
 
4
  app = Flask(__name__)
@@ -8,6 +8,7 @@ app = Flask(__name__)
8
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
9
  DB_FILE = os.path.join(BASE_DIR, "favorite_sites.json") # JSON file for backward compatibility
10
  SQLITE_DB = os.path.join(BASE_DIR, "favorite_sites.db") # SQLite database for persistence
 
11
 
12
  # Setup logging
13
  logging.basicConfig(level=logging.INFO)
@@ -425,60 +426,684 @@ def delete_url():
425
 
426
  return jsonify({"success": True, "message": "URL deleted successfully"})
427
 
428
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. MAIN ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
429
- @app.route('/')
430
- def home():
431
- # Create a simple initial template for debugging
432
- template_dir = os.path.join(BASE_DIR, 'templates')
433
- os.makedirs(template_dir, exist_ok=True)
434
-
435
- index_html = '''<!DOCTYPE html>
 
436
  <html>
437
  <head>
438
- <meta charset="utf-8">
439
- <meta name="viewport" content="width=device-width, initial-scale=1">
440
- <title>AI Favorite Sites</title>
441
- <style>
442
- body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
443
- h1 { text-align: center; }
444
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  </head>
446
  <body>
447
- <h1>๐ŸŒŸ AI Favorite Sites</h1>
448
- <p style="text-align: center;">
449
- <a href="https://discord.gg/openfreeai" target="_blank">
450
- <img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="Discord">
451
- </a>
452
- </p>
453
- <div id="content">Loading...</div>
454
-
455
- <script>
456
- // Simple alert to check if JavaScript is working
457
- window.onload = function() {
458
- document.getElementById('content').innerHTML = 'JavaScript is working! Loading content...';
459
-
460
- // List available categories
461
- const cats = {{cats|tojson}};
462
- let catList = '<ul>';
463
- cats.forEach(cat => {
464
- catList += '<li>' + cat + '</li>';
465
- });
466
- catList += '</ul>';
467
-
468
- document.getElementById('content').innerHTML += '<p>Available categories:</p>' + catList;
469
- };
470
- </script>
471
- </body>
472
- </html>'''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
- with open(os.path.join(template_dir, 'index.html'), 'w', encoding='utf-8') as f:
475
- f.write(index_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
- # Log for debugging
478
- logger.info(f"Template written to: {os.path.join(template_dir, 'index.html')}")
479
- logger.info(f"Categories: {list(CATEGORIES.keys())}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
 
481
- return render_template('index.html', cats=list(CATEGORIES.keys()))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
 
483
  # Initialize database on startup
484
  init_db()
 
1
+ from flask import Flask, render_template, request, jsonify, send_from_directory
2
  import os, re, json, sqlite3, logging
3
 
4
  app = Flask(__name__)
 
8
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
9
  DB_FILE = os.path.join(BASE_DIR, "favorite_sites.json") # JSON file for backward compatibility
10
  SQLITE_DB = os.path.join(BASE_DIR, "favorite_sites.db") # SQLite database for persistence
11
+ TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
12
 
13
  # Setup logging
14
  logging.basicConfig(level=logging.INFO)
 
426
 
427
  return jsonify({"success": True, "message": "URL deleted successfully"})
428
 
429
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. STATIC ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
430
+ @app.route('/static/<path:filename>')
431
+ def serve_static(filename):
432
+ static_dir = os.path.join(BASE_DIR, 'static')
433
+ os.makedirs(static_dir, exist_ok=True)
434
+ return send_from_directory(static_dir, filename)
435
+
436
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 7. HTML TEMPLATE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
437
+ HTML_TEMPLATE = """<!DOCTYPE html>
438
  <html>
439
  <head>
440
+ <meta charset="utf-8">
441
+ <meta name="viewport" content="width=device-width, initial-scale=1">
442
+ <title>AI Favorite Sites</title>
443
+ <style>
444
+ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;600&display=swap');
445
+ body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb;}
446
+ .tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px;}
447
+ .tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer;}
448
+ .tab.active{background:#a78bfa;color:#1a202c;}
449
+ .tab.manage{background:#ff6e91;color:white;}
450
+ .tab.manage.active{background:#ff2d62;color:white;}
451
+ .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;padding:0 16px 60px;}
452
+ @media(max-width:800px){.grid{grid-template-columns:1fr;}}
453
+ .card{background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);overflow:hidden;height:420px;display:flex;flex-direction:column;position:relative;}
454
+ .frame{flex:1;position:relative;overflow:hidden;}
455
+ .frame iframe{position:absolute;width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left;border:0;}
456
+ .frame img{width:100%;height:100%;object-fit:cover;}
457
+ .card-label{position:absolute;top:10px;left:10px;padding:4px 8px;border-radius:4px;font-size:11px;font-weight:bold;z-index:100;text-transform:uppercase;letter-spacing:0.5px;box-shadow:0 2px 4px rgba(0,0,0,0.2);}
458
+ .label-live{background:linear-gradient(135deg, #00c6ff, #0072ff);color:white;}
459
+ .label-static{background:linear-gradient(135deg, #ff9a9e, #fad0c4);color:#333;}
460
+ .foot{height:44px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee;}
461
+ .foot a{font-size:.82rem;font-weight:700;color:#4a6dd8;text-decoration:none;}
462
+ .pagination{display:flex;justify-content:center;margin:20px 0;gap:10px;}
463
+ .pagination button{padding:5px 15px;border:none;border-radius:20px;background:#e2e8f0;cursor:pointer;}
464
+ .pagination button:disabled{opacity:0.5;cursor:not-allowed;}
465
+ .manage-panel{background:white;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);margin:16px;padding:20px;}
466
+ .form-group{margin-bottom:15px;}
467
+ .form-group label{display:block;margin-bottom:5px;font-weight:600;}
468
+ .form-control{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box;}
469
+ textarea.form-control{min-height:100px;font-family:monospace;resize:vertical;}
470
+ .btn{padding:8px 15px;border:none;border-radius:4px;cursor:pointer;font-weight:600;}
471
+ .btn-primary{background:#4a6dd8;color:white;}
472
+ .btn-danger{background:#e53e3e;color:white;}
473
+ .btn-success{background:#38a169;color:white;}
474
+ .status{padding:10px;margin:10px 0;border-radius:4px;display:none;}
475
+ .status.success{display:block;background:#c6f6d5;color:#22543d;}
476
+ .status.error{display:block;background:#fed7d7;color:#822727;}
477
+ .url-list{margin:20px 0;border:1px solid #eee;border-radius:4px;max-height:300px;overflow-y:auto;}
478
+ .url-item{padding:10px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center;}
479
+ .url-item:last-child{border-bottom:none;}
480
+ .url-controls{display:flex;gap:5px;}
481
+ </style>
482
  </head>
483
  <body>
484
+ <header style="text-align: center; padding: 20px; background: linear-gradient(135deg, #f6f8fb, #e2e8f0); border-bottom: 1px solid #ddd;">
485
+ <h1 style="margin-bottom: 10px;">๐ŸŒŸ AI Favorite Sites</h1>
486
+ <p class="description" style="margin-bottom: 15px;">
487
+ ๐Ÿš€ <strong>Free AI Spaces & Website Gallery</strong> โœจ Save and manage your favorite sites! Supports <span style="background: linear-gradient(135deg, #00c6ff, #0072ff); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">LIVE</span> and <span style="background: linear-gradient(135deg, #ff9a9e, #fad0c4); color: #333; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Static</span> preview modes.
488
+ </p>
489
+ <p>
490
+ <a href="https://discord.gg/openfreeai" target="_blank"><img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="badge"></a>
491
+ </p>
492
+ </header>
493
+ <div class="tabs" id="tabs"></div>
494
+ <div id="content"></div>
495
+
496
+ <script>
497
+ // Basic configuration
498
+ const cats = %s;
499
+ const tabs = document.getElementById('tabs');
500
+ const content = document.getElementById('content');
501
+ let active = "";
502
+ let currentPage = 1;
503
+
504
+ // LocalStorage functionality for URL persistence
505
+ function saveToLocalStorage(urls) {
506
+ try {
507
+ localStorage.setItem('favoriteUrls', JSON.stringify(urls));
508
+ console.log('Saved URLs to localStorage:', urls.length);
509
+ return true;
510
+ } catch (e) {
511
+ console.error('Error saving to localStorage:', e);
512
+ return false;
513
+ }
514
+ }
515
+
516
+ function loadFromLocalStorage() {
517
+ try {
518
+ const data = localStorage.getItem('favoriteUrls');
519
+ if (data) {
520
+ const urls = JSON.parse(data);
521
+ console.log('Loaded URLs from localStorage:', urls.length);
522
+ return urls;
523
+ }
524
+ } catch (e) {
525
+ console.error('Error loading from localStorage:', e);
526
+ }
527
+ return null;
528
+ }
529
+
530
+ // Export/Import functionality
531
+ function exportUrls() {
532
+ makeRequest('/api/favorites?per_page=1000', 'GET', null, function(data) {
533
+ if (data.items && data.items.length > 0) {
534
+ const urls = data.items.map(item => item.url);
535
+
536
+ // Save to localStorage as backup
537
+ saveToLocalStorage(urls);
538
+
539
+ // Create file for download
540
+ const blob = new Blob([JSON.stringify(urls, null, 2)], { type: 'application/json' });
541
+ const a = document.createElement('a');
542
+ a.href = URL.createObjectURL(blob);
543
+ a.download = 'favorite_urls.json';
544
+ document.body.appendChild(a);
545
+ a.click();
546
+ document.body.removeChild(a);
547
+ } else {
548
+ alert('No URLs to export');
549
+ }
550
+ });
551
+ }
552
+
553
+ function importUrls() {
554
+ document.getElementById('import-file').click();
555
+ }
556
+
557
+ function handleImportFile(files) {
558
+ if (files.length === 0) return;
559
+
560
+ const file = files[0];
561
+ const reader = new FileReader();
562
+
563
+ reader.onload = function(e) {
564
+ try {
565
+ const urls = JSON.parse(e.target.result);
566
+ if (Array.isArray(urls)) {
567
+ // Save to localStorage
568
+ saveToLocalStorage(urls);
569
+
570
+ // Add each URL to the server
571
+ let processed = 0;
572
+
573
+ function addNextUrl(index) {
574
+ if (index >= urls.length) {
575
+ alert(`Import complete. Added ${processed} URLs.`);
576
+ loadUrlList();
577
+ if (active === 'Favorites') {
578
+ loadFavorites(currentPage);
579
+ }
580
+ return;
581
+ }
582
+
583
+ const formData = new FormData();
584
+ formData.append('url', urls[index]);
585
+
586
+ makeRequest('/api/url/add', 'POST', formData, function(data) {
587
+ if (data.success) processed++;
588
+ addNextUrl(index + 1);
589
+ });
590
+ }
591
+
592
+ addNextUrl(0);
593
+ } else {
594
+ alert('Invalid format. File must contain a JSON array of URLs.');
595
+ }
596
+ } catch (e) {
597
+ alert('Error parsing file: ' + e.message);
598
+ }
599
+ };
600
+
601
+ reader.readAsText(file);
602
+ }
603
+
604
+ // Simple utility functions
605
+ function makeRequest(url, method, data, callback) {
606
+ const xhr = new XMLHttpRequest();
607
+ xhr.open(method, url, true);
608
+ xhr.onreadystatechange = function() {
609
+ if (xhr.readyState === 4 && xhr.status === 200) {
610
+ callback(JSON.parse(xhr.responseText));
611
+ }
612
+ };
613
+ if (method === 'POST') {
614
+ xhr.send(data);
615
+ } else {
616
+ xhr.send();
617
+ }
618
+ }
619
+
620
+ function updateTabs() {
621
+ Array.from(tabs.children).forEach(b => {
622
+ b.classList.toggle('active', b.dataset.c === active);
623
+ });
624
+ }
625
+
626
+ // Tab handlers
627
+ function loadCategory(cat) {
628
+ if(cat === active) return;
629
+ active = cat;
630
+ updateTabs();
631
+
632
+ content.innerHTML = '<p style="text-align:center;padding:40px">Loadingโ€ฆ</p>';
633
+
634
+ makeRequest('/api/category?name=' + encodeURIComponent(cat), 'GET', null, function(data) {
635
+ let html = '<div class="grid">';
636
+
637
+ data.forEach(item => {
638
+ html += `
639
+ <div class="card">
640
+ <div class="card-label label-live">LIVE</div>
641
+ <div class="frame">
642
+ <iframe src="${item.iframe}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
643
+ </div>
644
+ <div class="foot">
645
+ <a href="${item.hf}" target="_blank">${item.title}</a>
646
+ </div>
647
+ </div>
648
+ `;
649
+ });
650
 
651
+ html += '</div>';
652
+ content.innerHTML = html;
653
+ });
654
+ }
655
+
656
+ function loadFavorites(page) {
657
+ if(active === 'Favorites' && currentPage === page) return;
658
+ active = 'Favorites';
659
+ currentPage = page || 1;
660
+ updateTabs();
661
+
662
+ content.innerHTML = '<p style="text-align:center;padding:40px">Loadingโ€ฆ</p>';
663
+
664
+ makeRequest('/api/favorites?page=' + currentPage, 'GET', null, function(data) {
665
+ let html = '<div class="grid">';
666
+
667
+ if(data.items.length === 0) {
668
+ html += '<p style="grid-column:1/-1;text-align:center;padding:40px">No favorites saved yet.</p>';
669
+ } else {
670
+ data.items.forEach(item => {
671
+ if(item.mode === 'snapshot') {
672
+ html += `
673
+ <div class="card">
674
+ <div class="card-label label-static">Static</div>
675
+ <div class="frame">
676
+ <img src="${item.preview_url}" loading="lazy">
677
+ </div>
678
+ <div class="foot">
679
+ <a href="${item.url}" target="_blank">${item.title}</a>
680
+ </div>
681
+ </div>
682
+ `;
683
+ } else {
684
+ html += `
685
+ <div class="card">
686
+ <div class="card-label label-live">LIVE</div>
687
+ <div class="frame">
688
+ <iframe src="${item.preview_url}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
689
+ </div>
690
+ <div class="foot">
691
+ <a href="${item.url}" target="_blank">${item.title}</a>
692
+ </div>
693
+ </div>
694
+ `;
695
+ }
696
+ });
697
+ }
698
+
699
+ html += '</div>';
700
+
701
+ // Add pagination
702
+ html += `
703
+ <div class="pagination">
704
+ <button ${currentPage <= 1 ? 'disabled' : ''} onclick="loadFavorites(${currentPage-1})">ยซ Previous</button>
705
+ <span>Page ${currentPage} of ${data.total_pages}</span>
706
+ <button ${currentPage >= data.total_pages ? 'disabled' : ''} onclick="loadFavorites(${currentPage+1})">Next ยป</button>
707
+ </div>
708
+ `;
709
 
710
+ content.innerHTML = html;
711
+ });
712
+ }
713
+
714
+ function loadManage() {
715
+ if(active === 'Manage') return;
716
+ active = 'Manage';
717
+ updateTabs();
718
+
719
+ content.innerHTML = `
720
+ <div class="manage-panel">
721
+ <h2>Add New URL</h2>
722
+ <div class="form-group">
723
+ <label for="new-url">Single URL</label>
724
+ <input type="text" id="new-url" class="form-control" placeholder="https://example.com">
725
+ <button onclick="addUrl()" class="btn btn-primary" style="margin-top:10px">Add URL</button>
726
+ </div>
727
+
728
+ <div class="form-group" style="margin-top:20px">
729
+ <label for="batch-urls">Multiple URLs (up to 100 URLs, one per line)</label>
730
+ <textarea id="batch-urls" class="form-control" rows="8" placeholder="https://example1.com&#10;https://example2.com&#10;https://example3.com"></textarea>
731
+ <button onclick="addBatchUrls()" class="btn btn-primary" style="margin-top:10px">Add All URLs</button>
732
+ </div>
733
+ <div id="add-status" class="status"></div>
734
+ <div id="progress-bar" style="display:none; margin:15px 0;">
735
+ <div style="height:20px; background-color:#f0f0f0; border-radius:4px; overflow:hidden;">
736
+ <div id="progress-fill" style="height:100%; width:0%; background-color:#4a6dd8; transition:width 0.3s;"></div>
737
+ </div>
738
+ <div id="progress-text" style="text-align:center; margin-top:5px; font-size:14px;">0%%</div>
739
+ </div>
740
+
741
+ <h2>Manage Saved URLs</h2>
742
+ <div id="url-list" class="url-list">Loading...</div>
743
+
744
+ <div style="margin-top: 30px;">
745
+ <h3>Backup & Restore</h3>
746
+ <p>Server storage may not persist across restarts. Use these options to save your data:</p>
747
+ <button onclick="exportUrls()" class="btn btn-success">Export URLs</button>
748
+ <button onclick="importUrls()" class="btn btn-primary">Import URLs</button>
749
+ <input type="file" id="import-file" style="display:none" onchange="handleImportFile(this.files)">
750
+ </div>
751
+ </div>
752
+ `;
753
+
754
+ loadUrlList();
755
+ }
756
+
757
+ // URL management functions
758
+ function loadUrlList() {
759
+ // First try to load from localStorage as a fallback
760
+ const localUrls = loadFromLocalStorage();
761
+
762
+ makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
763
+ const urlList = document.getElementById('url-list');
764
 
765
+ // If server has no URLs but localStorage does, restore from localStorage
766
+ if (data.items.length === 0 && localUrls && localUrls.length > 0) {
767
+ showStatus('add-status', 'Restoring URLs from local backup...', true);
768
+
769
+ // Add each URL from localStorage to the server
770
+ let restored = 0;
771
+
772
+ function restoreNextUrl(index) {
773
+ if (index >= localUrls.length) {
774
+ showStatus('add-status', `Restored ${restored} URLs from local backup`, true);
775
+ // Reload the list after restoration
776
+ setTimeout(() => {
777
+ loadUrlList();
778
+ if (active === 'Favorites') {
779
+ loadFavorites(currentPage);
780
+ }
781
+ }, 1000);
782
+ return;
783
+ }
784
+
785
+ const formData = new FormData();
786
+ formData.append('url', localUrls[index]);
787
+
788
+ makeRequest('/api/url/add', 'POST', formData, function(data) {
789
+ if (data.success) restored++;
790
+ restoreNextUrl(index + 1);
791
+ });
792
+ }
793
+
794
+ restoreNextUrl(0);
795
+ return;
796
+ }
797
+
798
+ if(data.items.length === 0) {
799
+ urlList.innerHTML = '<p style="text-align:center;padding:20px">No URLs saved yet.</p>';
800
+ return;
801
+ }
802
+
803
+ // Update localStorage with server data
804
+ saveToLocalStorage(data.items.map(item => item.url));
805
+
806
+ let html = '';
807
+ data.items.forEach(item => {
808
+ // Escape the URL to prevent JavaScript injection when used in onclick handlers
809
+ const escapedUrl = item.url.replace(/'/g, "\\'");
810
+
811
+ html += `
812
+ <div class="url-item">
813
+ <div>${item.url}</div>
814
+ <div class="url-controls">
815
+ <button class="btn" onclick="editUrl('${escapedUrl}')">Edit</button>
816
+ <button class="btn btn-danger" onclick="deleteUrl('${escapedUrl}')">Delete</button>
817
+ </div>
818
+ </div>
819
+ `;
820
+ });
821
+
822
+ urlList.innerHTML = html;
823
+ });
824
+ }
825
+
826
+ function addUrl() {
827
+ const url = document.getElementById('new-url').value.trim();
828
+
829
+ if(!url) {
830
+ showStatus('add-status', 'Please enter a URL', false);
831
+ return;
832
+ }
833
+
834
+ const formData = new FormData();
835
+ formData.append('url', url);
836
+
837
+ makeRequest('/api/url/add', 'POST', formData, function(data) {
838
+ showStatus('add-status', data.message, data.success);
839
+ if(data.success) {
840
+ document.getElementById('new-url').value = '';
841
+
842
+ // Update localStorage
843
+ const localUrls = loadFromLocalStorage() || [];
844
+ if (!localUrls.includes(url)) {
845
+ localUrls.unshift(url); // Add to beginning
846
+ saveToLocalStorage(localUrls);
847
+ }
848
+
849
+ loadUrlList();
850
+ // If currently in Favorites tab, reload to see changes immediately
851
+ if(active === 'Favorites') {
852
+ loadFavorites(currentPage);
853
+ }
854
+ }
855
+ });
856
+ }
857
+
858
+ function addBatchUrls() {
859
+ const textarea = document.getElementById('batch-urls');
860
+ const text = textarea.value.trim();
861
+
862
+ if (!text) {
863
+ showStatus('add-status', 'Please enter at least one URL', false);
864
+ return;
865
+ }
866
+
867
+ // Split by newlines and filter out empty lines
868
+ let urls = text.split(/\r?\n/).filter(url => url.trim() !== '');
869
+
870
+ // Limit to 100 URLs
871
+ if (urls.length > 100) {
872
+ showStatus('add-status', 'Too many URLs. Limited to 100 at once.', false);
873
+ urls = urls.slice(0, 100);
874
+ }
875
+
876
+ if (urls.length === 0) {
877
+ showStatus('add-status', 'No valid URLs found', false);
878
+ return;
879
+ }
880
+
881
+ // Show progress bar
882
+ const progressBar = document.getElementById('progress-bar');
883
+ const progressFill = document.getElementById('progress-fill');
884
+ const progressText = document.getElementById('progress-text');
885
+ progressBar.style.display = 'block';
886
+ progressFill.style.width = '0%%';
887
+ progressText.textContent = '0%%';
888
+
889
+ // Add URLs one by one
890
+ let processed = 0;
891
+ let succeeded = 0;
892
+
893
+ function updateProgress() {
894
+ const percentage = Math.round((processed / urls.length) * 100);
895
+ progressFill.style.width = percentage + '%%';
896
+ progressText.textContent = `${processed}/${urls.length} (${percentage}%%)`;
897
+ }
898
+
899
+ function addNextUrl(index) {
900
+ if (index >= urls.length) {
901
+ // All done
902
+ setTimeout(() => {
903
+ progressBar.style.display = 'none';
904
+ textarea.value = '';
905
+ showStatus('add-status', `Added ${succeeded} of ${urls.length} URLs successfully`, true);
906
+
907
+ // Reload URL list and favorites
908
+ loadUrlList();
909
+ if (active === 'Favorites') {
910
+ loadFavorites(currentPage);
911
+ }
912
+ }, 500);
913
+ return;
914
+ }
915
+
916
+ const url = urls[index].trim();
917
+ if (!url) {
918
+ // Skip empty URLs
919
+ processed++;
920
+ updateProgress();
921
+ addNextUrl(index + 1);
922
+ return;
923
+ }
924
+
925
+ const formData = new FormData();
926
+ formData.append('url', url);
927
+
928
+ makeRequest('/api/url/add', 'POST', formData, function(data) {
929
+ processed++;
930
+
931
+ if (data.success) {
932
+ succeeded++;
933
+
934
+ // Update localStorage
935
+ const localUrls = loadFromLocalStorage() || [];
936
+ if (!localUrls.includes(url)) {
937
+ localUrls.unshift(url);
938
+ saveToLocalStorage(localUrls);
939
+ }
940
+ }
941
+
942
+ updateProgress();
943
+ addNextUrl(index + 1);
944
+ });
945
+ }
946
+
947
+ // Start adding URLs
948
+ addNextUrl(0);
949
+ }
950
+
951
+ function editUrl(url) {
952
+ // Decode URL if it was previously escaped
953
+ const decodedUrl = url.replace(/\\'/g, "'");
954
+ const newUrl = prompt('Edit URL:', decodedUrl);
955
+
956
+ if(!newUrl || newUrl === decodedUrl) return;
957
+
958
+ const formData = new FormData();
959
+ formData.append('old', decodedUrl);
960
+ formData.append('new', newUrl);
961
+
962
+ makeRequest('/api/url/update', 'POST', formData, function(data) {
963
+ if(data.success) {
964
+ // Update localStorage
965
+ let localUrls = loadFromLocalStorage() || [];
966
+ const index = localUrls.indexOf(decodedUrl);
967
+ if (index !== -1) {
968
+ localUrls[index] = newUrl;
969
+ saveToLocalStorage(localUrls);
970
+ }
971
+
972
+ loadUrlList();
973
+ // If currently in Favorites tab, reload to see changes immediately
974
+ if(active === 'Favorites') {
975
+ loadFavorites(currentPage);
976
+ }
977
+ } else {
978
+ alert(data.message);
979
+ }
980
+ });
981
+ }
982
+
983
+ function deleteUrl(url) {
984
+ // Decode URL if it was previously escaped
985
+ const decodedUrl = url.replace(/\\'/g, "'");
986
+ if(!confirm('Are you sure you want to delete this URL?')) return;
987
+
988
+ const formData = new FormData();
989
+ formData.append('url', decodedUrl);
990
+
991
+ makeRequest('/api/url/delete', 'POST', formData, function(data) {
992
+ if(data.success) {
993
+ // Update localStorage
994
+ let localUrls = loadFromLocalStorage() || [];
995
+ const index = localUrls.indexOf(decodedUrl);
996
+ if (index !== -1) {
997
+ localUrls.splice(index, 1);
998
+ saveToLocalStorage(localUrls);
999
+ }
1000
+
1001
+ loadUrlList();
1002
+ // If currently in Favorites tab, reload to see changes immediately
1003
+ if(active === 'Favorites') {
1004
+ loadFavorites(currentPage);
1005
+ }
1006
+ } else {
1007
+ alert(data.message);
1008
+ }
1009
+ });
1010
+ }
1011
+
1012
+ function showStatus(id, message, success) {
1013
+ const status = document.getElementById(id);
1014
+ status.textContent = message;
1015
+ status.className = success ? 'status success' : 'status error';
1016
+ setTimeout(() => {
1017
+ status.className = 'status';
1018
+ }, 3000);
1019
+ }
1020
+
1021
+ // Create tabs
1022
+ // Favorites tab first
1023
+ const favTab = document.createElement('button');
1024
+ favTab.className = 'tab';
1025
+ favTab.textContent = 'Favorites';
1026
+ favTab.dataset.c = 'Favorites';
1027
+ favTab.onclick = function() { loadFavorites(1); };
1028
+ tabs.appendChild(favTab);
1029
+
1030
+ // Category tabs
1031
+ cats.forEach(c => {
1032
+ const b = document.createElement('button');
1033
+ b.className = 'tab';
1034
+ b.textContent = c;
1035
+ b.dataset.c = c;
1036
+ b.onclick = function() { loadCategory(c); };
1037
+ tabs.appendChild(b);
1038
+ });
1039
+
1040
+ // Manage tab last
1041
+ const manageTab = document.createElement('button');
1042
+ manageTab.className = 'tab manage';
1043
+ manageTab.textContent = 'Manage';
1044
+ manageTab.dataset.c = 'Manage';
1045
+ manageTab.onclick = function() { loadManage(); };
1046
+ tabs.appendChild(manageTab);
1047
+
1048
+ // Start with Favorites tab
1049
+ loadFavorites(1);
1050
+
1051
+ // Check for localStorage on page load
1052
+ window.addEventListener('load', function() {
1053
+ // Try to load URLs from localStorage
1054
+ const localUrls = loadFromLocalStorage();
1055
+
1056
+ // If we have URLs in localStorage, make sure they're on the server
1057
+ if (localUrls && localUrls.length > 0) {
1058
+ console.log(`Found ${localUrls.length} URLs in localStorage`);
1059
+
1060
+ // First get server URLs to compare
1061
+ makeRequest('/api/favorites?per_page=1000', 'GET', null, function(data) {
1062
+ const serverUrls = data.items.map(item => item.url);
1063
+
1064
+ // Find URLs that are in localStorage but not on server
1065
+ const missingUrls = localUrls.filter(url => !serverUrls.includes(url));
1066
+
1067
+ if (missingUrls.length > 0) {
1068
+ console.log(`Found ${missingUrls.length} URLs missing from server, restoring...`);
1069
+
1070
+ // Add missing URLs to server
1071
+ let restored = 0;
1072
+
1073
+ function restoreNextUrl(index) {
1074
+ if (index >= missingUrls.length) {
1075
+ console.log(`Restored ${restored} URLs from localStorage`);
1076
+ if (active === 'Favorites') {
1077
+ loadFavorites(currentPage);
1078
+ }
1079
+ return;
1080
+ }
1081
+
1082
+ const formData = new FormData();
1083
+ formData.append('url', missingUrls[index]);
1084
+
1085
+ makeRequest('/api/url/add', 'POST', formData, function(data) {
1086
+ if (data.success) restored++;
1087
+ restoreNextUrl(index + 1);
1088
+ });
1089
+ }
1090
+
1091
+ restoreNextUrl(0);
1092
+ }
1093
+ });
1094
+ }
1095
+ });
1096
+ </script>
1097
+ </body>
1098
+ </html>
1099
+ """
1100
+
1101
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 8. MAIN ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1102
+ @app.route('/')
1103
+ def home():
1104
+ # Instead of writing to a file, we'll serve the HTML directly
1105
+ categories_json = json.dumps(list(CATEGORIES.keys()))
1106
+ return HTML_TEMPLATE % categories_json
1107
 
1108
  # Initialize database on startup
1109
  init_db()