Spaces:
Running
Running
import os, re, time, json, datetime, requests, gradio as gr | |
# โโโโโโโโโโโโโโโโโโโโโ 1. ๊ธฐ๋ณธ ์ค์ โโโโโโโโโโโโโโโโโโโโโ | |
BEST_FILE, PER_PAGE = "best_games.json", 9 | |
# โโโโโโโโโโโโโโโโโโโโโ 2. BEST ๋ฐ์ดํฐ โโโโโโโโโโโโโโโโโโโโ | |
def _init_best(): | |
if not os.path.exists(BEST_FILE): | |
json.dump([], open(BEST_FILE, "w"), ensure_ascii=False) | |
def _load_best(): | |
"""best_games.json โ ["https://foo.vercel.app", ...] ํํ๋ก ๋ก๋""" | |
try: | |
raw = json.load(open(BEST_FILE)) | |
if isinstance(raw, list): | |
urls = [] | |
for it in raw: | |
if isinstance(it, str): | |
urls.append(it) | |
elif isinstance(it, dict) and "url" in it: | |
urls.append(it["url"]) | |
return urls | |
return [] | |
except Exception as e: | |
print(f"BEST ๋ฐ์ดํฐ ๋ก๋ ์ค๋ฅ: {e}") | |
return [] | |
def _save_best(url_list: list[str]) -> bool: | |
try: | |
json.dump(url_list, open(BEST_FILE, "w"), ensure_ascii=False, indent=2) | |
return True | |
except Exception as e: | |
print(f"BEST ๋ฐ์ดํฐ ์ ์ฅ ์ค๋ฅ: {e}") | |
return False | |
# โโโโโโโโโโโโโโโโโโโโโ 3. URL ์ถ๊ฐ ๊ธฐ๋ฅ โโโโโโโโโโโโโโโโโโโโโ | |
def add_url_to_best(url: str) -> bool: | |
try: | |
data = _load_best() | |
if url in data: | |
print("์ด๋ฏธ ์กด์ฌ:", url) | |
return False | |
data.insert(0, url) | |
return _save_best(data) | |
except Exception as e: | |
print("URL ์ถ๊ฐ ์ค๋ฅ:", e) | |
return False | |
# โโโโโโโโโโโโโโโโโโโโโ 4. ํ์ด์ง๋ค์ด์ โโโโโโโโโโโโโโโโโโโ | |
def page(lst, pg): | |
s = (pg-1) * PER_PAGE | |
e = s + PER_PAGE | |
total = (len(lst) + PER_PAGE - 1) // PER_PAGE | |
return lst[s:e], total | |
# โโโโโโโโโโโโโโโโโโโโโ 5. URL โ iframe ๋ณํ โโโโโโโโโโโโโโโ | |
def process_url_for_iframe(url): | |
is_huggingface = False | |
embed_urls = [] | |
if "huggingface.co/spaces" in url: | |
is_huggingface = True | |
base_url = url.rstrip("/") | |
try: | |
path = base_url.split("/spaces/")[1] | |
owner, *rest = path.split("/") | |
if rest: | |
name = rest[0] | |
clean_owner = owner.lower() | |
clean_name = name.replace('.', '-').replace('_', '-').lower() | |
embed_urls.append(f"https://huggingface.co/spaces/{owner}/{name}/embed") | |
embed_urls.append(f"https://{clean_owner}-{clean_name}.hf.space") | |
else: | |
embed_urls.append(f"https://huggingface.co/spaces/{owner}/embed") | |
except Exception: | |
if not base_url.endswith("/embed"): | |
embed_urls.append(f"{base_url}/embed") | |
else: | |
embed_urls.append(base_url) | |
elif ".hf.space" in url: | |
is_huggingface = True | |
embed_urls.append(url) | |
else: | |
return url, False, [] | |
primary = embed_urls[0] if embed_urls else url | |
return primary, is_huggingface, embed_urls[1:] | |
# โโโโโโโโโโโโโโโโโโโโโ 6. HTML ๋ ๋ โโโโโโโโโโโโโโโโโโโโโโโ | |
def html(urls, pg, total): | |
if not urls: | |
return "<div style='text-align:center;padding:70px;color:#555;'>ํ์ํ ๋ฐฐํฌ๊ฐ ์์ต๋๋ค.</div>" | |
css = r""" | |
<style> | |
body{margin:0;font-family:Poppins,sans-serif;background:#f4f5f7;} | |
.container{padding:16px;} | |
.grid{ | |
display:grid; | |
grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); | |
gap:16px; | |
} | |
.card{ | |
background:#fff; | |
border-radius:10px; | |
overflow:hidden; | |
box-shadow:0 2px 8px rgba(0,0,0,.05); | |
transition:transform .2s; | |
} | |
.card:hover{transform:translateY(-4px);} | |
.frame{position:relative;width:100%;padding-top:56.25%;} | |
.frame iframe{position:absolute;inset:0;width:100%;height:100%;border:none;} | |
.frame.huggingface iframe{padding:0;margin:0;} | |
.page-info{text-align:center;color:#777;margin:12px 0;} | |
</style>""" | |
js = """ | |
<script> | |
function handleIframeError(id, alternates, origin){ | |
const f=document.getElementById(id); if(!f)return; | |
f.onerror=()=>tryNext(id, alternates, origin); | |
f.onload=()=>setTimeout(()=>{if(f.offsetWidth===0||f.offsetHeight===0)tryNext(id, alternates, origin);},4000); | |
} | |
function tryNext(id, alternates, origin){ | |
const f=document.getElementById(id); if(!f)return; | |
if(alternates.length){ | |
f.src=alternates.shift(); | |
handleIframeError(id, alternates, origin); | |
}else{ | |
f.parentNode.innerHTML='<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:14px;color:#999;">๋ก๋ ์คํจ โ</div>'; | |
} | |
} | |
window.addEventListener('load',()=>{ | |
document.querySelectorAll('.huggingface iframe').forEach(f=>{ | |
const id=f.id; | |
const alt=(f.getAttribute('data-alt')||'').split(',').filter(Boolean); | |
if(id&&alt.length)handleIframeError(id,alt,f.src); | |
}); | |
}); | |
</script>""" | |
body = '<div class="container"><div class="grid">' | |
for i, url in enumerate(urls): | |
iframe_url, is_hf, alt = process_url_for_iframe(url) | |
frame_cls = "frame huggingface" if is_hf else "frame" | |
iframe_id = f"f{i}" | |
alt_attr = f'data-alt="{",".join(alt)}"' if alt else "" | |
body += f""" | |
<div class="card"> | |
<div class="{frame_cls}"> | |
<iframe id="{iframe_id}" src="{iframe_url}" loading="lazy" {alt_attr}></iframe> | |
</div> | |
</div>""" | |
body += "</div></div>" | |
page_info = f'<div class="page-info">Page {pg} / {total}</div>' | |
return css + js + body + page_info | |
# โโโโโโโโโโโโโโโโโโโโโ 7. UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
def build(): | |
_init_best() | |
header_html = """ | |
<style> | |
.app-header{text-align:center;padding:16px 0 8px;margin:0;position:sticky;top:0;background:#fff;z-index:1100;border-bottom:1px solid #eee;} | |
.badge-row{display:inline-flex;gap:8px;margin:8px 0;} | |
</style> | |
<div class="app-header"> | |
<h1 style="margin:0;font-size:28px;">๐ฎ Vibe Game Gallery</h1> | |
<div class="badge-row"> | |
<a href="https://huggingface.co/spaces/openfree/Vibe-Game" target="_blank"><img src="https://img.shields.io/static/v1?label=huggingface&message=Vibe%20Game%20Craft&color=%23800080&labelColor=%23ffa500&logo=huggingface&logoColor=%23ffff00&style=for-the-badge"></a> | |
<a href="https://huggingface.co/spaces/openfree/Game-Gallery" target="_blank"><img src="https://img.shields.io/static/v1?label=huggingface&message=Game%20Gallery&color=%23800080&labelColor=%23ffa500&logo=huggingface&logoColor=%23ffff00&style=for-the-badge"></a> | |
<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"></a> | |
</div> | |
</div> | |
""" | |
css_global = """ | |
footer{display:none !important;} | |
.button-row{position:fixed;bottom:0;left:0;right:0;height:60px;background:#f0f0f0;padding:10px;text-align:center;box-shadow:0 -2px 10px rgba(0,0,0,.05);z-index:1000;} | |
.button-row button{margin:0 10px;padding:10px 20px;font-size:16px;font-weight:bold;border-radius:50px;} | |
#content-area{overflow-y:auto;height:calc(100vh - 60px - 120px);} | |
""" | |
with gr.Blocks(title="Vibe Game Gallery", css=css_global) as demo: | |
gr.HTML(header_html) | |
out = gr.HTML(elem_id="content-area") | |
with gr.Row(elem_classes="button-row"): | |
b_prev = gr.Button("โ ์ด์ ", size="lg") | |
b_next = gr.Button("๋ค์ โถ", size="lg") | |
bp = gr.State(1) | |
def show_best(p=1): | |
d, t = page(_load_best(), p) | |
return html(d, p, t), p | |
def prev(b): | |
b = max(1, b-1) | |
h, _ = show_best(b) | |
return h, b | |
def nxt(b): | |
maxp = (len(_load_best()) + PER_PAGE - 1) // PER_PAGE | |
b = min(maxp, b+1) | |
h, _ = show_best(b) | |
return h, b | |
b_prev.click(prev, inputs=[bp], outputs=[out, bp]) | |
b_next.click(nxt, inputs=[bp], outputs=[out, bp]) | |
demo.load(show_best, outputs=[out, bp]) | |
return demo | |
app = build() | |
if __name__ == "__main__": | |
app.launch() | |