Spaces:
Running
Running
import os, json, time, datetime, requests, 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 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): | |
# Hugging Face Spaces embed ์ฐ์ | |
if "huggingface.co/spaces" in url: | |
owner, name = url.rstrip("/").split("/spaces/")[1].split("/")[:2] | |
return f"https://huggingface.co/spaces/{owner}/{name}/embed", True, [] | |
return url, False, [] | |
# โโโโโโโโโโโโโโโโโโโโโ 4. ์นด๋ ๊ทธ๋ฆฌ๋ HTML โโโโโโโโโโโโโโโ | |
# โโโโโโโโโโโโโโโโโโโโโ 6. HTML ๊ทธ๋ฆฌ๋ โโโโโโโโโโโโโโโโโโโ | |
# โโโโโโโโโโโโโโโโโโโโโ 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:#f0f0f0;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; | |
} | |
.frame.huggingface iframe{width:100%!important;height:100%!important;transform:none!important;border:none!important;} | |
/* ํ๋จ ๋ฐ๋ก๊ฐ๊ธฐ ๋ฐ */ | |
.foot{ | |
height:34px;flex-shrink:0;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, is_huggingface, alt_urls = process_url_for_iframe(url) | |
frame_class = "frame huggingface" if is_huggingface else "frame" | |
iframe_id = f"iframe-{idx}-{hash(url)%10000}" | |
alt_attr = f'data-alternate-urls="{",".join(alt_urls)}"' if alt_urls else "" | |
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="{url}" target="_blank">โ ์ ํญ์ผ๋ก ์ด๊ธฐ</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() | |