openfree commited on
Commit
0299d00
·
verified ·
1 Parent(s): 61ff302

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +67 -132
app.py CHANGED
@@ -1,7 +1,7 @@
1
- import os, re, time, json, datetime, requests, gradio as gr
2
 
3
  # ───────────────────── 1. 기본 설정 ─────────────────────
4
- BEST_FILE, PER_PAGE = "best_games.json", 9
5
 
6
  # ───────────────────── 2. BEST 데이터 ────────────────────
7
  def _init_best():
@@ -9,85 +9,48 @@ def _init_best():
9
  json.dump([], open(BEST_FILE, "w"), ensure_ascii=False)
10
 
11
  def _load_best():
12
- """best_games.json → ["https://foo.vercel.app", ...] 형태로 로드"""
13
  try:
14
  raw = json.load(open(BEST_FILE))
 
15
  if isinstance(raw, list):
16
- urls = []
17
- for it in raw:
18
- if isinstance(it, str):
19
- urls.append(it)
20
- elif isinstance(it, dict) and "url" in it:
21
- urls.append(it["url"])
22
- return urls
23
  return []
24
  except Exception as e:
25
- print(f"BEST 데이터 로드 오류: {e}")
26
  return []
27
 
28
- def _save_best(url_list: list[str]) -> bool:
29
  try:
30
- json.dump(url_list, open(BEST_FILE, "w"), ensure_ascii=False, indent=2)
31
  return True
32
  except Exception as e:
33
- print(f"BEST 데이터 저장 오류: {e}")
34
  return False
35
 
36
- # ───────────────────── 3. URL 추가 기능 ─────────────────────
37
- def add_url_to_best(url: str) -> bool:
38
- try:
39
- data = _load_best()
40
- if url in data:
41
- print("이미 존재:", url)
42
- return False
43
- data.insert(0, url)
44
- return _save_best(data)
45
- except Exception as e:
46
- print("URL 추가 오류:", e)
47
  return False
 
 
48
 
49
- # ───────────────────── 4. 페이지네이션 ───────────────────
50
  def page(lst, pg):
51
- s = (pg-1) * PER_PAGE
52
- e = s + PER_PAGE
53
- total = (len(lst) + PER_PAGE - 1) // PER_PAGE
54
  return lst[s:e], total
55
 
56
- # ───────────────────── 5. URL → iframe 변환 ───────────────
57
  def process_url_for_iframe(url):
58
- is_huggingface = False
59
- embed_urls = []
60
  if "huggingface.co/spaces" in url:
61
- is_huggingface = True
62
- base_url = url.rstrip("/")
63
- try:
64
- path = base_url.split("/spaces/")[1]
65
- owner, *rest = path.split("/")
66
- if rest:
67
- name = rest[0]
68
- clean_owner = owner.lower()
69
- clean_name = name.replace('.', '-').replace('_', '-').lower()
70
- embed_urls.append(f"https://huggingface.co/spaces/{owner}/{name}/embed")
71
- embed_urls.append(f"https://{clean_owner}-{clean_name}.hf.space")
72
- else:
73
- embed_urls.append(f"https://huggingface.co/spaces/{owner}/embed")
74
- except Exception:
75
- if not base_url.endswith("/embed"):
76
- embed_urls.append(f"{base_url}/embed")
77
- else:
78
- embed_urls.append(base_url)
79
- elif ".hf.space" in url:
80
- is_huggingface = True
81
- embed_urls.append(url)
82
- else:
83
- return url, False, []
84
- primary = embed_urls[0] if embed_urls else url
85
- return primary, is_huggingface, embed_urls[1:]
86
-
87
- # ───────────────────── 6. HTML 렌더 ───────────────────────
88
  def html(urls, pg, total):
89
  if not urls:
90
- return "<div style='text-align:center;padding:70px;color:#555;'>표시할 배포가 없습니다.</div>"
91
 
