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, []
# ───────────────────── 6. HTML 그리드 ───────────────────
def html(cards, pg, total):
if not cards:
return "
표시할 배포가 없습니다.
"
css = r"""
"""
js = """
"""
h = css + js + ''
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"""
"""
h += "
"
h += f'Page {pg} / {total}
'
return h
# ───────────────────── 5. Gradio UI ─────────────────────
def build():
_init_best()
header = """
"""
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()