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

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +215 -0
app.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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():
8
+ if not os.path.exists(BEST_FILE):
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>
94
+ body{margin:0;font-family:Poppins,sans-serif;background:#f4f5f7;}
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")
187
+ b_next = gr.Button("다음 ▶", size="lg")
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
+
212
+ app = build()
213
+
214
+ if __name__ == "__main__":
215
+ app.launch()