92
  css = r"""
93
  <style>
@@ -95,92 +58,74 @@ def html(urls, pg, total):
95
  .container{padding:16px;}
96
  .grid{
97
  display:grid;
98
- grid-template-columns:repeat(auto-fill,minmax(320px,1fr));
99
  gap:16px;
100
  }
101
  .card{
102
- background:#fff;
103
- border-radius:10px;
104
- overflow:hidden;
105
- box-shadow:0 2px 8px rgba(0,0,0,.05);
106
  transition:transform .2s;
107
  }
 
 
 
 
108
  .card:hover{transform:translateY(-4px);}
109
- .frame{position:relative;width:100%;padding-top:56.25%;}
110
  .frame iframe{position:absolute;inset:0;width:100%;height:100%;border:none;}
111
- .frame.huggingface iframe{padding:0;margin:0;}
112
  .page-info{text-align:center;color:#777;margin:12px 0;}
113
  </style>"""
114
 
115
- js = """
116
- <script>
117
- function handleIframeError(id, alternates, origin){
118
- const f=document.getElementById(id); if(!f)return;
119
- f.onerror=()=>tryNext(id, alternates, origin);
120
- f.onload=()=>setTimeout(()=>{if(f.offsetWidth===0||f.offsetHeight===0)tryNext(id, alternates, origin);},4000);
121
- }
122
- function tryNext(id, alternates, origin){
123
- const f=document.getElementById(id); if(!f)return;
124
- if(alternates.length){
125
- f.src=alternates.shift();
126
- handleIframeError(id, alternates, origin);
127
- }else{
128
- f.parentNode.innerHTML='<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:14px;color:#999;">로드 실패 ↗</div>';
129
- }
130
- }
131
- window.addEventListener('load',()=>{
132
- document.querySelectorAll('.huggingface iframe').forEach(f=>{
133
- const id=f.id;
134
- const alt=(f.getAttribute('data-alt')||'').split(',').filter(Boolean);
135
- if(id&&alt.length)handleIframeError(id,alt,f.src);
136
- });
137
- });
138
- </script>"""
139
-
140
  body = '<div class="container"><div class="grid">'
141
- for i, url in enumerate(urls):
142
- iframe_url, is_hf, alt = process_url_for_iframe(url)
143
- frame_cls = "frame huggingface" if is_hf else "frame"
144
- iframe_id = f"f{i}"
145
- alt_attr = f'data-alt="{",".join(alt)}"' if alt else ""
146
  body += f"""
147
  <div class="card">
148
- <div class="{frame_cls}">
149
- <iframe id="{iframe_id}" src="{iframe_url}" loading="lazy" {alt_attr}></iframe>
150
  </div>
151
  </div>"""
152
  body += "</div></div>"
153
  page_info = f'<div class="page-info">Page {pg} / {total}</div>'
154
- return css + js + body + page_info
155
 
156
- # ───────────────────── 7. UI ─────────────────────────────
157
  def build():
158
  _init_best()
159
 
160
- header_html = """
161
  <style>
162
- .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;}
 
163
  .badge-row{display:inline-flex;gap:8px;margin:8px 0;}
164
  </style>
165
  <div class="app-header">
166
  <h1 style="margin:0;font-size:28px;">🎮 Vibe Game Gallery</h1>
167
  <div class="badge-row">
168
- <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>
169
- <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>
170
- <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>
 
 
 
 
 
 
171
  </div>
172
- </div>
173
- """
174
-
175
- css_global = """
176
- footer{display:none !important;}
177
- .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;}
178
- .button-row button{margin:0 10px;padding:10px 20px;font-size:16px;font-weight:bold;border-radius:50px;}
179
- #content-area{overflow-y:auto;height:calc(100vh - 60px - 120px);}
 
180
  """
181
 
182
- with gr.Blocks(title="Vibe Game Gallery", css=css_global) as demo:
183
- gr.HTML(header_html)
184
  out = gr.HTML(elem_id="content-area")
185
  with gr.Row(elem_classes="button-row"):
186
  b_prev = gr.Button("◀ 이전", size="lg")
