Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,236 +1,175 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
โ Gradio 4.x ๊ณต์ฉ API๋ง ์ฌ์ฉ โ set_event_trigger ์ ๊ฑฐ
|
5 |
-
โ Prev / Next ๋ฒํผ์ผ๋ก ํ์ด์ง ์ ํ
|
6 |
-
โ os.getenv("SVR_TOKEN") ํ์
|
7 |
-
"""
|
8 |
-
|
9 |
-
import os
|
10 |
-
import time
|
11 |
-
import json
|
12 |
-
import requests
|
13 |
-
import datetime
|
14 |
-
import gradio as gr
|
15 |
-
|
16 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
17 |
# 1. Vercel API ์ค์
|
18 |
-
VERCEL_API_TOKEN = os.getenv("SVR_TOKEN")
|
|
|
19 |
if not VERCEL_API_TOKEN:
|
20 |
-
raise EnvironmentError("
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
def
|
35 |
-
if not os.path.exists(BEST_GAMES_FILE):
|
36 |
-
sample = [
|
37 |
-
{
|
38 |
-
"title": "ํ
ํธ๋ฆฌ์ค",
|
39 |
-
"description": "ํด๋์ ํ
ํธ๋ฆฌ์ค ๊ฒ์",
|
40 |
-
"url": "https://tmkdop.vercel.app",
|
41 |
-
"timestamp": time.time(),
|
42 |
-
},
|
43 |
-
{
|
44 |
-
"title": "์ค๋ค์ดํฌ",
|
45 |
-
"description": "์ ํต์ ์ธ ์ค๋ค์ดํฌ ๊ฒ์",
|
46 |
-
"url": "https://tmkdop.vercel.app",
|
47 |
-
"timestamp": time.time(),
|
48 |
-
},
|
49 |
-
{
|
50 |
-
"title": "ํฉ๋งจ",
|
51 |
-
"description": "๊ณ ์ ์์ผ์ด๋ ๊ฒ์",
|
52 |
-
"url": "https://tmkdop.vercel.app",
|
53 |
-
"timestamp": time.time(),
|
54 |
-
},
|
55 |
-
]
|
56 |
-
json.dump(sample, open(BEST_GAMES_FILE, "w"), ensure_ascii=False, indent=2)
|
57 |
-
|
58 |
-
def load_best_games() -> list:
|
59 |
try:
|
60 |
-
return json.load(open(
|
61 |
except Exception:
|
62 |
return []
|
63 |
|
64 |
-
#
|
65 |
-
#
|
66 |
-
def
|
67 |
try:
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
)
|
74 |
-
|
75 |
games = []
|
76 |
-
for d in
|
77 |
if d.get("state") != "READY":
|
78 |
continue
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
"url": f"https://{d.get('url')}",
|
88 |
-
"timestamp": ts,
|
89 |
-
}
|
90 |
-
)
|
91 |
-
games.sort(key=lambda x: x["timestamp"], reverse=True)
|
92 |
-
return games
|
93 |
except Exception as e:
|
94 |
print("Vercel API ์ค๋ฅ:", e)
|
95 |
return []
|
96 |
|
97 |
-
#
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
return lst[start:end], total
|
104 |
|
105 |
-
#
|
106 |
-
#
|
107 |
-
def
|
108 |
-
if not
|
109 |
-
return "<div style='text-align:center;padding:
|
110 |
|
111 |
css = """
|
112 |
<style>
|
113 |
-
|
114 |
-
.
|
115 |
-
.
|
116 |
-
.
|
117 |
-
.
|
118 |
-
.
|
119 |
-
.
|
120 |
-
.
|
121 |
-
|
122 |
-
.footer{padding:
|
123 |
-
.
|
124 |
-
.
|
125 |
-
.
|
126 |
-
|
|
|
|
|
|
|
127 |
</style>
|
128 |
"""
|
|
|
129 |
html = css + "<div class='grid'>"
|
130 |
-
for
|
131 |
-
date = datetime.datetime.fromtimestamp(
|
132 |
html += f"""
|
133 |
-
<div class='
|
134 |
-
<div class='
|
135 |
-
<p class='
|
136 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
</div>
|
138 |
-
<div class='iframe-box'><iframe src="{g['url']}" loading="lazy"
|
139 |
-
allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div>
|
140 |
-
<div class='footer'><a href="{g['url']}" target="_blank">์๋ณธ ์ฌ์ดํธ ์ด๊ธฐ โ</a></div>
|
141 |
</div>
|
142 |
"""
|
143 |
html += "</div>"
|
144 |
-
|
145 |
-
html += f"<div style='text-align:center;margin-top:4px;font-size:.85rem;color:#666;'>Page {page} / {total_pages}</div>"
|
146 |
return html
|
147 |
|
148 |
-
#
|
149 |
-
|
150 |
-
|
151 |
-
initialize_best_games()
|
152 |
|
153 |
-
with gr.Blocks(title="Vibe Game Craft") as demo:
|
154 |
-
gr.
|
155 |
|
156 |
with gr.Row():
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
if tab == "new":
|
206 |
-
games_total = len(get_latest_deployments())
|
207 |
-
max_pages = (games_total + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE
|
208 |
-
p_new = min(max_pages, p_new + 1)
|
209 |
-
html, _, _ = render_new(p_new)
|
210 |
-
return html, p_new, p_best
|
211 |
-
games_total = len(load_best_games())
|
212 |
-
max_pages = (games_total + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE
|
213 |
-
p_best = min(max_pages, p_best + 1)
|
214 |
-
html, _, _ = render_best(p_best)
|
215 |
-
return html, p_new, p_best
|
216 |
-
|
217 |
-
prev_btn.click(fn=prev_page,
|
218 |
-
inputs=[current_tab, new_page, best_page],
|
219 |
-
outputs=[gallery_html, new_page, best_page])
|
220 |
-
|
221 |
-
next_btn.click(fn=next_page,
|
222 |
-
inputs=[current_tab, new_page, best_page],
|
223 |
-
outputs=[gallery_html, new_page, best_page])
|
224 |
-
|
225 |
-
# ์ด๊ธฐ NEW ํญ ํ์
|
226 |
-
demo.load(fn=render_new,
|
227 |
-
outputs=[gallery_html, current_tab, new_page])
|
228 |
|
229 |
return demo
|
230 |
|
231 |
-
#
|
232 |
-
|
233 |
-
app = create_gallery_interface()
|
234 |
|
235 |
if __name__ == "__main__":
|
236 |
app.launch()
|
|
|
1 |
+
import os, time, json, datetime, requests, gradio as gr
|
2 |
+
|
3 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
# 1. Vercel API ์ค์
|
5 |
+
VERCEL_API_TOKEN = os.getenv("SVR_TOKEN")
|
6 |
+
VERCEL_TEAM_ID = os.getenv("VERCEL_TEAM_ID") # ๊ฐ์ธ ๊ณ์ ์ด๋ฉด None
|
7 |
if not VERCEL_API_TOKEN:
|
8 |
+
raise EnvironmentError("SVR_TOKEN ํ๊ฒฝ๋ณ์๋ฅผ ์ค์ ํ์ธ์.")
|
9 |
+
|
10 |
+
API_BASE = "https://api.vercel.com"
|
11 |
+
HEADERS = {"Authorization": f"Bearer {VERCEL_API_TOKEN}"}
|
12 |
+
|
13 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
14 |
+
# 2. BEST ํญ ํ์ผ / ์์
|
15 |
+
BEST_FILE = "best_games.json"
|
16 |
+
CARDS_PER_PG = 48
|
17 |
+
|
18 |
+
def init_best():
|
19 |
+
if not os.path.exists(BEST_FILE):
|
20 |
+
json.dump([], open(BEST_FILE, "w"))
|
21 |
+
|
22 |
+
def load_best():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
try:
|
24 |
+
return json.load(open(BEST_FILE))
|
25 |
except Exception:
|
26 |
return []
|
27 |
|
28 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
29 |
+
# 3. ๋ชจ๋ ํ๋ก์ ํธ ์ต์ ๋ฐฐํฌ ๊ฐ์ ธ์ค๊ธฐ
|
30 |
+
def fetch_deployments(limit=100):
|
31 |
try:
|
32 |
+
params = {"limit": limit}
|
33 |
+
if VERCEL_TEAM_ID:
|
34 |
+
params["teamId"] = VERCEL_TEAM_ID
|
35 |
+
r = requests.get(f"{API_BASE}/v13/deployments",
|
36 |
+
headers=HEADERS, params=params, timeout=30)
|
37 |
+
r.raise_for_status()
|
38 |
+
|
39 |
games = []
|
40 |
+
for d in r.json().get("deployments", []):
|
41 |
if d.get("state") != "READY":
|
42 |
continue
|
43 |
+
ts = int(d.get("createdAt", time.time()*1000) / 1000)
|
44 |
+
games.append({
|
45 |
+
"title" : d.get("name", "(์ ๋ชฉ ์์)"),
|
46 |
+
"description": f"ํ๋ก์ ํธ: {d.get('name')}",
|
47 |
+
"url" : f"https://{d.get('url')}",
|
48 |
+
"timestamp" : ts,
|
49 |
+
})
|
50 |
+
return sorted(games, key=lambda x: x["timestamp"], reverse=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
except Exception as e:
|
52 |
print("Vercel API ์ค๋ฅ:", e)
|
53 |
return []
|
54 |
|
55 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
56 |
+
def paginate(lst, page):
|
57 |
+
s = (page-1) * CARDS_PER_PG
|
58 |
+
e = s + CARDS_PER_PG
|
59 |
+
total = (len(lst) + CARDS_PER_PG - 1)//CARDS_PER_PG
|
60 |
+
return lst[s:e], total
|
|
|
61 |
|
62 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
63 |
+
# 4. HTML ์์ฑ (ํ์คํ
์คํ์ผ, ๊ฐ๋ก 100 %)
|
64 |
+
def make_html(cards, page, total):
|
65 |
+
if not cards:
|
66 |
+
return "<div style='text-align:center;padding:80px;font-size:1.1rem;color:#555;'>ํ์ํ ๋ฐฐํฌ๊ฐ ์์ต๋๋ค.</div>"
|
67 |
|
68 |
css = """
|
69 |
<style>
|
70 |
+
body{margin:0;padding:0;font-family:'Poppins',sans-serif;background:linear-gradient(135deg,#C5E8FF 0%,#FFD6E0 100%);}
|
71 |
+
.grid{display:grid;grid-template-columns:1fr;gap:40px;padding-bottom:60px}
|
72 |
+
.card{background:#fff;border-radius:20px;overflow:hidden;box-shadow:0 15px 30px rgba(0,0,0,.08);transition:.3s}
|
73 |
+
.card:hover{transform:translateY(-8px);box-shadow:0 25px 45px rgba(0,0,0,.12)}
|
74 |
+
.header{padding:24px 28px;background:rgba(255,255,255,.75);backdrop-filter:blur(8px);border-bottom:1px solid #eee}
|
75 |
+
.title{margin:0;font-size:1.25rem;font-weight:700;color:#333}
|
76 |
+
.date{font-size:.9rem;color:#777;margin-top:4px}
|
77 |
+
.frame{position:relative;width:100%;padding-top:56.25%;overflow:hidden}
|
78 |
+
.frame iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:0}
|
79 |
+
.footer{padding:16px 28px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right}
|
80 |
+
.link{font-size:.9rem;font-weight:600;color:#4a6dd8;text-decoration:none}
|
81 |
+
.pager{display:flex;justify-content:center;gap:16px;margin:20px 0}
|
82 |
+
.btn{border:none;background:#fff;padding:10px 24px;border-radius:12px;font-weight:600;
|
83 |
+
box-shadow:0 4px 12px rgba(0,0,0,.05);cursor:pointer;transition:.25s}
|
84 |
+
.btn:hover{background:#f1e8ff}
|
85 |
+
.btn:disabled{opacity:.4;cursor:default}
|
86 |
+
.count{font-size:.85rem;color:#555;text-align:center;margin-top:-4px}
|
87 |
</style>
|
88 |
"""
|
89 |
+
|
90 |
html = css + "<div class='grid'>"
|
91 |
+
for c in cards:
|
92 |
+
date = datetime.datetime.fromtimestamp(c["timestamp"]).strftime("%Y-%m-%d")
|
93 |
html += f"""
|
94 |
+
<div class='card'>
|
95 |
+
<div class='header'>
|
96 |
+
<p class='title'>{c['title']}</p>
|
97 |
+
<p class='date'>{date}</p>
|
98 |
+
</div>
|
99 |
+
<div class='frame'>
|
100 |
+
<iframe src="{c['url']}" loading="lazy"
|
101 |
+
allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe>
|
102 |
+
</div>
|
103 |
+
<div class='footer'>
|
104 |
+
<a href="{c['url']}" target="_blank" class='link'>์๋ณธ ์ฌ์ดํธ โ</a>
|
105 |
</div>
|
|
|
|
|
|
|
106 |
</div>
|
107 |
"""
|
108 |
html += "</div>"
|
109 |
+
html += f"<p class='count'>Page {page} / {total}</p>"
|
|
|
110 |
return html
|
111 |
|
112 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
113 |
+
def build_app():
|
114 |
+
init_best()
|
|
|
115 |
|
116 |
+
with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo:
|
117 |
+
gr.Markdown("<h1 style='text-align:center;padding:30px 0;color:#333;'>๐ฎ Vibe Game Craft</h1>")
|
118 |
|
119 |
with gr.Row():
|
120 |
+
btn_new = gr.Button("NEW", size="sm")
|
121 |
+
btn_best = gr.Button("BEST", size="sm")
|
122 |
+
btn_prev = gr.Button("โฌ
๏ธ Prev", size="sm")
|
123 |
+
btn_next = gr.Button("Next โก๏ธ", size="sm")
|
124 |
+
btn_ref = gr.Button("๐ Reload", size="sm")
|
125 |
+
|
126 |
+
tab = gr.State("new")
|
127 |
+
np = gr.State(1)
|
128 |
+
bp = gr.State(1)
|
129 |
+
out = gr.HTML()
|
130 |
+
|
131 |
+
# ์ฝ๋ฐฑ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
132 |
+
def show_new(p=1):
|
133 |
+
cards, total = paginate(fetch_deployments(), p)
|
134 |
+
return make_html(cards, p, total), "new", p
|
135 |
+
|
136 |
+
def show_best(p=1):
|
137 |
+
cards, total = paginate(load_best(), p)
|
138 |
+
return make_html(cards, p, total), "best", p
|
139 |
+
|
140 |
+
def prev(t, cp_n, cp_b):
|
141 |
+
if t=="new":
|
142 |
+
cp_n = max(1, cp_n-1); html, _,_ = show_new(cp_n)
|
143 |
+
return html, cp_n, cp_b
|
144 |
+
cp_b = max(1, cp_b-1); html, _,_ = show_best(cp_b)
|
145 |
+
return html, cp_n, cp_b
|
146 |
+
|
147 |
+
def nxt(t, cp_n, cp_b):
|
148 |
+
if t=="new":
|
149 |
+
max_pg = (len(fetch_deployments())+CARDS_PER_PG-1)//CARDS_PER_PG
|
150 |
+
cp_n = min(max_pg, cp_n+1); html, _,_ = show_new(cp_n)
|
151 |
+
return html, cp_n, cp_b
|
152 |
+
max_pg = (len(load_best())+CARDS_PER_PG-1)//CARDS_PER_PG
|
153 |
+
cp_b = min(max_pg, cp_b+1); html, _,_ = show_best(cp_b)
|
154 |
+
return html, cp_n, cp_b
|
155 |
+
|
156 |
+
def refresh(t, cp_n, cp_b):
|
157 |
+
return show_new(cp_n)[0] if t=="new" else show_best(cp_b)[0]
|
158 |
+
|
159 |
+
# โฆฟ ๋ฒํผ ์ฐ๊ฒฐ
|
160 |
+
btn_new.click(show_new, outputs=[out, tab, np])
|
161 |
+
btn_best.click(show_best, outputs=[out, tab, bp])
|
162 |
+
btn_prev.click(prev, inputs=[tab, np, bp], outputs=[out, np, bp])
|
163 |
+
btn_next.click(nxt, inputs=[tab, np, bp], outputs=[out, np, bp])
|
164 |
+
btn_ref.click(refresh, inputs=[tab, np, bp], outputs=[out])
|
165 |
+
|
166 |
+
# ์ด๊ธฐ ๋ก๋
|
167 |
+
demo.load(show_new, outputs=[out, tab, np])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
168 |
|
169 |
return demo
|
170 |
|
171 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
172 |
+
app = build_app()
|
|
|
173 |
|
174 |
if __name__ == "__main__":
|
175 |
app.launch()
|