Spaces:
Sleeping
Sleeping
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)
|