@@ -188,24 +133,14 @@ def build():
188
 
189
  bp = gr.State(1)
190
 
191
- def show_best(p=1):
192
- d, t = page(_load_best(), p)
193
- return html(d, p, t), p
194
-
195
- def prev(b):
196
- b = max(1, b-1)
197
- h, _ = show_best(b)
198
- return h, b
199
 
200
- def nxt(b):
201
- maxp = (len(_load_best()) + PER_PAGE - 1) // PER_PAGE
202
- b = min(maxp, b+1)
203
- h, _ = show_best(b)
204
- return h, b
205
 
206
- b_prev.click(prev, inputs=[bp], outputs=[out, bp])
207
- b_next.click(nxt, inputs=[bp], outputs=[out, bp])
208
- demo.load(show_best, outputs=[out, bp])
209
 
210
  return demo
211
 
 
1
+ import os, json, time, datetime, requests, gradio as gr
2
 
3
  # ───────────────────── 1. 기본 설정 ─────────────────────
4
+ BEST_FILE, PER_PAGE = "best_games.json", 9 # ❶ 페이지당 9개 유지
5
 
6
  # ───────────────────── 2. BEST 데이터 ────────────────────
7
  def _init_best():
 
9
  json.dump([], open(BEST_FILE, "w"), ensure_ascii=False)
10
 
11
  def _load_best():
 
12
  try:
13
  raw = json.load(open(BEST_FILE))
14
+ # URL 리스트만 반환
15
  if isinstance(raw, list):
16
+ return [u if isinstance(u, str) else u.get("url") for u in raw]
 
 
 
 
 
 
17
  return []
18
  except Exception as e:
19
+ print("BEST 로드 오류:", e)
20
  return []
21
 
22
+ def _save_best(lst): # URL 리스트 저장
23
  try:
24
+ json.dump(lst, open(BEST_FILE, "w"), ensure_ascii=False, indent=2)
25
  return True
26
  except Exception as e:
27
+ print("BEST 저장 오류:", e)
28
  return False
29
 
30
+ def add_url_to_best(url: str):
31
+ data = _load_best()
32
+ if url in data:
 
 
 
 
 
 
 
 
33
  return False
34
+ data.insert(0, url)
35
+ return _save_best(data)
36
 
37
+ # ───────────────────── 3. 유틸 ──────────────────────────
38
  def page(lst, pg):
39
+ s, e = (pg-1)*PER_PAGE, (pg-1)*PER_PAGE+PER_PAGE
40
+ total = (len(lst)+PER_PAGE-1)//PER_PAGE
 
41
  return lst[s:e], total
42
 
 
43
  def process_url_for_iframe(url):
44
+ # Hugging Face Spaces embed 우선
 
45
  if "huggingface.co/spaces" in url:
46
+ owner, name = url.rstrip("/").split("/spaces/")[1].split("/")[:2]
47
+ return f"https://huggingface.co/spaces/{owner}/{name}/embed", True, []
48
+ return url, False, []
49
+
50
+ # ───────────────────── 4. 카드 그리드 HTML ───────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  def html(urls, pg, total):
52
  if not urls:
53
+ return '<div style="text-align:center;padding:70px;color:#555;">표시할 배포가 ��습니다.</div>'
54
 
55
  css = r"""
56
  <style>
 
58
  .container{padding:16px;}
59
  .grid{
60
  display:grid;
61
+ grid-template-columns:repeat(auto-fill,minmax(340px,1fr));
62
  gap:16px;
63
  }
64
  .card{
65
+ background:#fff;border-radius:10px;overflow:hidden;
66
+ box-shadow:0 2px 10px rgba(0,0,0,.08);
67
+ height:480px; /* ★ 기본 카드 높이 ↑ */
 
68
  transition:transform .2s;
69
  }
70
+ @media (min-width:1400px){.card{height:640px;} }/* ★ 아주 큰 화면 */
71
+ @media (max-width:992px) {.card{height:420px;} }/* 태블릿 */
72
+ @media (max-width:600px) {.card{height:360px;} }/* 모바일 */
73
+
74
  .card:hover{transform:translateY(-4px);}
75
+ .frame{position:relative;width:100%;height:100%;}
76
  .frame iframe{position:absolute;inset:0;width:100%;height:100%;border:none;}
 
77
  .page-info{text-align:center;color:#777;margin:12px 0;}
78
  </style>"""
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  body = '<div class="container"><div class="grid">'
81
+ for i, origin in enumerate(urls):
82
+ src, is_hf, _ = process_url_for_iframe(origin)
 
 
 
