Spaces:
Running
Running
import os | |
import json | |
import re | |
import gradio as gr | |
# โโโโโโโโโโโโโโโโโโโโโ 1. ๊ธฐ๋ณธ ์ค์ โโโโโโโโโโโโโโโโโโโโโ | |
BEST_FILE, PER_PAGE = "best_games.json", 9 # โถ ํ ํ์ด์ง์ 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(): | |
try: | |
raw = json.load(open(BEST_FILE)) | |
return [u if isinstance(u, str) else u.get("url") for u in raw] if isinstance(raw, list) else [] | |
except Exception as e: | |
print("BEST ๋ก๋ ์ค๋ฅ:", e) | |
return [] | |
def _save_best(lst): | |
try: | |
json.dump(lst, open(BEST_FILE, "w"), ensure_ascii=False, indent=2) | |
return True | |
except Exception as e: | |
print("BEST ์ ์ฅ ์ค๋ฅ:", e) | |
return False | |
# *.hf.space โ Hub URL(์ ํญ์ฉ) ๋ณํ | |
def to_hub_space_url(url: str) -> str: | |
m = re.match(r"https?://([^-]+)-([^.]+)\.hf\.space(/.*)?", url) | |
if m: | |
owner, space, _ = m.groups() | |
return f"https://huggingface.co/spaces/{owner}/{space}" | |
return url | |
def add_url_to_best(url: str): | |
data = _load_best() | |
if url in data: | |
return False | |
data.insert(0, url) | |
return _save_best(data) | |
# โโโโโโโโโโโโโโโโโโโโโ 3. ์ ํธ โโโโโโโโโโโโโโโโโโโโโโโโโโ | |
def page(lst, pg): | |
s, e = (pg - 1) * PER_PAGE, (pg - 1) * PER_PAGE + PER_PAGE | |
total = (len(lst) + PER_PAGE - 1) // PER_PAGE | |
return lst[s:e], total | |
def process_url_for_iframe(url): | |
"""iframe์ฉ ์ฃผ์ ๋ณํ""" | |
if "huggingface.co/spaces" in url: | |
owner, name = url.rstrip("/").split("/spaces/")[1].split("/")[:2] | |
return f"https://huggingface.co/spaces/{owner}/{name}/embed", "huggingface", [] | |
m = re.match(r"https?://([^/]+)\.hf\.space(/.*)?", url) | |
if m: | |
sub, rest = m.groups() | |
static_url = f"https://{sub}.static.hf.space{rest or ''}" | |
return static_url, "hfspace", [url] | |
return url, "", [] | |
# โโโโโโโโโโโโโโโโโโโโโ 4. HTML ๊ทธ๋ฆฌ๋ โโโโโโโโโโโโโโโโโโโ | |
def html(cards, pg, total): | |
if not cards: | |
return "<div style='text-align:center;padding:70px;color:#555;'>ํ์ํ ๋ฐฐํฌ๊ฐ ์์ต๋๋ค.</div>" | |
css = r""" | |
<style> | |
/* ํ์คํ ๋ฐฐ๊ฒฝ */ | |
body{ | |
margin:0;padding:0;font-family:Poppins,sans-serif; | |
background:linear-gradient(135deg,#fdf4ff 0%,#f6fbff 50%,#fffaf4 100%); | |
background-attachment:fixed; | |
overflow-x:hidden;overflow-y:auto; | |
} | |
.container{width:100%;padding:10px 10px var(--bottom-gap,70px);box-sizing:border-box;} | |
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;width:100%;} | |
.card{ | |
background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 4px 10px rgba(0,0,0,0.08); | |
height:420px;display:flex;flex-direction:column;position:relative; | |
} | |
.frame{flex:1;position:relative;overflow:hidden;} | |
.frame iframe{ | |
position:absolute;top:0;left:0; | |
width:166.667%;height:166.667%; | |
transform:scale(0.6);transform-origin:top left;border:0; | |
} | |
.frame.huggingface iframe{width:100%!important;height:100%!important;transform:none!important;border:none!important;} | |
.frame.hfspace iframe{ | |
width:200%;height:200%; | |
transform:scale(0.5);transform-origin:top left;border:0; | |
} | |
.foot{height:34px;display:flex;align-items:center;justify-content:center;background:#fafafa;border-top:1px solid #eee;} | |
.foot a{font-size:0.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;} | |
.foot a:hover{text-decoration:underline;} | |
@media(min-width:1200px){.card{height:560px;}} | |
@media(max-width:767px){ | |
.grid{grid-template-columns:1fr;} | |
.card{height:480px;} | |
} | |
</style>""" | |
# ๋ฒํผ๊ณผ ํค๋ ๋์ด๋ฅผ ๊ณ ๋ คํด ์คํฌ๋กค ์์ญ ๋์ ์ผ๋ก ๊ณ์ฐ | |
js = r""" | |
<script> | |
function adjustGap(){ | |
const header = document.querySelector('.app-header'); | |
const buttons = document.querySelector('.button-row'); | |
const gap = (buttons?.offsetHeight || 60) + 10; // 10px ์ฌ์ | |
document.documentElement.style.setProperty('--bottom-gap', gap + 'px'); | |
const content = document.getElementById('content-area'); | |
const h = (header?.offsetHeight || 0) + (buttons?.offsetHeight || 60); | |
content.style.height = `calc(100vh - ${h}px)`; | |
} | |
window.addEventListener('load',adjustGap); | |
window.addEventListener('resize',adjustGap); | |
</script> | |
""" | |
h = css + js + '<div class="container"><div class="grid">' | |
for idx, url in enumerate(cards): | |
iframe_url, extra_cls, alt_urls = process_url_for_iframe(url) | |
frame_class = f"frame {extra_cls}".strip() | |
iframe_id = f"iframe-{idx}-{hash(url)%10000}" | |
alt_attr = f'data-alternate-urls="{",".join(alt_urls)}"' if alt_urls else "" | |
safe_url = to_hub_space_url(url) | |
h += f""" | |
<div class="card"> | |
<div class="{frame_class}"> | |
<iframe id="{iframe_id}" src="{iframe_url}" loading="lazy" | |
sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-downloads" | |
data-original-url="{url}" {alt_attr}></iframe> | |
</div> | |
<div class="foot"> | |
<a href="{safe_url}" target="_blank" rel="noopener noreferrer">โ Open in Full Screen (New Tab)</a> | |
</div> | |
</div>""" | |
h += "</div></div>" | |
h += f'<div class="page-info">Page {pg} / {total}</div>' | |
return h | |
# โโโโโโโโโโโโโโโโโโโโโ 5. Gradio UI โโโโโโโโโโโโโโโโโโโโโ | |
def build(): | |
_init_best() | |
header = """ | |
<style> | |
.app-header{position:sticky;top:0;text-align:center;background:#fff;padding:16px 0 8px;border-bottom:1px solid #eee;z-index:1100;} | |
.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> | |
<p style="margin:4px 0;font-size:11px;"> | |
Only high-quality games automatically generated with <b>Vibe Game Craft</b> are showcased here.<br> | |
Every game includes its full source code, and anyone can freely copy the <code>index.html</code> | |
file from each URL and modify it as desired. All content is released under the <b>Apache 2.0</b> license. | |
</p> | |
<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=800080&labelColor=ffa500&logo=huggingface&logoColor=ffff00&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=800080&labelColor=ffa500&logo=huggingface&logoColor=ffff00&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=0000ff&labelColor=800080&logo=discord&logoColor=white&style=for-the-badge"> | |
</a> | |
</div> | |
</div>""" | |
global_css = """ | |
footer{display:none !important;} | |
.button-row{ | |
position:fixed!important;bottom:0!important;left:0!important;right:0!important; | |
height:60px;background:#f0f0f0;padding:10px;text-align:center; | |
box-shadow:0 -2px 10px rgba(0,0,0,.05);z-index:10000; | |
} | |
.button-row button{margin:0 10px;padding:10px 20px;font-size:16px;font-weight:bold;border-radius:50px;} | |
#content-area{overflow-y:auto;} | |
""" | |
with gr.Blocks(title="Vibe Game Gallery", css=global_css) as demo: | |
gr.HTML(header) | |
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 render(p=1): | |
data, tot = page(_load_best(), p) | |
return html(data, p, tot), p | |
b_prev.click(lambda p: render(max(1, p-1)), inputs=bp, outputs=[out, bp]) | |
b_next.click(lambda p: render(p+1), inputs=bp, outputs=[out, bp]) | |
demo.load(render, outputs=[out, bp]) | |
return demo | |
app = build() | |
if __name__ == "__main__": | |
app.launch() |