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)) | |
# URL ๋ฆฌ์คํธ๋ง ๋ฐํ | |
if isinstance(raw, list): | |
return [u if isinstance(u, str) else u.get("url") for u in raw] | |
return [] | |
except Exception as e: | |
print("BEST ๋ก๋ ์ค๋ฅ:", e) | |
return [] | |
def _save_best(lst): # URLโ๋ฆฌ์คํธ ์ ์ฅ | |
try: | |
json.dump(lst, open(BEST_FILE, "w"), ensure_ascii=False, indent=2) | |
return True | |
except Exception as e: | |
print("BEST ์ ์ฅ ์ค๋ฅ:", e) | |
return False | |
def to_hub_space_url(url: str) -> str: | |
""" | |
*.hf.space URL์ https://huggingface.co/spaces/<owner>/<space> ํ์์ผ๋ก ๋ณํ | |
๋ค๋ฅธ URL์ ๊ทธ๋๋ก ๋ฐํ | |
""" | |
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_url, extra_class, alternate_urls) | |
extra_class : '' | 'huggingface' | 'hfspace' | |
""" | |
# Hugging Face Spaces embed (Gradio/Streamlit) | |
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", [] | |
# *.hf.space (์ ์ /static Space ํฌํจ) | |
m = re.match(r"https?://([^/]+)\.hf\.space(/.*)?", url) | |
if m: | |
sub, rest = m.groups() | |
static_url = f"https://{sub}.static.hf.space{rest or ''}" | |
# alt_urls ์ ์๋ณธ ์ ์ฅ(์คํจ ์ ์ฌ์๋ ๊ฐ๋ฅ) | |
return static_url, "hfspace", [url] | |
return url, "", [] | |
# โโโโโโโโโโโโโโโโโโโโโ 6. 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 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; | |
} | |
/* Gradio/Streamlit Embed์ฉ */ | |
.frame.huggingface iframe{ | |
width:100%!important;height:100%!important; | |
transform:none!important;border:none!important; | |
} | |
/* hf.space ์ ์ฒด ํ์ด์ง์ฉ - ํ ๋ฒ ๋ ์ถ์ */ | |
.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 = """ | |
<script> | |
/* ํ๊น ํ์ด์ค iframe ๋ก๋ฉ ์ค๋ฅ ์ฒ๋ฆฌ(์๋ต - ๊ธฐ์กด ์คํฌ๋ฆฝํธ ๊ทธ๋๋ก) */ | |
</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) # ์ ํญ ๋งํฌ์ฉ ๋ณํ 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> | |
<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;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=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() | |