83
  body += f"""
84
  <div class="card">
85
+ <div class="frame{' huggingface' if is_hf else ''}">
86
+ <iframe src="{src}" loading="lazy"></iframe>
87
  </div>
88
  </div>"""
89
  body += "</div></div>"
90
  page_info = f'<div class="page-info">Page {pg} / {total}</div>'
91
+ return css + body + page_info
92
 
93
+ # ───────────────────── 5. Gradio UI ─────────────────────
94
  def build():
95
  _init_best()
96
 
97
+ header = """
98
  <style>
99
+ .app-header{position:sticky;top:0;text-align:center;background:#fff;
100
+ padding:16px 0 8px;border-bottom:1px solid #eee;z-index:1100;}
101
  .badge-row{display:inline-flex;gap:8px;margin:8px 0;}
102
  </style>
103
  <div class="app-header">
104
  <h1 style="margin:0;font-size:28px;">🎮 Vibe Game Gallery</h1>
105
  <div class="badge-row">
106
+ <a href="https://huggingface.co/spaces/openfree/Vibe-Game" target="_blank">
107
+ <img src="https://img.shields.io/static/v1?label=huggingface&message=Vibe%20Game%20Craft&color=800080&labelColor=ffa500&logo=huggingface&logoColor=ffff00&style=for-the-badge">
108
+ </a>
109
+ <a href="https://huggingface.co/spaces/openfree/Game-Gallery" target="_blank">
110
+ <img src="https://img.shields.io/static/v1?label=huggingface&message=Game%20Gallery&color=800080&labelColor=ffa500&logo=huggingface&logoColor=ffff00&style=for-the-badge">
111
+ </a>
112
+ <a href="https://discord.gg/openfreeai" target="_blank">
113
+ <img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=0000ff&labelColor=800080&logo=discord&logoColor=white&style=for-the-badge">
114
+ </a>
115
  </div>
116
+ </div>"""
117
+
118
+ global_css = """
119
+ footer{display:none !important;}
120
+ .button-row{position:fixed;bottom:0;left:0;right:0;height:60px;
121
+ background:#f0f0f0;padding:10px;text-align:center;
122
+ box-shadow:0 -2px 10px rgba(0,0,0,.05);z-index:1000;}
123
+ .button-row button{margin:0 10px;padding:10px 20px;font-size:16px;font-weight:bold;border-radius:50px;}
124
+ #content-area{overflow-y:auto;height:calc(100vh - 60px - 120px);}
125
  """
126
 
127
+ with gr.Blocks(title="Vibe Game Gallery", css=global_css) as demo:
128
+ gr.HTML(header)
129
  out = gr.HTML(elem_id="content-area")
130
  with gr.Row(elem_classes="button-row"):
131
  b_prev = gr.Button("◀ 이전", size="lg")
 
133
 
134
  bp = gr.State(1)
135
 
136
+ def render(p=1):
137
+ data, tot = page(_load_best(), p)
138
+ return html(data, p, tot), p
 
 
 
 
 
139
 
140
+ b_prev.click(lambda p: render(max(1, p-1)), inputs=bp, outputs=[out, bp])
141
+ b_next.click(lambda p: render(p+1), inputs=bp, outputs=[out, bp])
 
 
 
142
 
143
+ demo.load(render, outputs=[out, bp])
 
 
144
 
145
  return demo
146