Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -61,7 +61,7 @@ CATEGORIES = {
|
|
61 |
],
|
62 |
}
|
63 |
|
64 |
-
# โโโโโโโโโโโโโโโ 2. URL HELPERS
|
65 |
def direct_url(hf_url: str) -> str:
|
66 |
m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
|
67 |
if not m:
|
@@ -74,31 +74,27 @@ def direct_url(hf_url: str) -> str:
|
|
74 |
def screenshot_url(hf_url: str) -> str:
|
75 |
return f"https://image.thum.io/get/fullpage/{direct_url(hf_url)}"
|
76 |
|
77 |
-
#
|
78 |
@app.route('/api/category')
|
79 |
def api_category():
|
80 |
cat = request.args.get('name', '')
|
81 |
urls = CATEGORIES.get(cat, [])
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
data.append({
|
87 |
-
"title": name,
|
88 |
-
"owner": owner,
|
89 |
-
"name": name,
|
90 |
"iframe": direct_url(url),
|
91 |
"shot": screenshot_url(url),
|
92 |
"hf": url
|
93 |
-
}
|
94 |
-
|
95 |
|
96 |
-
#
|
97 |
@app.route('/')
|
98 |
def home():
|
99 |
return render_template('index.html', cats=list(CATEGORIES.keys()))
|
100 |
|
101 |
-
#
|
102 |
os.makedirs('templates', exist_ok=True)
|
103 |
with open('templates/index.html', 'w', encoding='utf-8') as fp:
|
104 |
fp.write(r'''<!DOCTYPE html>
|
@@ -110,70 +106,83 @@ body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb}
|
|
110 |
.tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px}
|
111 |
.tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer}
|
112 |
.tab.active{background:#a78bfa;color:#1a202c}
|
113 |
-
.grid{display:grid;grid-template-columns:repeat(
|
|
|
114 |
.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}
|
115 |
.frame{flex:1;position:relative;overflow:hidden}
|
116 |
.frame iframe,.frame img{position:absolute;width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left;border:0}
|
117 |
.err{display:none;align-items:center;justify-content:center;width:100%;height:100%;background:#fafafa;text-align:center;padding:10px;color:#666;font-size:.9rem}
|
118 |
.foot{height:44px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee}
|
119 |
.foot a{font-size:.82rem;font-weight:700;color:#4a6dd8;text-decoration:none}
|
120 |
-
</style
|
|
|
121 |
<body>
|
122 |
<div class="tabs" id="tabs"></div>
|
123 |
<div class="grid" id="grid"></div>
|
124 |
|
125 |
<script>
|
126 |
-
|
|
|
|
|
|
|
|
|
|
|
127 |
function createCard(sp){
|
128 |
-
const
|
129 |
-
|
|
|
130 |
const ifr=document.createElement('iframe');
|
131 |
-
ifr.src=sp.iframe; ifr.loading='lazy';
|
|
|
|
|
132 |
const err=document.createElement('div');err.className='err';
|
133 |
err.innerHTML=`Preview blocked.<br><a href="${sp.hf}" target="_blank">Open on HF โ</a>`;
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
|
|
145 |
}
|
|
|
146 |
const foot=document.createElement('div');foot.className='foot';
|
147 |
foot.innerHTML=`<a href="${sp.iframe}" target="_blank">${sp.title}</a>`;
|
148 |
-
|
149 |
-
|
|
|
|
|
150 |
}
|
151 |
-
|
152 |
-
|
153 |
-
const tabs=document.getElementById('tabs');
|
154 |
-
const grid=document.getElementById('grid');
|
155 |
-
let active="";
|
156 |
function load(cat){
|
157 |
-
if(cat===active)return;
|
|
|
158 |
[...tabs.children].forEach(b=>b.classList.toggle('active',b.dataset.c===cat));
|
159 |
grid.innerHTML='<p style="grid-column:1/-1;text-align:center;padding:40px">Loadingโฆ</p>';
|
160 |
fetch('/api/category?name='+encodeURIComponent(cat))
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
}
|
166 |
-
|
|
|
167 |
cats.forEach((c,i)=>{
|
168 |
const b=document.createElement('button');
|
169 |
-
b.className='tab';b.textContent=c;b.dataset.c=c;
|
170 |
-
b.onclick=()=>load(c);
|
171 |
-
|
|
|
172 |
});
|
173 |
</script>
|
174 |
</body></html>''')
|
175 |
|
176 |
-
#
|
177 |
if __name__ == '__main__':
|
178 |
-
# hugggingface spaces use 7860 by convention
|
179 |
app.run(host='0.0.0.0', port=7860)
|
|
|
61 |
],
|
62 |
}
|
63 |
|
64 |
+
# โโโโโโโโโโโโโโโ 2. URL HELPERS โโโโโโโโโโโโโโโโ
|
65 |
def direct_url(hf_url: str) -> str:
|
66 |
m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
|
67 |
if not m:
|
|
|
74 |
def screenshot_url(hf_url: str) -> str:
|
75 |
return f"https://image.thum.io/get/fullpage/{direct_url(hf_url)}"
|
76 |
|
77 |
+
# โโโโโโโโโโโโโโโ 3. API FOR TABS โโโโโโโโโโโโโโโ
|
78 |
@app.route('/api/category')
|
79 |
def api_category():
|
80 |
cat = request.args.get('name', '')
|
81 |
urls = CATEGORIES.get(cat, [])
|
82 |
+
return jsonify([
|
83 |
+
{
|
84 |
+
"title": url.split('/')[-1],
|
85 |
+
"owner": url.split('/')[-2] if '/spaces/' in url else '',
|
|
|
|
|
|
|
|
|
86 |
"iframe": direct_url(url),
|
87 |
"shot": screenshot_url(url),
|
88 |
"hf": url
|
89 |
+
} for url in urls
|
90 |
+
])
|
91 |
|
92 |
+
# โโโโโโโโโโโโโโโ 4. ROUTES โโโโโโโโโโโโโโโ
|
93 |
@app.route('/')
|
94 |
def home():
|
95 |
return render_template('index.html', cats=list(CATEGORIES.keys()))
|
96 |
|
97 |
+
# โโโโโโโโโโโโโโโ 5. CREATE TEMPLATE โโโโโโโโโโโโโโโ
|
98 |
os.makedirs('templates', exist_ok=True)
|
99 |
with open('templates/index.html', 'w', encoding='utf-8') as fp:
|
100 |
fp.write(r'''<!DOCTYPE html>
|
|
|
106 |
.tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px}
|
107 |
.tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer}
|
108 |
.tab.active{background:#a78bfa;color:#1a202c}
|
109 |
+
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;padding:0 16px 60px}
|
110 |
+
@media(max-width:800px){.grid{grid-template-columns:1fr}}
|
111 |
.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}
|
112 |
.frame{flex:1;position:relative;overflow:hidden}
|
113 |
.frame iframe,.frame img{position:absolute;width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left;border:0}
|
114 |
.err{display:none;align-items:center;justify-content:center;width:100%;height:100%;background:#fafafa;text-align:center;padding:10px;color:#666;font-size:.9rem}
|
115 |
.foot{height:44px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee}
|
116 |
.foot a{font-size:.82rem;font-weight:700;color:#4a6dd8;text-decoration:none}
|
117 |
+
</style>
|
118 |
+
</head>
|
119 |
<body>
|
120 |
<div class="tabs" id="tabs"></div>
|
121 |
<div class="grid" id="grid"></div>
|
122 |
|
123 |
<script>
|
124 |
+
const cats={{cats|tojson}};
|
125 |
+
const tabs=document.getElementById('tabs');
|
126 |
+
const grid=document.getElementById('grid');
|
127 |
+
let active="";
|
128 |
+
|
129 |
+
/* ---------- card builder (dynamic + fallback) ---------- */
|
130 |
function createCard(sp){
|
131 |
+
const card=document.createElement('div');card.className='card';
|
132 |
+
|
133 |
+
const frameBox=document.createElement('div');frameBox.className='frame';
|
134 |
const ifr=document.createElement('iframe');
|
135 |
+
ifr.src=sp.iframe; ifr.loading='lazy';
|
136 |
+
frameBox.appendChild(ifr);
|
137 |
+
|
138 |
const err=document.createElement('div');err.className='err';
|
139 |
err.innerHTML=`Preview blocked.<br><a href="${sp.hf}" target="_blank">Open on HF โ</a>`;
|
140 |
+
frameBox.appendChild(err);
|
141 |
+
|
142 |
+
let loaded=false;
|
143 |
+
ifr.onload = ()=>{ loaded=true; }; // success
|
144 |
+
ifr.onerror= fallback; // immediate fail
|
145 |
+
setTimeout(()=>{ if(!loaded) fallback(); },10000); // watchdog
|
146 |
+
|
147 |
+
function fallback(){
|
148 |
+
ifr.remove();
|
149 |
+
const img=new Image(); img.src=sp.shot; img.loading='lazy';
|
150 |
+
img.onerror=()=>{ err.style.display='flex'; };
|
151 |
+
frameBox.prepend(img);
|
152 |
}
|
153 |
+
|
154 |
const foot=document.createElement('div');foot.className='foot';
|
155 |
foot.innerHTML=`<a href="${sp.iframe}" target="_blank">${sp.title}</a>`;
|
156 |
+
|
157 |
+
card.appendChild(frameBox);
|
158 |
+
card.appendChild(foot);
|
159 |
+
return card;
|
160 |
}
|
161 |
+
|
162 |
+
/* ---------- load tab ---------- */
|
|
|
|
|
|
|
163 |
function load(cat){
|
164 |
+
if(cat===active) return;
|
165 |
+
active=cat;
|
166 |
[...tabs.children].forEach(b=>b.classList.toggle('active',b.dataset.c===cat));
|
167 |
grid.innerHTML='<p style="grid-column:1/-1;text-align:center;padding:40px">Loadingโฆ</p>';
|
168 |
fetch('/api/category?name='+encodeURIComponent(cat))
|
169 |
+
.then(r=>r.json()).then(arr=>{
|
170 |
+
grid.innerHTML='';
|
171 |
+
arr.forEach(sp=>grid.appendChild(createCard(sp)));
|
172 |
+
});
|
173 |
}
|
174 |
+
|
175 |
+
/* ---------- init tabs ---------- */
|
176 |
cats.forEach((c,i)=>{
|
177 |
const b=document.createElement('button');
|
178 |
+
b.className='tab'; b.textContent=c; b.dataset.c=c;
|
179 |
+
b.onclick=()=>load(c);
|
180 |
+
tabs.appendChild(b);
|
181 |
+
if(i===0) load(c);
|
182 |
});
|
183 |
</script>
|
184 |
</body></html>''')
|
185 |
|
186 |
+
# โโโโโโโโโโโโโโโ 6. RUN โโโโโโโโโโโโโโโ
|
187 |
if __name__ == '__main__':
|
|
|
188 |
app.run(host='0.0.0.0', port=7860)
|