openfree commited on
Commit
c398a0b
Β·
verified Β·
1 Parent(s): 22e7760

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -27
app.py CHANGED
@@ -1,10 +1,11 @@
1
  from flask import Flask, render_template, request, jsonify
2
- import os, re, json
3
 
4
  app = Flask(__name__)
5
 
6
  # ────────────────────────── 1. CONFIGURATION ──────────────────────────
7
- DB_FILE = "favorite_sites.json" # Database file for user-saved sites
 
8
 
9
  # Domains that commonly block iframes
10
  BLOCKED_DOMAINS = [
@@ -15,7 +16,7 @@ BLOCKED_DOMAINS = [
15
 
16
  # ────────────────────────── 2. CURATED CATEGORIES ──────────────────────────
17
  CATEGORIES = {
18
- "Free AI: Productivity": [
19
  "https://huggingface.co/spaces/ginigen/perflexity-clone",
20
  "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
21
  "https://huggingface.co/spaces/VIDraft/mouse-webgen",
@@ -26,7 +27,7 @@ CATEGORIES = {
26
  "https://huggingface.co/spaces/fantaxy/Space-Leaderboard",
27
  "https://huggingface.co/spaces/openfree/Korean-Leaderboard",
28
  ],
29
- "Free AI: Multimodal": [
30
  "https://huggingface.co/spaces/openfree/DreamO-video",
31
  "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
32
  "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
@@ -36,7 +37,7 @@ CATEGORIES = {
36
  "https://huggingface.co/spaces/aiqcamp/MCP-kokoro",
37
  "https://huggingface.co/spaces/aiqcamp/ENGLISH-Speaking-Scoring",
38
  ],
39
- "Free AI: Professional": [
40
  "https://huggingface.co/spaces/ginigen/blogger",
41
  "https://huggingface.co/spaces/VIDraft/money-radar",
42
  "https://huggingface.co/spaces/immunobiotech/drug-discovery",
@@ -46,7 +47,7 @@ CATEGORIES = {
46
  "https://huggingface.co/spaces/ginipick/AgentX-Papers",
47
  "https://huggingface.co/spaces/openfree/Cycle-Navigator",
48
  ],
49
- "Free AI: Image": [
50
  "https://huggingface.co/spaces/ginigen/interior-design",
51
  "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
52
  "https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
@@ -60,7 +61,7 @@ CATEGORIES = {
60
  "https://huggingface.co/spaces/VIDraft/Open-Meme-Studio",
61
  "https://huggingface.co/spaces/ginigen/3D-LLAMA",
62
  ],
63
- "Free AI: LLM / VLM": [
64
  "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-4B",
65
  "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-12B",
66
  "https://huggingface.co/spaces/ginigen/Mistral-Perflexity",
@@ -73,11 +74,36 @@ CATEGORIES = {
73
 
74
  # ────────────────────────── 3. DATABASE FUNCTIONS ──────────────────────────
75
  def init_db():
 
76
  if not os.path.exists(DB_FILE):
77
  with open(DB_FILE, "w", encoding="utf-8") as f:
78
  json.dump([], f, ensure_ascii=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- def load_db():
 
81
  try:
82
  with open(DB_FILE, "r", encoding="utf-8") as f:
83
  raw = json.load(f)
@@ -85,7 +111,8 @@ def load_db():
85
  except Exception:
86
  return []
87
 
88
- def save_db(lst):
 
89
  try:
90
  with open(DB_FILE, "w", encoding="utf-8") as f:
91
  json.dump(lst, f, ensure_ascii=False, indent=2)
@@ -93,6 +120,87 @@ def save_db(lst):
93
  except Exception:
94
  return False
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  # ────────────────────────── 4. URL HELPERS ──────────────────────────
97
  def direct_url(hf_url):
98
  m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
@@ -148,7 +256,9 @@ def api_category():
148
 
149
  @app.route('/api/favorites')
150
  def api_favorites():
 
151
  urls = load_db()
 
152
  page = int(request.args.get('page', 1))
153
  per_page = int(request.args.get('per_page', 9))
154
 
@@ -189,12 +299,16 @@ def add_url():
189
  if not url:
190
  return jsonify({"success": False, "message": "URL is required"})
191
 
192
- data = load_db()
193
- if url in data:
194
  return jsonify({"success": False, "message": "URL already exists"})
195
 
196
- data.insert(0, url)
197
- save_db(data)
 
 
 
 
198
  return jsonify({"success": True, "message": "URL added successfully"})
199
 
200
  @app.route('/api/url/update', methods=['POST'])
@@ -205,25 +319,40 @@ def update_url():
205
  if not new:
206
  return jsonify({"success": False, "message": "New URL is required"})
207
 
208
- data = load_db()
 
 
 
 
 
209
  try:
210
  idx = data.index(old)
211
  data[idx] = new
212
- save_db(data)
213
- return jsonify({"success": True, "message": "URL updated successfully"})
214
  except ValueError:
215
- return jsonify({"success": False, "message": "URL not found"})
 
 
 
 
216
 
217
  @app.route('/api/url/delete', methods=['POST'])
218
  def delete_url():
219
  url = request.form.get('url', '')
220
- data = load_db()
 
 
 
 
 
 
221
  try:
222
  data.remove(url)
223
- save_db(data)
224
- return jsonify({"success": True, "message": "URL deleted successfully"})
225
  except ValueError:
226
- return jsonify({"success": False, "message": "URL not found"})
 
 
227
 
228
  # ────────────────────────── 6. MAIN ROUTES ──────────────────────────
229
  @app.route('/')
@@ -279,6 +408,7 @@ body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb;}
279
  <body>
280
  <div class="tabs" id="tabs"></div>
281
  <div id="content"></div>
 
282
  <script>
283
  // Basic configuration
284
  const cats = {{cats|tojson}};
@@ -286,6 +416,7 @@ const tabs = document.getElementById('tabs');
286
  const content = document.getElementById('content');
287
  let active = "";
288
  let currentPage = 1;
 
289
  // Simple utility functions
290
  function loadHTML(url, callback) {
291
  const xhr = new XMLHttpRequest();
@@ -297,6 +428,7 @@ function loadHTML(url, callback) {
297
  };
298
  xhr.send();
299
  }
 
300
  function makeRequest(url, method, data, callback) {
301
  const xhr = new XMLHttpRequest();
302
  xhr.open(method, url, true);
@@ -311,11 +443,13 @@ function makeRequest(url, method, data, callback) {
311
  xhr.send();
312
  }
313
  }
 
314
  function updateTabs() {
315
  Array.from(tabs.children).forEach(b => {
316
  b.classList.toggle('active', b.dataset.c === active);
317
  });
318
  }
 
319
  // Tab handlers
320
  function loadCategory(cat) {
321
  if(cat === active) return;
@@ -330,6 +464,7 @@ function loadCategory(cat) {
330
  data.forEach(item => {
331
  html += `
332
  <div class="card">
 
333
  <div class="frame">
334
  <iframe src="${item.iframe}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
335
  </div>
@@ -344,6 +479,7 @@ function loadCategory(cat) {
344
  content.innerHTML = html;
345
  });
346
  }
 
347
  function loadFavorites(page) {
348
  if(active === 'Favorites' && currentPage === page) return;
349
  active = 'Favorites';
@@ -401,6 +537,7 @@ function loadFavorites(page) {
401
  content.innerHTML = html;
402
  });
403
  }
 
404
  function loadManage() {
405
  if(active === 'Manage') return;
406
  active = 'Manage';
@@ -423,6 +560,7 @@ function loadManage() {
423
 
424
  loadUrlList();
425
  }
 
426
  // URL management functions
427
  function loadUrlList() {
428
  makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
@@ -435,12 +573,15 @@ function loadUrlList() {
435
 
436
  let html = '';
437
  data.items.forEach(item => {
 
 
 
438
  html += `
439
  <div class="url-item">
440
  <div>${item.url}</div>
441
  <div class="url-controls">
442
- <button class="btn" onclick="editUrl('${item.url}')">Edit</button>
443
- <button class="btn btn-danger" onclick="deleteUrl('${item.url}')">Delete</button>
444
  </div>
445
  </div>
446
  `;
@@ -449,6 +590,7 @@ function loadUrlList() {
449
  urlList.innerHTML = html;
450
  });
451
  }
 
452
  function addUrl() {
453
  const url = document.getElementById('new-url').value.trim();
454
 
@@ -465,40 +607,59 @@ function addUrl() {
465
  if(data.success) {
466
  document.getElementById('new-url').value = '';
467
  loadUrlList();
 
 
 
 
468
  }
469
  });
470
  }
 
471
  function editUrl(url) {
472
- const newUrl = prompt('Edit URL:', url);
 
 
473
 
474
- if(!newUrl || newUrl === url) return;
475
 
476
  const formData = new FormData();
477
- formData.append('old', url);
478
  formData.append('new', newUrl);
479
 
480
  makeRequest('/api/url/update', 'POST', formData, function(data) {
481
  if(data.success) {
482
  loadUrlList();
 
 
 
 
483
  } else {
484
  alert(data.message);
485
  }
486
  });
487
  }
 
488
  function deleteUrl(url) {
 
 
489
  if(!confirm('Are you sure you want to delete this URL?')) return;
490
 
491
  const formData = new FormData();
492
- formData.append('url', url);
493
 
494
  makeRequest('/api/url/delete', 'POST', formData, function(data) {
495
  if(data.success) {
496
  loadUrlList();
 
 
 
 
497
  } else {
498
  alert(data.message);
499
  }
500
  });
501
  }
 
502
  function showStatus(id, message, success) {
503
  const status = document.getElementById(id);
504
  status.textContent = message;
@@ -507,6 +668,7 @@ function showStatus(id, message, success) {
507
  status.className = 'status';
508
  }, 3000);
509
  }
 
510
  // Create tabs
511
  // Favorites tab first
512
  const favTab = document.createElement('button');
@@ -515,6 +677,7 @@ favTab.textContent = 'Favorites';
515
  favTab.dataset.c = 'Favorites';
516
  favTab.onclick = function() { loadFavorites(1); };
517
  tabs.appendChild(favTab);
 
518
  // Category tabs
519
  cats.forEach(c => {
520
  const b = document.createElement('button');
@@ -524,6 +687,7 @@ cats.forEach(c => {
524
  b.onclick = function() { loadCategory(c); };
525
  tabs.appendChild(b);
526
  });
 
527
  // Manage tab last
528
  const manageTab = document.createElement('button');
529
  manageTab.className = 'tab manage';
@@ -531,6 +695,7 @@ manageTab.textContent = 'Manage';
531
  manageTab.dataset.c = 'Manage';
532
  manageTab.onclick = function() { loadManage(); };
533
  tabs.appendChild(manageTab);
 
534
  // Start with Favorites tab
535
  loadFavorites(1);
536
  </script>
@@ -543,5 +708,12 @@ loadFavorites(1);
543
  # Initialize database on startup
544
  init_db()
545
 
 
 
 
 
 
 
 
546
  if __name__ == '__main__':
547
  app.run(host='0.0.0.0', port=7860)
 
1
  from flask import Flask, render_template, request, jsonify
2
+ import os, re, json, sqlite3
3
 
4
  app = Flask(__name__)
5
 
6
  # ────────────────────────── 1. CONFIGURATION ──────────────────────────
7
+ DB_FILE = "favorite_sites.json" # JSON file for backward compatibility
8
+ SQLITE_DB = "favorite_sites.db" # SQLite database for persistence
9
 
10
  # Domains that commonly block iframes
11
  BLOCKED_DOMAINS = [
 
16
 
17
  # ────────────────────────── 2. CURATED CATEGORIES ──────────────────────────
18
  CATEGORIES = {
19
+ "Productivity": [
20
  "https://huggingface.co/spaces/ginigen/perflexity-clone",
21
  "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
22
  "https://huggingface.co/spaces/VIDraft/mouse-webgen",
 
27
  "https://huggingface.co/spaces/fantaxy/Space-Leaderboard",
28
  "https://huggingface.co/spaces/openfree/Korean-Leaderboard",
29
  ],
30
+ "Multimodal": [
31
  "https://huggingface.co/spaces/openfree/DreamO-video",
32
  "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
33
  "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
 
37
  "https://huggingface.co/spaces/aiqcamp/MCP-kokoro",
38
  "https://huggingface.co/spaces/aiqcamp/ENGLISH-Speaking-Scoring",
39
  ],
40
+ "Professional": [
41
  "https://huggingface.co/spaces/ginigen/blogger",
42
  "https://huggingface.co/spaces/VIDraft/money-radar",
43
  "https://huggingface.co/spaces/immunobiotech/drug-discovery",
 
47
  "https://huggingface.co/spaces/ginipick/AgentX-Papers",
48
  "https://huggingface.co/spaces/openfree/Cycle-Navigator",
49
  ],
50
+ "Image": [
51
  "https://huggingface.co/spaces/ginigen/interior-design",
52
  "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
53
  "https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
 
61
  "https://huggingface.co/spaces/VIDraft/Open-Meme-Studio",
62
  "https://huggingface.co/spaces/ginigen/3D-LLAMA",
63
  ],
64
+ "LLM / VLM": [
65
  "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-4B",
66
  "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-12B",
67
  "https://huggingface.co/spaces/ginigen/Mistral-Perflexity",
 
74
 
75
  # ────────────────────────── 3. DATABASE FUNCTIONS ──────────────────────────
76
  def init_db():
77
+ # Initialize JSON file if it doesn't exist
78
  if not os.path.exists(DB_FILE):
79
  with open(DB_FILE, "w", encoding="utf-8") as f:
80
  json.dump([], f, ensure_ascii=False)
81
+
82
+ # Initialize SQLite database
83
+ conn = sqlite3.connect(SQLITE_DB)
84
+ cursor = conn.cursor()
85
+ cursor.execute('''
86
+ CREATE TABLE IF NOT EXISTS urls (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ url TEXT UNIQUE NOT NULL,
89
+ date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP
90
+ )
91
+ ''')
92
+ conn.commit()
93
+
94
+ # If we have data in JSON but not in SQLite (first run with new SQLite DB),
95
+ # migrate the data from JSON to SQLite
96
+ json_urls = load_json()
97
+ if json_urls:
98
+ db_urls = load_db_sqlite()
99
+ for url in json_urls:
100
+ if url not in db_urls:
101
+ add_url_to_sqlite(url)
102
+
103
+ conn.close()
104
 
105
+ def load_json():
106
+ """Load URLs from JSON file (for backward compatibility)"""
107
  try:
108
  with open(DB_FILE, "r", encoding="utf-8") as f:
109
  raw = json.load(f)
 
111
  except Exception:
112
  return []
113
 
114
+ def save_json(lst):
115
+ """Save URLs to JSON file (for backward compatibility)"""
116
  try:
117
  with open(DB_FILE, "w", encoding="utf-8") as f:
118
  json.dump(lst, f, ensure_ascii=False, indent=2)
 
120
  except Exception:
121
  return False
122
 
123
+ def load_db_sqlite():
124
+ """Load URLs from SQLite database"""
125
+ conn = sqlite3.connect(SQLITE_DB)
126
+ cursor = conn.cursor()
127
+ cursor.execute("SELECT url FROM urls ORDER BY date_added DESC")
128
+ urls = [row[0] for row in cursor.fetchall()]
129
+ conn.close()
130
+ return urls
131
+
132
+ def add_url_to_sqlite(url):
133
+ """Add a URL to SQLite database"""
134
+ conn = sqlite3.connect(SQLITE_DB)
135
+ cursor = conn.cursor()
136
+ try:
137
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
138
+ conn.commit()
139
+ success = True
140
+ except sqlite3.IntegrityError:
141
+ # URL already exists
142
+ success = False
143
+ conn.close()
144
+ return success
145
+
146
+ def update_url_in_sqlite(old_url, new_url):
147
+ """Update a URL in SQLite database"""
148
+ conn = sqlite3.connect(SQLITE_DB)
149
+ cursor = conn.cursor()
150
+ try:
151
+ cursor.execute("UPDATE urls SET url = ? WHERE url = ?", (new_url, old_url))
152
+ if cursor.rowcount > 0:
153
+ conn.commit()
154
+ success = True
155
+ else:
156
+ success = False
157
+ except sqlite3.IntegrityError:
158
+ # New URL already exists
159
+ success = False
160
+ conn.close()
161
+ return success
162
+
163
+ def delete_url_from_sqlite(url):
164
+ """Delete a URL from SQLite database"""
165
+ conn = sqlite3.connect(SQLITE_DB)
166
+ cursor = conn.cursor()
167
+ cursor.execute("DELETE FROM urls WHERE url = ?", (url,))
168
+ if cursor.rowcount > 0:
169
+ conn.commit()
170
+ success = True
171
+ else:
172
+ success = False
173
+ conn.close()
174
+ return success
175
+
176
+ def load_db():
177
+ """Primary function to load URLs - prioritizes SQLite DB but falls back to JSON"""
178
+ urls = load_db_sqlite()
179
+ if not urls:
180
+ # If SQLite DB is empty, try loading from JSON
181
+ urls = load_json()
182
+ # If we found URLs in JSON, migrate them to SQLite
183
+ for url in urls:
184
+ add_url_to_sqlite(url)
185
+ return urls
186
+
187
+ def save_db(lst):
188
+ """Save URLs to both SQLite and JSON"""
189
+ # Get existing URLs from SQLite for comparison
190
+ existing_urls = load_db_sqlite()
191
+
192
+ # Clear all URLs from SQLite and add the new list
193
+ conn = sqlite3.connect(SQLITE_DB)
194
+ cursor = conn.cursor()
195
+ cursor.execute("DELETE FROM urls")
196
+ for url in lst:
197
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
198
+ conn.commit()
199
+ conn.close()
200
+
201
+ # Also save to JSON for backward compatibility
202
+ return save_json(lst)
203
+
204
  # ────────────────────────── 4. URL HELPERS ──────────────────────────
205
  def direct_url(hf_url):
206
  m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
 
256
 
257
  @app.route('/api/favorites')
258
  def api_favorites():
259
+ # Load URLs from SQLite database
260
  urls = load_db()
261
+
262
  page = int(request.args.get('page', 1))
263
  per_page = int(request.args.get('per_page', 9))
264
 
 
299
  if not url:
300
  return jsonify({"success": False, "message": "URL is required"})
301
 
302
+ # Check if URL already exists in database
303
+ if not add_url_to_sqlite(url):
304
  return jsonify({"success": False, "message": "URL already exists"})
305
 
306
+ # Also update JSON file for backward compatibility
307
+ data = load_json()
308
+ if url not in data:
309
+ data.insert(0, url)
310
+ save_json(data)
311
+
312
  return jsonify({"success": True, "message": "URL added successfully"})
313
 
314
  @app.route('/api/url/update', methods=['POST'])
 
319
  if not new:
320
  return jsonify({"success": False, "message": "New URL is required"})
321
 
322
+ # Update in SQLite DB
323
+ if not update_url_in_sqlite(old, new):
324
+ return jsonify({"success": False, "message": "URL not found or new URL already exists"})
325
+
326
+ # Also update JSON file for backward compatibility
327
+ data = load_json()
328
  try:
329
  idx = data.index(old)
330
  data[idx] = new
331
+ save_json(data)
 
332
  except ValueError:
333
+ # If URL not in JSON, add it
334
+ data.append(new)
335
+ save_json(data)
336
+
337
+ return jsonify({"success": True, "message": "URL updated successfully"})
338
 
339
  @app.route('/api/url/delete', methods=['POST'])
340
  def delete_url():
341
  url = request.form.get('url', '')
342
+
343
+ # Delete from SQLite DB
344
+ if not delete_url_from_sqlite(url):
345
+ return jsonify({"success": False, "message": "URL not found"})
346
+
347
+ # Also update JSON file for backward compatibility
348
+ data = load_json()
349
  try:
350
  data.remove(url)
351
+ save_json(data)
 
352
  except ValueError:
353
+ pass
354
+
355
+ return jsonify({"success": True, "message": "URL deleted successfully"})
356
 
357
  # ────────────────────────── 6. MAIN ROUTES ──────────────────────────
358
  @app.route('/')
 
408
  <body>
409
  <div class="tabs" id="tabs"></div>
410
  <div id="content"></div>
411
+
412
  <script>
413
  // Basic configuration
414
  const cats = {{cats|tojson}};
 
416
  const content = document.getElementById('content');
417
  let active = "";
418
  let currentPage = 1;
419
+
420
  // Simple utility functions
421
  function loadHTML(url, callback) {
422
  const xhr = new XMLHttpRequest();
 
428
  };
429
  xhr.send();
430
  }
431
+
432
  function makeRequest(url, method, data, callback) {
433
  const xhr = new XMLHttpRequest();
434
  xhr.open(method, url, true);
 
443
  xhr.send();
444
  }
445
  }
446
+
447
  function updateTabs() {
448
  Array.from(tabs.children).forEach(b => {
449
  b.classList.toggle('active', b.dataset.c === active);
450
  });
451
  }
452
+
453
  // Tab handlers
454
  function loadCategory(cat) {
455
  if(cat === active) return;
 
464
  data.forEach(item => {
465
  html += `
466
  <div class="card">
467
+ <div class="card-label label-live">LIVE</div>
468
  <div class="frame">
469
  <iframe src="${item.iframe}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
470
  </div>
 
479
  content.innerHTML = html;
480
  });
481
  }
482
+
483
  function loadFavorites(page) {
484
  if(active === 'Favorites' && currentPage === page) return;
485
  active = 'Favorites';
 
537
  content.innerHTML = html;
538
  });
539
  }
540
+
541
  function loadManage() {
542
  if(active === 'Manage') return;
543
  active = 'Manage';
 
560
 
561
  loadUrlList();
562
  }
563
+
564
  // URL management functions
565
  function loadUrlList() {
566
  makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
 
573
 
574
  let html = '';
575
  data.items.forEach(item => {
576
+ // Escape the URL to prevent JavaScript injection when used in onclick handlers
577
+ const escapedUrl = item.url.replace(/'/g, "\\'");
578
+
579
  html += `
580
  <div class="url-item">
581
  <div>${item.url}</div>
582
  <div class="url-controls">
583
+ <button class="btn" onclick="editUrl('${escapedUrl}')">Edit</button>
584
+ <button class="btn btn-danger" onclick="deleteUrl('${escapedUrl}')">Delete</button>
585
  </div>
586
  </div>
587
  `;
 
590
  urlList.innerHTML = html;
591
  });
592
  }
593
+
594
  function addUrl() {
595
  const url = document.getElementById('new-url').value.trim();
596
 
 
607
  if(data.success) {
608
  document.getElementById('new-url').value = '';
609
  loadUrlList();
610
+ // If currently in Favorites tab, reload to see changes immediately
611
+ if(active === 'Favorites') {
612
+ loadFavorites(currentPage);
613
+ }
614
  }
615
  });
616
  }
617
+
618
  function editUrl(url) {
619
+ // Decode URL if it was previously escaped
620
+ const decodedUrl = url.replace(/\\'/g, "'");
621
+ const newUrl = prompt('Edit URL:', decodedUrl);
622
 
623
+ if(!newUrl || newUrl === decodedUrl) return;
624
 
625
  const formData = new FormData();
626
+ formData.append('old', decodedUrl);
627
  formData.append('new', newUrl);
628
 
629
  makeRequest('/api/url/update', 'POST', formData, function(data) {
630
  if(data.success) {
631
  loadUrlList();
632
+ // If currently in Favorites tab, reload to see changes immediately
633
+ if(active === 'Favorites') {
634
+ loadFavorites(currentPage);
635
+ }
636
  } else {
637
  alert(data.message);
638
  }
639
  });
640
  }
641
+
642
  function deleteUrl(url) {
643
+ // Decode URL if it was previously escaped
644
+ const decodedUrl = url.replace(/\\'/g, "'");
645
  if(!confirm('Are you sure you want to delete this URL?')) return;
646
 
647
  const formData = new FormData();
648
+ formData.append('url', decodedUrl);
649
 
650
  makeRequest('/api/url/delete', 'POST', formData, function(data) {
651
  if(data.success) {
652
  loadUrlList();
653
+ // If currently in Favorites tab, reload to see changes immediately
654
+ if(active === 'Favorites') {
655
+ loadFavorites(currentPage);
656
+ }
657
  } else {
658
  alert(data.message);
659
  }
660
  });
661
  }
662
+
663
  function showStatus(id, message, success) {
664
  const status = document.getElementById(id);
665
  status.textContent = message;
 
668
  status.className = 'status';
669
  }, 3000);
670
  }
671
+
672
  // Create tabs
673
  // Favorites tab first
674
  const favTab = document.createElement('button');
 
677
  favTab.dataset.c = 'Favorites';
678
  favTab.onclick = function() { loadFavorites(1); };
679
  tabs.appendChild(favTab);
680
+
681
  // Category tabs
682
  cats.forEach(c => {
683
  const b = document.createElement('button');
 
687
  b.onclick = function() { loadCategory(c); };
688
  tabs.appendChild(b);
689
  });
690
+
691
  // Manage tab last
692
  const manageTab = document.createElement('button');
693
  manageTab.className = 'tab manage';
 
695
  manageTab.dataset.c = 'Manage';
696
  manageTab.onclick = function() { loadManage(); };
697
  tabs.appendChild(manageTab);
698
+
699
  // Start with Favorites tab
700
  loadFavorites(1);
701
  </script>
 
708
  # Initialize database on startup
709
  init_db()
710
 
711
+ # Create a backup of the database at regular intervals
712
+ @app.before_first_request
713
+ def setup_db_backup():
714
+ # Make sure we have the latest data in both JSON and SQLite
715
+ urls = load_db()
716
+ save_json(urls)
717
+
718
  if __name__ == '__main__':
719
  app.run(host='0.0.0.0', port=7860)