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