openfree commited on
Commit
9d8bac2
·
verified ·
1 Parent(s): 331a7a6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +191 -30
app.py CHANGED
@@ -1,35 +1,196 @@
1
- import os
2
- import sys
3
- import streamlit as st
4
- from tempfile import NamedTemporaryFile
5
 
6
- def main():
 
 
 
 
 
 
 
 
7
  try:
8
- # Get the code from secrets
9
- code = os.environ.get("MAIN_CODE")
10
-
11
- if not code:
12
- st.error("⚠️ The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
13
- return
14
-
15
- # Create a temporary Python file
16
- with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
17
- tmp.write(code)
18
- tmp_path = tmp.name
19
-
20
- # Execute the code
21
- exec(compile(code, tmp_path, 'exec'), globals())
22
-
23
- # Clean up the temporary file
24
- try:
25
- os.unlink(tmp_path)
26
- except:
27
- pass
28
-
29
  except Exception as e:
30
- st.error(f"⚠️ Error loading or executing the application: {str(e)}")
31
- import traceback
32
- st.code(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  if __name__ == "__main__":
35
- main()
 
1
+ import os, json, time, datetime, requests, gradio as gr, re
 
 
 
2
 
3
+ # ───────────────────── 1. 기본 설정 ─────────────────────
4
+ BEST_FILE, PER_PAGE = "best_games.json", 9 # ❶ 페이지당 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
  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
+ """
45
+ 반환: (iframe_url, extra_class, alternate_urls)
46
+ extra_class : '' | 'huggingface' | 'hfspace'
47
+ """
48
+ # Hugging Face Spaces embed (Gradio/Streamlit)
49
+ if "huggingface.co/spaces" in url:
50
+ owner, name = url.rstrip("/").split("/spaces/")[1].split("/")[:2]
51
+ return f"https://huggingface.co/spaces/{owner}/{name}/embed", "huggingface", []
52
+
53
+ # *.hf.space (정적/static Space 포함)
54
+ m = re.match(r"https?://([^/]+)\.hf\.space(/.*)?", url)
55
+ if m:
56
+ sub, rest = m.groups()
57
+ static_url = f"https://{sub}.static.hf.space{rest or ''}"
58
+ # alt_urls 에 원본 저장(실패 시 재시도 가능)
59
+ return static_url, "hfspace", [url]
60
+
61
+ return url, "", []
62
+
63
+ # ───────────────────── 6. HTML 그리드 ───────────────────
64
+ def html(cards, pg, total):
65
+ if not cards:
66
+ return "<div style='text-align:center;padding:70px;color:#555;'>표시할 배포가 없습니다.</div>"
67
+
68
+ css = r"""
69
+ <style>
70
+ /* 파스텔 그라디에이션 배경 */
71
+ body{
72
+ margin:0;padding:0;font-family:Poppins,sans-serif;
73
+ background:linear-gradient(135deg,#fdf4ff 0%,#f6fbff 50%,#fffaf4 100%);
74
+ background-attachment:fixed;
75
+ overflow-x:hidden;overflow-y:auto;
76
+ }
77
+ .container{width:100%;padding:10px 10px 70px;box-sizing:border-box;}
78
+ .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;width:100%;}
79
+ .card{
80
+ background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 4px 10px rgba(0,0,0,0.08);
81
+ height:420px;display:flex;flex-direction:column;position:relative;
82
+ }
83
+ /* 게임 화면 축소 */
84
+ .frame{flex:1;position:relative;overflow:hidden;}
85
+ .frame iframe{
86
+ position:absolute;top:0;left:0;
87
+ width:166.667%;height:166.667%;
88
+ transform:scale(0.6);transform-origin:top left;border:0;
89
+ }
90
+ /* Gradio/Streamlit Embed용 */
91
+ .frame.huggingface iframe{
92
+ width:100%!important;height:100%!important;
93
+ transform:none!important;border:none!important;
94
+ }
95
+ /* hf.space 전체 페이지용 - 한 번 더 축소 */
96
+ .frame.hfspace iframe{
97
+ width:200%;height:200%;
98
+ transform:scale(0.5);
99
+ transform-origin:top left;border:0;
100
+ }
101
+ /* 하단 바로가기 */
102
+ .foot{height:34px;display:flex;align-items:center;justify-content:center;background:#fafafa;border-top:1px solid #eee;}
103
+ .foot a{font-size:0.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;}
104
+ .foot a:hover{text-decoration:underline;}
105
+ /* 반응형 높이 */
106
+ @media(min-width:1200px){.card{height:560px;}}
107
+ @media(max-width:767px){
108
+ .grid{grid-template-columns:1fr;}
109
+ .card{height:480px;}
110
+ }
111
+ </style>"""
112
+
113
+ js = """
114
+ <script>
115
+ /* 허깅페이스 iframe 로딩 오류 처리(생략 - 기존 스크립트 그대로) */
116
+ </script>
117
+ """
118
+
119
+ h = css + js + '<div class="container"><div class="grid">'
120
+ for idx, url in enumerate(cards):
121
+ iframe_url, extra_cls, alt_urls = process_url_for_iframe(url)
122
+ frame_class = f"frame {extra_cls}".strip()
123
+ iframe_id = f"iframe-{idx}-{hash(url)%10000}"
124
+ alt_attr = f'data-alternate-urls="{",".join(alt_urls)}"' if alt_urls else ""
125
+ h += f"""
126
+ <div class="card">
127
+ <div class="{frame_class}">
128
+ <iframe id="{iframe_id}" src="{iframe_url}" loading="lazy"
129
+ sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-downloads"
130
+ data-original-url="{url}" {alt_attr}></iframe>
131
+ </div>
132
+ <div class="foot"><a href="{url}" target="_blank">↗ Open in Full Screen (New Tab)</a></div>
133
+ </div>"""
134
+ h += "</div></div>"
135
+ h += f'<div class="page-info">Page {pg} / {total}</div>'
136
+ return h
137
+
138
+
139
+ # ───────────────────── 5. Gradio UI ─────────────────────
140
+ def build():
141
+ _init_best()
142
+
143
+ header = """
144
+ <style>
145
+ .app-header{position:sticky;top:0;text-align:center;background:#fff;
146
+ padding:16px 0 8px;border-bottom:1px solid #eee;z-index:1100;}
147
+ .badge-row{display:inline-flex;gap:8px;margin:8px 0;}
148
+ </style>
149
+ <div class="app-header">
150
+ <h1 style="margin:0;font-size:28px;">🎮 Vibe Game Gallery</h1>
151
+ <div class="badge-row">
152
+ <a href="https://huggingface.co/spaces/openfree/Vibe-Game" target="_blank">
153
+ <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">
154
+ </a>
155
+ <a href="https://huggingface.co/spaces/openfree/Game-Gallery" target="_blank">
156
+ <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">
157
+ </a>
158
+ <a href="https://discord.gg/openfreeai" target="_blank">
159
+ <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">
160
+ </a>
161
+ </div>
162
+ </div>"""
163
+
164
+ global_css = """
165
+ footer{display:none !important;}
166
+ .button-row{position:fixed;bottom:0;left:0;right:0;height:60px;
167
+ background:#f0f0f0;padding:10px;text-align:center;
168
+ box-shadow:0 -2px 10px rgba(0,0,0,.05);z-index:1000;}
169
+ .button-row button{margin:0 10px;padding:10px 20px;font-size:16px;font-weight:bold;border-radius:50px;}
170
+ #content-area{overflow-y:auto;height:calc(100vh - 60px - 120px);}
171
+ """
172
+
173
+ with gr.Blocks(title="Vibe Game Gallery", css=global_css) as demo:
174
+ gr.HTML(header)
175
+ out = gr.HTML(elem_id="content-area")
176
+ with gr.Row(elem_classes="button-row"):
177
+ b_prev = gr.Button("◀ 이전", size="lg")
178
+ b_next = gr.Button("다음 ▶", size="lg")
179
+
180
+ bp = gr.State(1)
181
+
182
+ def render(p=1):
183
+ data, tot = page(_load_best(), p)
184
+ return html(data, p, tot), p
185
+
186
+ b_prev.click(lambda p: render(max(1, p-1)), inputs=bp, outputs=[out, bp])
187
+ b_next.click(lambda p: render(p+1), inputs=bp, outputs=[out, bp])
188
+
189
+ demo.load(render, outputs=[out, bp])
190
+
191
+ return demo
192
+
193
+ app = build()
194
 
195
  if __name__ == "__main__":
196
+ app.launch()