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 "
표시할 배포가 없습니다.
" css = r""" """ # 버튼과 헤더 높이를 고려해 스크롤 영역 동적으로 계산 js = r""" """ h = css + js + '
' 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""" """ h += "
" h += f'
Page {pg} / {total}
' return h # ───────────────────── 5. Gradio UI ───────────────────── def build(): _init_best() header = """

🎮 Vibe Game Gallery

Only high-quality games automatically generated with Vibe Game Craft are showcased here.
Every game includes its full source code, and anyone can freely copy the index.html file from each URL and modify it as desired. All content is released under the Apache 2.0 license.

""" 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()