openfree commited on
Commit
a727606
Β·
verified Β·
1 Parent(s): 1df9340

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +302 -94
app.py CHANGED
@@ -1,58 +1,121 @@
1
- import gradio as gr
2
- import re
3
 
4
- # ────────────────────────── CATEGORIES ──────────────────────────
 
 
 
 
 
 
 
 
 
 
5
  CATEGORIES = {
6
- "Popular": [
7
- "https://huggingface.co/spaces/fantos/Heatmap-Leaderboard-KOREA",
8
- "https://huggingface.co/spaces/ginigen/Today",
9
- "https://huggingface.co/spaces/ginigen/Nano-Banana-PRO",
10
- "https://huggingface.co/spaces/ginigen/SAJU",
11
- "https://huggingface.co/spaces/ginipick/SAJU-Couple",
12
- "https://huggingface.co/spaces/openfree/News-AI",
13
- "https://huggingface.co/spaces/openfree/Face-blurring",
14
- "https://huggingface.co/spaces/ginigen/Fashion-Fit360",
15
- "https://huggingface.co/spaces/VIDraft/DNA-Diffusion",
16
- "https://huggingface.co/spaces/ginigen/VEO3-Free",
17
- "https://huggingface.co/spaces/ginigen/text3d-r1",
18
- "https://huggingface.co/spaces/VIDraft/stable-diffusion-3.5-large-turboX",
19
  ],
20
  "BEST": [
21
- "https://huggingface.co/spaces/ginipick/AGI-Personal",
22
- "https://huggingface.co/spaces/ginigen/AI",
23
  "https://huggingface.co/spaces/ginigen/AGI-WebToon-KOREA",
24
- "https://huggingface.co/spaces/ginigen/Flux-Kontext-Style",
25
- "https://huggingface.co/spaces/ginigen/Seedance-Free",
26
- "https://huggingface.co/spaces/VIDraft/SOMA-AGI",
27
- "https://huggingface.co/spaces/aiqtech/Heatmap-Leaderboard",
28
- "https://huggingface.co/spaces/VIDraft/DNA-CASINO",
 
 
 
 
 
 
 
29
  ],
30
- "NEW": [
31
- "https://huggingface.co/spaces/ginigen/Family",
32
- "https://huggingface.co/spaces/openfree/OpenAI-gpt-oss",
33
- "https://huggingface.co/spaces/ginipick/Private-AI",
34
- "https://huggingface.co/spaces/VIDraft/ACE-Singer",
35
- "https://huggingface.co/spaces/ginipick/AI-BOOK",
36
- "https://huggingface.co/spaces/Heartsync/VEO3-RealTime",
37
  ],
38
- "Multimodal": [
39
- "https://huggingface.co/spaces/ginigen/Seedance-Free",
40
- "https://huggingface.co/spaces/Heartsync/VEO3-RealTime",
41
- "https://huggingface.co/spaces/ginigen/VEO3-Free",
42
- "https://huggingface.co/spaces/ginigen/Flux-VIDEO",
43
- "https://huggingface.co/spaces/VIDraft/ACE-Singer",
44
- "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  ],
46
- "Image": [
47
- "https://huggingface.co/spaces/openfree/Face-blurring",
48
- "https://huggingface.co/spaces/ginigen/Flux-Kontext-FaceLORA",
49
- "https://huggingface.co/spaces/ginigen/text3d-r1",
50
- "https://huggingface.co/spaces/ginipick/FLUXllama",
51
- "https://huggingface.co/spaces/ginigen/MagicFace-V3",
52
- "https://huggingface.co/spaces/VIDraft/stable-diffusion-3.5-large-turboX",
 
 
 
 
 
 
 
 
 
53
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  }
55
 
 
56
  def direct_url(hf_url):
57
  m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
58
  if not m:
@@ -62,59 +125,204 @@ def direct_url(hf_url):
62
  name = name.replace('.', '-').replace('_', '-').lower()
63
  return f"https://{owner}-{name}.hf.space"
64
 
65
- def create_gallery_html(category):
66
- urls = CATEGORIES.get(category, [])[:8] # Show first 8
 
 
 
 
 
 
67
 
68
- html = '<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:20px;padding:20px;max-width:1400px;margin:0 auto;">'
 
 
69
 
70
- for url in urls:
71
- title = url.split('/')[-1]
72
- iframe_url = direct_url(url)
73
-
74
- html += f'''
75
- <div style="background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);overflow:hidden;height:500px;display:flex;flex-direction:column;">
76
- <div style="flex:1;position:relative;overflow:hidden;background:#f5f5f5;">
77
- <div style="position:absolute;top:10px;left:10px;padding:4px 8px;border-radius:4px;background:linear-gradient(135deg,#00c6ff,#0072ff);color:white;font-size:11px;font-weight:bold;z-index:10;">LIVE</div>
78
- <iframe src="{iframe_url}" style="width:100%;height:100%;border:0;" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts"></iframe>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  </div>
80
- <div style="padding:12px;background:#fafafa;border-top:1px solid #eee;text-align:center;">
81
- <a href="{url}" target="_blank" style="font-size:14px;font-weight:600;color:#4a6dd8;text-decoration:none;">{title}</a>
82
  </div>
83
- </div>
84
- '''
 
 
85
 
86
- html += '</div>'
87
- return html
88
-
89
- # Create Gradio interface
90
- with gr.Blocks(
91
- title="🌟 BEST AI Playground",
92
- css="""
93
- @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap');
94
- body {font-family: 'Nunito', sans-serif !important;}
95
- .gradio-container {max-width: 100% !important;}
96
- """
97
- ) as demo:
98
-
99
- gr.HTML("""
100
- <div style="text-align:center;padding:30px 20px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;border-radius:10px;margin-bottom:20px;">
101
- <h1 style="margin:0 0 15px 0;font-size:2.5em;font-weight:700;">🌟 BEST AI Playground</h1>
102
- <p style="margin:10px 0;font-size:1.1em;">Curated Collection of Amazing AI Spaces on Hugging Face</p>
103
- <div style="margin-top:20px;">
104
- <a href="https://huggingface.co/OpenFreeAI" target="_blank" style="display:inline-block;margin:5px;">
105
- <img src="https://img.shields.io/badge/Community-OpenFree_AI-purple?style=for-the-badge&logo=huggingface" alt="Community">
106
- </a>
107
- <a href="https://discord.gg/openfreeai" target="_blank" style="display:inline-block;margin:5px;">
108
- <img src="https://img.shields.io/badge/Discord-Join_Us-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord">
109
- </a>
110
- </div>
111
- </div>
112
- """)
113
-
114
- with gr.Tabs():
115
- for category_name, urls in CATEGORIES.items():
116
- with gr.Tab(f"✨ {category_name}"):
117
- gr.HTML(create_gallery_html(category_name))
118
 
119
- if __name__ == "__main__":
120
- demo.launch()
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import os, re, json
3
 
4
+ app = Flask(__name__)
5
+
6
+ # ────────────────────────── 1. CONFIGURATION ──────────────────────────
7
+ # Domains that commonly block iframes
8
+ BLOCKED_DOMAINS = [
9
+ "naver.com", "daum.net", "google.com",
10
+ "facebook.com", "instagram.com", "kakao.com",
11
+ "ycombinator.com"
12
+ ]
13
+
14
+ # ────────────────────────── 2. CURATED CATEGORIES ──────────────────────────
15
  CATEGORIES = {
16
+ "Popular": [
17
+ "https://huggingface.co/spaces/aiqtech/NSFW-Real",
18
+ "https://huggingface.co/spaces/Heartsync/Hentai-Adult",
19
+ "https://huggingface.co/spaces/Heartsync/PornHUB",
20
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-image",
21
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
22
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video2",
23
+ "https://huggingface.co/spaces/Heartsync/Novel-NSFW",
24
+ "https://huggingface.co/spaces/Heartsync/VEO3-RealTime",
25
+ "https://huggingface.co/spaces/ginigen/VEO3-Directors",
26
+ "https://huggingface.co/spaces/yoinked/da_nsfw_checker", #####
27
+ "https://huggingface.co/spaces/LearningnRunning/adult_image_detector", ###
 
28
  ],
29
  "BEST": [
 
 
30
  "https://huggingface.co/spaces/ginigen/AGI-WebToon-KOREA",
31
+ "https://huggingface.co/spaces/ginigen/webtoon-studio",
32
+ "https://huggingface.co/spaces/ginigen/Wan-2.2-Enhanced",
33
+ "https://huggingface.co/spaces/Heartsync/VEO3-RealTime",
34
+ "https://huggingface.co/spaces/ginigen/VEO3-Directors",
35
+ "https://huggingface.co/spaces/ginigen/VEO3-Free",
36
+ "https://huggingface.co/spaces/Heartsync/adult",
37
+ "https://huggingface.co/spaces/ginigen/Flux-VIDEO",
38
+ "https://huggingface.co/spaces/Heartsync/WAN2-1-fast-T2V-FusioniX",
39
+ "https://huggingface.co/spaces/openfree/DreamO-video",
40
+ "https://huggingface.co/spaces/Heartsync/NSFW-novels",
41
+ "https://huggingface.co/spaces/fantaxy/fantasy-novel",
42
+
43
  ],
44
+
45
+ "TEXT generate": [
46
+ "https://huggingface.co/spaces/openfree/OpenAI-gpt-oss",
47
+ "https://huggingface.co/spaces/Heartsync/Novel-NSFW",
48
+ "https://huggingface.co/spaces/fantaxy/fantasy-novel",
49
+ "https://huggingface.co/spaces/Heartsync/NSFW-novels",
 
50
  ],
51
+
52
+ "TEXT TO IMAGE": [
53
+ "https://huggingface.co/spaces/Heartsync/PornHUB",
54
+ "https://huggingface.co/spaces/Heartsync/Hentai-Adult",
55
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
56
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
57
+ "https://huggingface.co/spaces/Heartsync/adult",
58
+ "https://huggingface.co/spaces/Heartsync/NSFW-novels",
59
+ "https://huggingface.co/spaces/IbarakiDouji/WAI-NSFW-illustrious-SDXL", ###
60
+ "https://huggingface.co/spaces/armen425221356/UnfilteredAI-NSFW-gen-v2_self_parms", ####
61
+
62
+ ],
63
+ "IMAGE TO VIDEO": [
64
+ "https://huggingface.co/spaces/Heartsync/adult",
65
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video",
66
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video2",
67
+ "https://huggingface.co/spaces/Heartsync/WAN2-1-fast-T2V-FusioniX",
68
+ "https://huggingface.co/spaces/openfree/DreamO-video",
69
+ "https://huggingface.co/spaces/Heartsync/wan2-1-fast-security",
70
+ "https://huggingface.co/spaces/ginigen/Flux-VIDEO",
71
+ "https://huggingface.co/spaces/Heartsync/WAN-VIDEO-AUDIO",
72
+ ],
73
+
74
+ "IMAGE IN/OUT-PAINTING": [
75
+ "https://huggingface.co/spaces/ginigen/FLUX-Ghibli-LoRA2",
76
+ "https://huggingface.co/spaces/davecarrau/nsfw-face-swap", ###
77
+ "https://huggingface.co/spaces/VIDraft/ReSize-Image-Outpainting",
78
+ "https://huggingface.co/spaces/aiqcamp/REMOVAL-TEXT-IMAGE",
79
+ "https://huggingface.co/spaces/ginigen/MagicFace-V3",
80
+ "https://huggingface.co/spaces/openfree/ColorRevive",
81
+ "https://huggingface.co/spaces/ginigen/VisualCloze",
82
+ "https://huggingface.co/spaces/fantos/textcutobject",
83
+
84
  ],
85
+
86
+ "Extension": [
87
+ "https://huggingface.co/spaces/VIDraft/ACE-Singer",
88
+ "https://huggingface.co/spaces/VIDraft/Voice-Clone-Podcast",
89
+ "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
90
+ "https://huggingface.co/spaces/openfree/Multilingual-TTS",
91
+ "https://huggingface.co/spaces/fantaxy/Sound-AI-SFX",
92
+ "https://huggingface.co/spaces/ginigen/SFX-Sound-magic",
93
+ "https://huggingface.co/spaces/fantaxy/Remove-Video-Background",
94
+ "https://huggingface.co/spaces/VIDraft/stable-diffusion-3.5-large-turboX",
95
+
96
+ "https://huggingface.co/spaces/aiqtech/imaginpaint",
97
+ "https://huggingface.co/spaces/openfree/ultpixgen",
98
+ # "https://huggingface.co/spaces/ginipick/Change-Hair",
99
+ # "https://huggingface.co/spaces/ginigen/Every-Text",
100
+
101
  ],
102
+
103
+ "Utility": [
104
+ "https://huggingface.co/spaces/openfree/Best-AI",
105
+ "https://huggingface.co/spaces/openfree/Chart-GPT",
106
+ "https://huggingface.co/spaces/ginipick/AI-BOOK",
107
+ "https://huggingface.co/spaces/openfree/Live-Podcast",
108
+ "https://huggingface.co/spaces/openfree/AI-Podcast",
109
+ "https://huggingface.co/spaces/ginipick/FLUXllama",
110
+ "https://huggingface.co/spaces/VIDraft/Polaroid-Style",
111
+ "https://huggingface.co/spaces/ginigen/text3d-r1",
112
+ "https://huggingface.co/spaces/openfree/Naming",
113
+ "https://huggingface.co/spaces/ginigen/3D-LLAMA-V1",
114
+ "https://huggingface.co/spaces/fantaxy/flx-pulid",
115
+ ],
116
  }
117
 
118
+ # ────────────────────────── 3. URL HELPERS ──────────────────────────
119
  def direct_url(hf_url):
120
  m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
121
  if not m:
 
125
  name = name.replace('.', '-').replace('_', '-').lower()
126
  return f"https://{owner}-{name}.hf.space"
127
 
128
+ def screenshot_url(url):
129
+ return f"https://image.thum.io/get/fullpage/{url}"
130
+
131
+ def process_url_for_preview(url):
132
+ """Returns (preview_url, mode)"""
133
+ # Handle blocked domains first
134
+ if any(d for d in BLOCKED_DOMAINS if d in url):
135
+ return screenshot_url(url), "snapshot"
136
 
137
+ # Special case handling for problematic URLs
138
+ if "vibe-coding-tetris" in url or "World-of-Tank-GAME" in url or "Minesweeper-Game" in url:
139
+ return screenshot_url(url), "snapshot"
140
 
141
+ # General HF space handling
142
+ try:
143
+ if "huggingface.co/spaces" in url:
144
+ parts = url.rstrip("/").split("/")
145
+ if len(parts) >= 5:
146
+ owner = parts[-2]
147
+ name = parts[-1]
148
+ embed_url = f"https://huggingface.co/spaces/{owner}/{name}/embed"
149
+ return embed_url, "iframe"
150
+ except Exception:
151
+ return screenshot_url(url), "snapshot"
152
+
153
+ # Default handling
154
+ return url, "iframe"
155
+
156
+ # ────────────────────────── 4. API ROUTES ──────────────────────────
157
+ @app.route('/api/category')
158
+ def api_category():
159
+ cat = request.args.get('name', '')
160
+ urls = CATEGORIES.get(cat, [])
161
+
162
+ # Add pagination for categories
163
+ page = int(request.args.get('page', 1))
164
+ per_page = int(request.args.get('per_page', 4)) # 4 per page for 2x2 grid
165
+
166
+ total_pages = max(1, (len(urls) + per_page - 1) // per_page)
167
+ start = (page - 1) * per_page
168
+ end = min(start + per_page, len(urls))
169
+
170
+ urls_page = urls[start:end]
171
+
172
+ items = [
173
+ {
174
+ "title": url.split('/')[-1],
175
+ "owner": url.split('/')[-2] if '/spaces/' in url else '',
176
+ "iframe": direct_url(url),
177
+ "shot": screenshot_url(url),
178
+ "hf": url
179
+ } for url in urls_page
180
+ ]
181
+
182
+ return jsonify({
183
+ "items": items,
184
+ "page": page,
185
+ "total_pages": total_pages
186
+ })
187
+
188
+ # ────────────────────────── 5. MAIN ROUTES ──────────────────────────
189
+ @app.route('/')
190
+ def home():
191
+ os.makedirs('templates', exist_ok=True)
192
+
193
+ with open('templates/index.html', 'w', encoding='utf-8') as fp:
194
+ fp.write(r'''<!DOCTYPE html>
195
+ <html>
196
+ <head>
197
+ <meta charset="utf-8">
198
+ <meta name="viewport" content="width=device-width, initial-scale=1">
199
+ <title>Free NSFW Hub</title>
200
+ <style>
201
+ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;600&display=swap');
202
+ body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb;}
203
+ .tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px;}
204
+ .tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer;}
205
+ .tab.active{background:#a78bfa;color:#1a202c;}
206
+ /* Updated grid to show 2x2 layout */
207
+ .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:20px;padding:0 16px 60px;max-width:1200px;margin:0 auto;}
208
+ @media(max-width:800px){.grid{grid-template-columns:1fr;}}
209
+ /* Increased card height for larger display */
210
+ .card{background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);overflow:hidden;height:540px;display:flex;flex-direction:column;position:relative;}
211
+ .frame{flex:1;position:relative;overflow:hidden;}
212
+ .frame iframe{position:absolute;width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left;border:0;}
213
+ .frame img{width:100%;height:100%;object-fit:cover;}
214
+ .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);}
215
+ .label-live{background:linear-gradient(135deg, #00c6ff, #0072ff);color:white;}
216
+ .label-static{background:linear-gradient(135deg, #ff9a9e, #fad0c4);color:#333;}
217
+ .foot{height:44px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee;}
218
+ .foot a{font-size:.82rem;font-weight:700;color:#4a6dd8;text-decoration:none;}
219
+ .pagination{display:flex;justify-content:center;margin:20px 0;gap:10px;}
220
+ .pagination button{padding:5px 15px;border:none;border-radius:20px;background:#e2e8f0;cursor:pointer;}
221
+ .pagination button:disabled{opacity:0.5;cursor:not-allowed;}
222
+ </style>
223
+ </head>
224
+ <body>
225
+ <header style="text-align: center; padding: 20px; background: linear-gradient(135deg, #f6f8fb, #e2e8f0); border-bottom: 1px solid #ddd;">
226
+ <h1 style="margin-bottom: 10px;">πŸ”₯Free NSFW Hub</h1>
227
+ <p style="margin-bottom: 15px; color: #666; font-size: 14px;">
228
+ A curated collection of the most popular and polished NSFW Detection projects on Hugging Face Spaces,<br>
229
+ organized for easy visual exploration and discovery.
230
+ </p>
231
+ <p>
232
+ <a href="https://huggingface.co/spaces/Heartsync/FREE-NSFW-HUB" target="_blank"><img src="https://img.shields.io/static/v1?label=huggingface&message=FREE%20NSFW%20HUB&color=%230000ff&labelColor=%23800080&logo=huggingface&logoColor=%23ffa500&style=for-the-badge" alt="badge"></a>
233
+ </p>
234
+
235
+ </header>
236
+ <div class="tabs" id="tabs"></div>
237
+ <div id="content"></div>
238
+ <script>
239
+ // Basic configuration
240
+ const cats = {{cats|tojson}};
241
+ const tabs = document.getElementById('tabs');
242
+ const content = document.getElementById('content');
243
+ let active = "";
244
+ let currentPage = 1;
245
+ // Simple utility functions
246
+ function makeRequest(url, method, data, callback) {
247
+ const xhr = new XMLHttpRequest();
248
+ xhr.open(method, url, true);
249
+ xhr.onreadystatechange = function() {
250
+ if (xhr.readyState === 4 && xhr.status === 200) {
251
+ callback(JSON.parse(xhr.responseText));
252
+ }
253
+ };
254
+ if (method === 'POST') {
255
+ xhr.send(data);
256
+ } else {
257
+ xhr.send();
258
+ }
259
+ }
260
+ function updateTabs() {
261
+ Array.from(tabs.children).forEach(b => {
262
+ b.classList.toggle('active', b.dataset.c === active);
263
+ });
264
+ }
265
+ // Tab handler for categories
266
+ function loadCategory(cat, page) {
267
+ if(cat === active && currentPage === page) return;
268
+ active = cat;
269
+ currentPage = page || 1;
270
+ updateTabs();
271
+
272
+ content.innerHTML = '<p style="text-align:center;padding:40px">Loading…</p>';
273
+
274
+ makeRequest('/api/category?name=' + encodeURIComponent(cat) + '&page=' + currentPage + '&per_page=4', 'GET', null, function(data) {
275
+ let html = '<div class="grid">';
276
+
277
+ if(data.items.length === 0) {
278
+ html += '<p style="grid-column:1/-1;text-align:center;padding:40px">No items in this category.</p>';
279
+ } else {
280
+ data.items.forEach(item => {
281
+ html += `
282
+ <div class="card">
283
+ <div class="card-label label-live">LIVE</div>
284
+ <div class="frame">
285
+ <iframe src="${item.iframe}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
286
  </div>
287
+ <div class="foot">
288
+ <a href="${item.hf}" target="_blank">${item.title}</a>
289
  </div>
290
+ </div>
291
+ `;
292
+ });
293
+ }
294
 
295
+ html += '</div>';
296
+
297
+ // Add pagination
298
+ html += `
299
+ <div class="pagination">
300
+ <button ${currentPage <= 1 ? 'disabled' : ''} onclick="loadCategory('${cat}', ${currentPage-1})">Β« Previous</button>
301
+ <span>Page ${currentPage} of ${data.total_pages}</span>
302
+ <button ${currentPage >= data.total_pages ? 'disabled' : ''} onclick="loadCategory('${cat}', ${currentPage+1})">Next Β»</button>
303
+ </div>
304
+ `;
305
+
306
+ content.innerHTML = html;
307
+ });
308
+ }
309
+ // Create category tabs
310
+ cats.forEach(c => {
311
+ const b = document.createElement('button');
312
+ b.className = 'tab';
313
+ b.textContent = c;
314
+ b.dataset.c = c;
315
+ b.onclick = function() { loadCategory(c, 1); };
316
+ tabs.appendChild(b);
317
+ });
318
+ // Start with the first category (Productivity)
319
+ loadCategory(cats[0], 1);
320
+ </script>
321
+ </body>
322
+ </html>''')
323
+
324
+ # Return the rendered template
325
+ return render_template('index.html', cats=list(CATEGORIES.keys()))
 
326
 
327
+ if __name__ == '__main__':
328
+ app.run(host='0.0.0.0', port=7860)