Game-Gallery / app.py
openfree's picture
Update app.py
f3e82a2 verified
raw
history blame
8.75 kB
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()