Game-Gallery / app-backup-last.py
openfree's picture
Rename app.py to app-backup-last.py
c8c0a60 verified
raw
history blame
20.2 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"))
def _load_best():
try:
data = json.load(open(BEST_FILE))
for it in data:
if "ts" not in it:
it["ts"] = int(it.get("timestamp", time.time()))
return data
except Exception as e:
print(f"BEST ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜: {e}")
return []
def _save_best(data):
try:
json.dump(data, open(BEST_FILE, "w"))
return True
except Exception as e:
print(f"BEST ๋ฐ์ดํ„ฐ ์ €์žฅ ์˜ค๋ฅ˜: {e}")
return False
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 3. URL ์ถ”๊ฐ€ ๊ธฐ๋Šฅ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def add_url_to_best(title, url):
"""์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ URL์„ BEST ๋ชฉ๋ก์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค."""
try:
# ํ˜„์žฌ BEST ๋ฐ์ดํ„ฐ ๋กœ๋“œ
data = _load_best()
# URL์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
for item in data:
if item.get("url") == url:
print(f"URL์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค: {url}")
return False
# ์ƒˆ ํ•ญ๋ชฉ ์ถ”๊ฐ€
new_item = {
"title": title,
"url": url,
"ts": int(time.time()),
"projectId": "", # ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ถ”๊ฐ€ํ•˜๋ฏ€๋กœ projectId ์—†์Œ
"deploymentId": "" # ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ถ”๊ฐ€ํ•˜๋ฏ€๋กœ deploymentId ์—†์Œ
}
data.append(new_item)
# ์‹œ๊ฐ„์ˆœ์œผ๋กœ ์ •๋ ฌ
data = sorted(data, key=lambda x: x["ts"], reverse=True)
# ์ €์žฅ
if _save_best(data):
print(f"URL์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค: {url}")
return True
return False
except Exception as e:
print(f"URL ์ถ”๊ฐ€ ์˜ค๋ฅ˜: {str(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 ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def process_url_for_iframe(url):
"""URL์„ iframe์— ํ‘œ์‹œํ•˜๊ธฐ ์ ํ•ฉํ•œ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
# ํ—ˆ๊น…ํŽ˜์ด์Šค URL ํŒจํ„ด ๊ฐ์ง€
is_huggingface = False
embed_urls = []
# 1. huggingface.co/spaces ํŒจํ„ด ์ฒ˜๋ฆฌ
if "huggingface.co/spaces" in url:
is_huggingface = True
# ๊ธฐ๋ณธ URL ์ •๊ทœํ™”
base_url = url.rstrip("/")
try:
# /spaces/ ์ดํ›„์˜ ๊ฒฝ๋กœ ์ถ”์ถœ
if "/spaces/" in base_url:
path = base_url.split("/spaces/")[1]
parts = path.split("/")
owner = parts[0]
# name ๋ถ€๋ถ„ ์ถ”์ถœ
if len(parts) > 1:
name = parts[1]
# ํŠน์ˆ˜ ๋ฌธ์ž ๋ณ€ํ™˜
clean_name = name.replace('.', '-').replace('_', '-').lower()
clean_owner = owner.lower()
# ์—ฌ๋Ÿฌ ํฌ๋งท์˜ URL์„ ์‹œ๋„ํ•˜๊ธฐ ์œ„ํ•ด ๋ชฉ๋ก์— ์ถ”๊ฐ€
embed_urls.append(f"https://huggingface.co/spaces/{owner}/{name}/embed") # ๊ณต์‹ embed URL
embed_urls.append(f"https://{clean_owner}-{clean_name}.hf.space") # ์ง์ ‘ ๋„๋ฉ”์ธ ์ ‘๊ทผ
else:
# owner๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ณต์‹ URL ์‚ฌ์šฉ
embed_urls.append(f"https://huggingface.co/spaces/{owner}/embed")
except Exception as e:
print(f"ํ—ˆ๊น…ํŽ˜์ด์Šค URL ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}")
# ๊ธฐ๋ณธ embed URL ์‹œ๋„
if not base_url.endswith("/embed"):
embed_urls.append(f"{base_url}/embed")
else:
embed_urls.append(base_url)
# 2. .hf.space ๋„๋ฉ”์ธ ์ฒ˜๋ฆฌ
elif ".hf.space" in url:
is_huggingface = True
embed_urls.append(url) # ํ˜„์žฌ URL ๊ทธ๋Œ€๋กœ ์‹œ๋„
# 3. ์ผ๋ฐ˜ URL์€ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜
else:
return url, is_huggingface, []
# ์ตœ์ข… URL๊ณผ ํ•จ๊ป˜ ์‹œ๋„ํ•  ๋Œ€์ฒด URL ๋ชฉ๋ก ๋ฐ˜ํ™˜
primary_url = embed_urls[0] if embed_urls else url
return primary_url, is_huggingface, embed_urls[1:] if len(embed_urls) > 1 else []
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 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 {
position: relative;
width: 100%;
height: auto;
box-sizing: border-box;
padding: 10px;
padding-bottom: 70px; /* Space for buttons */
overflow-y: auto;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
width: 100%;
margin-bottom: 60px; /* Space for buttons */
}
.card {
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
/* Each card is 50% taller */
height: 300px; /* Base height - will be 50% larger */
}
.hdr {
padding: 8px 12px;
background: rgba(255,255,255,.95);
border-bottom: 1px solid #eee;
flex-shrink: 0;
z-index: 10;
}
.ttl {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.date {
margin-top: 2px;
font-size: 0.75rem;
color: #777;
}
.frame {
flex: 1;
position: relative;
overflow: hidden;
}
.frame iframe {
position: absolute;
top: 0;
left: 0;
width: 142.857%;
height: 142.857%;
transform: scale(0.7);
transform-origin: top left;
border: 0;
frameborder: 0;
}
/* ํ—ˆ๊น…ํŽ˜์ด์Šค iframe ํŠน๋ณ„ ์Šคํƒ€์ผ */
.frame.huggingface iframe {
width: 100% !important;
height: 100% !important;
transform: none !important;
border: none !important;
}
.foot {
padding: 6px 12px;
background: rgba(255,255,255,.95);
text-align: right;
flex-shrink: 0;
border-top: 1px solid #f0f0f0;
z-index: 10;
}
.link {
font-size: 0.8rem;
font-weight: 600;
color: #4a6dd8;
text-decoration: none;
}
/* ๋ฒ„ํŠผ ์ปจํ…Œ์ด๋„ˆ */
.button-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
background: #f0f0f0;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
z-index: 100;
}
/* ํŽ˜์ด์ง€ ์ •๋ณด */
.page-info {
position: fixed;
bottom: 5px;
left: 0;
right: 0;
text-align: center;
font-size: 0.8rem;
color: #777;
z-index: 101;
}
/* ๋ฏธ๋””์–ด ์ฟผ๋ฆฌ */
@media (min-width: 1200px) {
.card {
height: 450px; /* 50% taller than 300px */
}
}
@media (min-width: 768px) and (max-width: 1199px) {
.card {
height: 400px; /* Adjusted for medium screens */
}
}
@media (max-width: 767px) {
.grid {
grid-template-columns: 1fr;
}
.card {
height: 350px; /* Adjusted for small screens */
}
}
/* ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๊ด€๋ จ CSS */
.error-message {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.9);
z-index: 20;
padding: 20px;
text-align: center;
}
.error-icon {
font-size: 48px;
margin-bottom: 15px;
}
.error-text {
font-weight: bold;
margin-bottom: 15px;
}
.error-link {
display: inline-block;
padding: 8px 16px;
background-color: #f0f0f7;
border-radius: 4px;
color: #4a6dd8;
font-weight: bold;
text-decoration: none;
transition: background-color 0.2s;
}
.error-link:hover {
background-color: #e0e0f0;
}
</style>"""
js = """
<script>
// ํ—ˆ๊น…ํŽ˜์ด์Šค iframe ๋กœ๋”ฉ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
function handleIframeError(iframeId, alternateUrls, originalUrl) {
const iframe = document.getElementById(iframeId);
if (!iframe) return;
// iframe ๋กœ๋“œ ์‹คํŒจ ์ฒ˜๋ฆฌ
iframe.onerror = function() {
tryNextUrl(iframeId, alternateUrls, originalUrl);
};
// onload ์ด๋ฒคํŠธ์—์„œ ๋กœ๋“œ ์‹คํŒจ ํ™•์ธ
iframe.onload = function() {
try {
// iframe ๋‚ด์šฉ์— ์ ‘๊ทผ ์‹œ๋„ (cross-origin ์ œํ•œ์œผ๋กœ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์Œ)
const iframeContent = iframe.contentWindow.document;
console.log("iframe loaded successfully: " + iframeId);
} catch (e) {
// cross-origin ์˜ค๋ฅ˜๋Š” ๋ฌด์‹œ (์ •์ƒ์ ์ธ ์ƒํ™ฉ์ผ ์ˆ˜ ์žˆ์Œ)
console.log("Cross-origin iframe loaded (expected): " + iframeId);
}
// iframe์ด ์ œ๋Œ€๋กœ ํ‘œ์‹œ๋˜๋Š”์ง€ 10์ดˆ ํ›„ ํ™•์ธ
setTimeout(function() {
if (iframe.offsetWidth === 0 || iframe.offsetHeight === 0) {
console.log("iframe not visible, trying alternate URL: " + iframeId);
tryNextUrl(iframeId, alternateUrls, originalUrl);
} else {
console.log("iframe appears to be visible: " + iframeId);
}
}, 5000);
};
}
// ๋Œ€์ฒด URL ์‹œ๋„
function tryNextUrl(iframeId, alternateUrls, originalUrl) {
const iframe = document.getElementById(iframeId);
if (!iframe) return;
// ๋Œ€์ฒด URL์ด ์žˆ์œผ๋ฉด ์‹œ๋„
if (alternateUrls && alternateUrls.length > 0) {
const nextUrl = alternateUrls.shift();
console.log("Trying alternate URL: " + nextUrl);
iframe.src = nextUrl;
// ๋Œ€์ฒด URL๋กœ ๋‹ค์‹œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ์„ค์ •
handleIframeError(iframeId, alternateUrls, originalUrl);
} else {
// ๋ชจ๋“  URL ์‹œ๋„ ์‹คํŒจ ์‹œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
console.log("All URLs failed, showing error for: " + iframeId);
const container = iframe.parentNode;
// ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.innerHTML = `
<div class="error-icon">โš ๏ธ</div>
<p class="error-text">์ฝ˜ํ…์ธ ๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</p>
<a href="${originalUrl}" target="_blank" class="error-link">
์›๋ณธ ํŽ˜์ด์ง€์—์„œ ์—ด๊ธฐ
</a>
`;
// iframe ์ˆจ๊ธฐ๊ณ  ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
iframe.style.display = 'none';
container.appendChild(errorDiv);
}
}
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ๋ชจ๋“  ํ—ˆ๊น…ํŽ˜์ด์Šค iframe ์ฒ˜๋ฆฌ
window.addEventListener('load', function() {
const iframes = document.querySelectorAll('.huggingface iframe');
iframes.forEach(function(iframe) {
const id = iframe.id;
const alternateUrlsStr = iframe.getAttribute('data-alternate-urls');
const alternateUrls = alternateUrlsStr ? alternateUrlsStr.split(',').filter(url => url) : [];
const originalUrl = iframe.getAttribute('data-original-url') || iframe.src;
if (id && alternateUrls.length > 0) {
handleIframeError(id, alternateUrls, originalUrl);
}
});
});
</script>
"""
h = css + js + """
<div class="container">
<div class="grid">
"""
for idx, c in enumerate(cards):
date = datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d")
# URL ์ฒ˜๋ฆฌ: ํ—ˆ๊น…ํŽ˜์ด์Šค URL์ธ ๊ฒฝ์šฐ ํŠน๋ณ„ ์ฒ˜๋ฆฌ
url = c['url']
iframe_url, is_huggingface, alt_urls = process_url_for_iframe(url)
# ํ—ˆ๊น…ํŽ˜์ด์Šค URL์— ํŠน๋ณ„ ํด๋ž˜์Šค ์ถ”๊ฐ€
frame_class = "frame huggingface" if is_huggingface else "frame"
# ๊ณ ์œ  ID ์ƒ์„ฑ
iframe_id = f"iframe-{idx}-{hash(url) % 10000}"
# ๋Œ€์ฒด URL์„ ๋ฐ์ดํ„ฐ ์†์„ฑ์œผ๋กœ ์ถ”๊ฐ€
alternate_urls_attr = ""
if alt_urls:
alternate_urls_attr = f'data-alternate-urls="{",".join(alt_urls)}"'
h += f"""
<div class="card">
<div class="hdr"><p class="ttl">{c['title']}</p><p class="date">{date}</p></div>
<div class="{frame_class}">
<iframe
id="{iframe_id}"
src="{iframe_url}"
loading="lazy"
frameborder="0"
width="100%"
height="100%"
allowfullscreen="true"
allow="accelerometer; camera; encrypted-media; gyroscope; geolocation;"
sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-downloads"
data-original-url="{url}"
{alternate_urls_attr}>
</iframe>
</div>
<div class="foot"><a class="link" href="{url}" target="_blank">์›๋ณธโ†—</a></div>
</div>"""
h += """
</div>
</div>
"""
# ํŽ˜์ด์ง€ ์ •๋ณด
h += f'<div class="page-info">Page {pg} / {total}</div>'
return h
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 7. Gradio Blocks UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 7. Gradio Blocks UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def build():
_init_best()
# (1) โ”€โ”€ ํ—ค๋” HTML (๋ณ€๊ฒฝ ์—†์Œ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
header_html_snippet = """
<style>
.app-header{ text-align:center; margin-bottom:24px; }
.badge-row{
display:inline-flex;
gap:8px;
margin:8px 0;
}
</style>
<div class="app-header">
<h1>๐ŸŽฎ 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" alt="HF-Vibe">
</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" alt="HF-Gallery">
</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" alt="Discord">
</a>
</div>
<p>ํ”„๋กฌํ”„ํŠธ ์ž…๋ ฅ๋งŒ์œผ๋กœ ์ตœ์‹  LLM๋“ค๊ณผ Agent๊ฐ€ ํ˜‘์—…ํ•˜์—ฌ ์›น ๊ธฐ๋ฐ˜ ๊ฒŒ์ž„์„ ์ƒ์„ฑํ•˜๊ณ  ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค.</p>
</div>
"""
# (2) โ”€โ”€ ์ „์—ญ CSS (ํ—ค๋” sticky + ์Šคํฌ๋กค ์˜์—ญ ์žฌ์กฐ์ •) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
css_global = """
footer{display:none !important;}
/* ์ƒ๋‹จ ํ—ค๋”๋ฅผ ํ•ญ์ƒ ๋ณด์ด๋„๋ก ๊ณ ์ • */
.app-header{
position:sticky;
top:0;
background:#fff;
z-index:1100;
padding:16px 0 8px;
border-bottom:1px solid #eee;
}
/* ํ•˜๋‹จ ๊ณ ์ • ๋ฒ„ํŠผ ๋ฐ” */
.button-row{
position:fixed !important;
bottom:0 !important;
left:0 !important;
right:0 !important;
height:60px !important;
background:#f0f0f0 !important;
padding:10px !important;
text-align:center !important;
box-shadow:0 -2px 10px rgba(0,0,0,0.05) !important;
margin:0 !important;
z-index:1000 !important;
}
.button-row button{
margin:0 10px !important;
padding:10px 20px !important;
font-size:16px !important;
font-weight:bold !important;
border-radius:50px !important;
}
/* ์นด๋“œ ๊ทธ๋ฆฌ๋“œ ์Šคํฌ๋กค ์˜์—ญ */
#content-area{
overflow-y:auto !important;
height:calc(100vh - 60px - 160px) !important; /* ์ „์ฒด-๋†’์ด - ํ•˜๋‹จ๋ฐ” - ํ—ค๋” */
box-sizing:border-box;
padding-top:10px;
}
"""
# (3) โ”€โ”€ Gradio Blocks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Blocks(title="Vibe Game Craft", css=css_global) as demo:
# โ‘  ๊ณ ์ • ํ—ค๋”
gr.HTML(header_html_snippet)
# โ‘ก ๋ณธ๋ฌธ(์นด๋“œ ๊ทธ๋ฆฌ๋“œ) โ†’ ๊ณ ์œ  ID ๋ถ€์—ฌ
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
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 8. ์•ฑ ์ƒ์„ฑ & ์‹คํ–‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app = build() # โ† Blocks ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
if __name__ == "__main__":
# Spaces๋‚˜ ๋กœ์ปฌ์—์„œ ์‹คํ–‰๋  ๋•Œ ์ง„์ž…์ 
app.launch() # share=True ๋“ฑ ์˜ต์…˜์ด ํ•„์š”ํ•˜๋ฉด ์—ฌ๊ธฐ์„œ ์ง€์ •