Game-Gallery / app.py
openfree's picture
Create app.py
61ff302 verified
raw
history blame
8.97 kB
import os, re, time, json, datetime, requests, gradio as gr
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. ๊ธฐ๋ณธ ์„ค์ • โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
BEST_FILE, PER_PAGE = "best_games.json", 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():
"""best_games.json โ†’ ["https://foo.vercel.app", ...] ํ˜•ํƒœ๋กœ ๋กœ๋“œ"""
try:
raw = json.load(open(BEST_FILE))
if isinstance(raw, list):
urls = []
for it in raw:
if isinstance(it, str):
urls.append(it)
elif isinstance(it, dict) and "url" in it:
urls.append(it["url"])
return urls
return []
except Exception as e:
print(f"BEST ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜: {e}")
return []
def _save_best(url_list: list[str]) -> bool:
try:
json.dump(url_list, open(BEST_FILE, "w"), ensure_ascii=False, indent=2)
return True
except Exception as e:
print(f"BEST ๋ฐ์ดํ„ฐ ์ €์žฅ ์˜ค๋ฅ˜: {e}")
return False
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 3. URL ์ถ”๊ฐ€ ๊ธฐ๋Šฅ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def add_url_to_best(url: str) -> bool:
try:
data = _load_best()
if url in data:
print("์ด๋ฏธ ์กด์žฌ:", url)
return False
data.insert(0, url)
return _save_best(data)
except Exception as e:
print("URL ์ถ”๊ฐ€ ์˜ค๋ฅ˜:", e)
return False
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 4. ํŽ˜์ด์ง€๋„ค์ด์…˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def page(lst, pg):
s = (pg-1) * PER_PAGE
e = s + PER_PAGE
total = (len(lst) + PER_PAGE - 1) // PER_PAGE
return lst[s:e], total
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 5. URL โ†’ iframe ๋ณ€ํ™˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def process_url_for_iframe(url):
is_huggingface = False
embed_urls = []
if "huggingface.co/spaces" in url:
is_huggingface = True
base_url = url.rstrip("/")
try:
path = base_url.split("/spaces/")[1]
owner, *rest = path.split("/")
if rest:
name = rest[0]
clean_owner = owner.lower()
clean_name = name.replace('.', '-').replace('_', '-').lower()
embed_urls.append(f"https://huggingface.co/spaces/{owner}/{name}/embed")
embed_urls.append(f"https://{clean_owner}-{clean_name}.hf.space")
else:
embed_urls.append(f"https://huggingface.co/spaces/{owner}/embed")
except Exception:
if not base_url.endswith("/embed"):
embed_urls.append(f"{base_url}/embed")
else:
embed_urls.append(base_url)
elif ".hf.space" in url:
is_huggingface = True
embed_urls.append(url)
else:
return url, False, []
primary = embed_urls[0] if embed_urls else url
return primary, is_huggingface, embed_urls[1:]
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. HTML ๋ Œ๋” โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def html(urls, pg, total):
if not urls:
return "<div style='text-align:center;padding:70px;color:#555;'>ํ‘œ์‹œํ•  ๋ฐฐํฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</div>"
css = r"""
<style>
body{margin:0;font-family:Poppins,sans-serif;background:#f4f5f7;}
.container{padding:16px;}
.grid{
display:grid;
grid-template-columns:repeat(auto-fill,minmax(320px,1fr));
gap:16px;
}
.card{
background:#fff;
border-radius:10px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,.05);
transition:transform .2s;
}
.card:hover{transform:translateY(-4px);}
.frame{position:relative;width:100%;padding-top:56.25%;}
.frame iframe{position:absolute;inset:0;width:100%;height:100%;border:none;}
.frame.huggingface iframe{padding:0;margin:0;}
.page-info{text-align:center;color:#777;margin:12px 0;}
</style>"""
js = """
<script>
function handleIframeError(id, alternates, origin){
const f=document.getElementById(id); if(!f)return;
f.onerror=()=>tryNext(id, alternates, origin);
f.onload=()=>setTimeout(()=>{if(f.offsetWidth===0||f.offsetHeight===0)tryNext(id, alternates, origin);},4000);
}
function tryNext(id, alternates, origin){
const f=document.getElementById(id); if(!f)return;
if(alternates.length){
f.src=alternates.shift();
handleIframeError(id, alternates, origin);
}else{
f.parentNode.innerHTML='<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:14px;color:#999;">๋กœ๋“œ ์‹คํŒจ โ†—</div>';
}
}
window.addEventListener('load',()=>{
document.querySelectorAll('.huggingface iframe').forEach(f=>{
const id=f.id;
const alt=(f.getAttribute('data-alt')||'').split(',').filter(Boolean);
if(id&&alt.length)handleIframeError(id,alt,f.src);
});
});
</script>"""
body = '<div class="container"><div class="grid">'
for i, url in enumerate(urls):
iframe_url, is_hf, alt = process_url_for_iframe(url)
frame_cls = "frame huggingface" if is_hf else "frame"
iframe_id = f"f{i}"
alt_attr = f'data-alt="{",".join(alt)}"' if alt else ""
body += f"""
<div class="card">
<div class="{frame_cls}">
<iframe id="{iframe_id}" src="{iframe_url}" loading="lazy" {alt_attr}></iframe>
</div>
</div>"""
body += "</div></div>"
page_info = f'<div class="page-info">Page {pg} / {total}</div>'
return css + js + body + page_info
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 7. UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def build():
_init_best()
header_html = """
<style>
.app-header{text-align:center;padding:16px 0 8px;margin:0;position:sticky;top:0;background:#fff;z-index:1100;border-bottom:1px solid #eee;}
.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=%23800080&labelColor=%23ffa500&logo=huggingface&logoColor=%23ffff00&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=%23800080&labelColor=%23ffa500&logo=huggingface&logoColor=%23ffff00&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=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge"></a>
</div>
</div>
"""
css_global = """
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=css_global) as demo:
gr.HTML(header_html)
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 show_best(p=1):
d, t = page(_load_best(), p)
return html(d, p, t), p
def prev(b):
b = max(1, b-1)
h, _ = show_best(b)
return h, b
def nxt(b):
maxp = (len(_load_best()) + PER_PAGE - 1) // PER_PAGE
b = min(maxp, b+1)
h, _ = show_best(b)
return h, b
b_prev.click(prev, inputs=[bp], outputs=[out, bp])
b_next.click(nxt, inputs=[bp], outputs=[out, bp])
demo.load(show_best, outputs=[out, bp])
return demo
app = build()
if __name__ == "__main__":
app.launch()