WACATW commited on
Commit
69306e8
·
verified ·
1 Parent(s): 1a0cd9f

Delete cloud-browser

Browse files
cloud-browser/.env.example DELETED
@@ -1,7 +0,0 @@
1
- # 浏览器配置
2
- VIEWPORT_WIDTH=1280
3
- VIEWPORT_HEIGHT=800
4
- DEFAULT_URL=https://www.google.com
5
-
6
- # 服务配置
7
- PORT=7860
 
 
 
 
 
 
 
 
cloud-browser/Dockerfile DELETED
@@ -1,26 +0,0 @@
1
- FROM mcr.microsoft.com/playwright/python:v1.40.0-jammy
2
-
3
- WORKDIR /app
4
-
5
- # Install dependencies
6
- COPY requirements.txt .
7
- RUN pip install --no-cache-dir -r requirements.txt
8
-
9
- # Install browsers
10
- RUN playwright install chromium
11
- RUN playwright install-deps chromium
12
-
13
- # Copy application
14
- COPY . .
15
-
16
- # Environment variables
17
- ENV PORT=7860
18
- ENV VIEWPORT_WIDTH=1280
19
- ENV VIEWPORT_HEIGHT=800
20
- ENV DEFAULT_URL=https://www.google.com
21
-
22
- # Expose port
23
- EXPOSE 7860
24
-
25
- # Run the application
26
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cloud-browser/README.md DELETED
@@ -1,133 +0,0 @@
1
- # 云浏览器 - Cloud Browser
2
-
3
- 一个基于 Hugging Face Spaces 的云浏览器应用,在服务器端运行浏览器,用户本地只需要看画面和操作,就像云电脑一样。
4
-
5
- ## ✨ 功能特性
6
-
7
- - 🌐 **云端浏览器**:所有网页都在服务器端加载和渲染
8
- - 🖱️ **点击操作**:直接在画面上点击,就像操作真实浏览器
9
- - ⌨️ **键盘输入**:点击画面后直接打字,文字输入到云端浏览器
10
- - 📜 **滚动页面**:使用鼠标滚轮在画面上滚动
11
- - 🎮 **玩网页游戏**:输入游戏网址,在云端运行,本地不占资源
12
- - 🎨 **科技感界面**:深色主题,渐变设计,现代化 UI
13
- - 📊 **实时状态**:显示当前网址、页面标题、延迟等信息
14
-
15
- ## 🚀 部署到 Hugging Face Spaces
16
-
17
- ### 方法:Docker 部署
18
-
19
- 1. 在 Hugging Face 创建一个新的 Space,选择 **Docker** 作为 SDK
20
- 2. 将本项目的所有文件上传到 Space 仓库
21
- 3. 等待部署完成(首次部署会比较慢,需要下载浏览器)
22
- 4. 访问你的 Space 即可使用
23
-
24
- ### 注意事项
25
-
26
- - 首次启动需要下载 Chromium 浏览器,可能需要 1-2 分钟
27
- - 建议使用至少 2GB 内存的 Space 配置
28
- - 浏览器是单实例的,所有用户共享同一个浏览器会话
29
-
30
- ## 💻 本地运行
31
-
32
- ```bash
33
- # 安装依赖
34
- pip install -r requirements.txt
35
-
36
- # 安装浏览器
37
- playwright install chromium
38
- playwright install-deps chromium
39
-
40
- # 运行
41
- python app.py
42
- ```
43
-
44
- 访问 http://localhost:7860 即可使用。
45
-
46
- ## ⚙️ 环境变量配置
47
-
48
- | 变量名 | 说明 | 默认值 |
49
- |--------|------|--------|
50
- | `PORT` | 服务端口 | 7860 |
51
- | `VIEWPORT_WIDTH` | 浏览器视口宽度 | 1280 |
52
- | `VIEWPORT_HEIGHT` | 浏览器视口高度 | 800 |
53
- | `DEFAULT_URL` | 默认打开的网址 | https://www.google.com |
54
-
55
- ## 🎯 使用说明
56
-
57
- ### 访问网页
58
- 1. 在地址栏输入网址(例如 google.com)
59
- 2. 按回车或点击前往按钮
60
- 3. 等待页面加载完成
61
-
62
- ### 点击操作
63
- - 直接在浏览器画面上点击
64
- - 点击位置会精确映射到云端浏览器
65
-
66
- ### 键盘输入
67
- 1. 点击浏览器画面获得焦点
68
- 2. 直接打字,文字会输入到云端浏览器
69
- 3. 支持 Enter、Backspace、方向键等特殊按键
70
-
71
- ### 滚动页面
72
- - 使用鼠标滚轮在画面上滚动
73
- - 滚动会同步到云端浏览器
74
-
75
- ### 浏览器控制
76
- - 后退按钮:返回上一页
77
- - 前进按钮:前进到下一页
78
- - 刷新按钮:重新加载当前页面
79
-
80
- ## 🏗️ 技术栈
81
-
82
- - **后端**:Flask + Playwright (Python)
83
- - **前端**:HTML + CSS + JavaScript
84
- - **浏览器引擎**:Chromium (Playwright 驱动)
85
- - **部署**:Docker + Hugging Face Spaces
86
-
87
- ## ⚠️ 注意事项
88
-
89
- ### 延迟问题
90
- - 每次操作都需要发送请求到服务器并等待截图返回
91
- - 会有一定的延迟(通常 100-500ms)
92
- - 不适合需要快速反应的游戏
93
-
94
- ### 资源消耗
95
- - 浏览器运行会占用较多内存
96
- - Hugging Face Spaces 免费版可能会有限制
97
-
98
- ### 安全性
99
- - 用户可以访问任意网站
100
- - 所有用户共享同一个浏览器实例
101
- - 不建议在公共 Space 中登录重要账号
102
-
103
- ### 音频问题
104
- - 当前版本不支持音频传输
105
- - 网页视频和游戏没有声音
106
-
107
- ## 🔧 工作原理
108
-
109
- ```
110
- 用户操作 → 发送到服务器 → Playwright 执行 → 截图 → 返回给用户显示
111
- ```
112
-
113
- 1. 服务器端运行无头 Chromium 浏览器
114
- 2. 用户在前端点击/输入/滚动
115
- 3. 操作通过 HTTP 请求发送到后端
116
- 4. Playwright 在浏览器中模拟执行操作
117
- 5. 截取当前页面截图
118
- 6. 将截图返回给前端显示
119
-
120
- ## 📝 改进方向
121
-
122
- - [ ] 使用 WebSocket 替代 HTTP,降低延迟
123
- - [ ] 支持多用户隔离(每个用户独立浏览器)
124
- - [ ] 添加音频传输支持
125
- - [ ] 优化截图传输(增量更新、视频流等)
126
- - [ ] 添加书签/历史记录功能
127
- - [ ] 支持调整视口大小
128
- - [ ] 添加密码保护
129
- - [ ] 支持文件上传下载
130
-
131
- ## 📄 License
132
-
133
- MIT License
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cloud-browser/app.py DELETED
@@ -1,317 +0,0 @@
1
- import os
2
- import io
3
- import base64
4
- import time
5
- from flask import Flask, render_template, request, jsonify, send_file
6
- from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
7
-
8
- app = Flask(__name__)
9
-
10
- # 浏览器实例管理
11
- browser_instance = None
12
- page_instance = None
13
- playwright_instance = None
14
-
15
- # 配置
16
- VIEWPORT_WIDTH = int(os.environ.get('VIEWPORT_WIDTH', 1280))
17
- VIEWPORT_HEIGHT = int(os.environ.get('VIEWPORT_HEIGHT', 800))
18
- DEFAULT_URL = os.environ.get('DEFAULT_URL', 'https://www.google.com')
19
-
20
-
21
- def get_browser():
22
- """获取或创建浏览器实例"""
23
- global browser_instance, page_instance, playwright_instance
24
-
25
- if browser_instance is None:
26
- playwright_instance = sync_playwright().start()
27
- browser_instance = playwright_instance.chromium.launch(
28
- headless=True,
29
- args=[
30
- '--no-sandbox',
31
- '--disable-dev-shm-usage',
32
- '--disable-gpu',
33
- '--disable-extensions',
34
- '--disable-web-security',
35
- '--disable-features=IsolateOrigins,site-per-process',
36
- ]
37
- )
38
- context = browser_instance.new_context(
39
- viewport={'width': VIEWPORT_WIDTH, 'height': VIEWPORT_HEIGHT},
40
- user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
41
- )
42
- page_instance = context.new_page()
43
- page_instance.goto(DEFAULT_URL, timeout=30000, wait_until='domcontentloaded')
44
-
45
- return page_instance
46
-
47
-
48
- def take_screenshot():
49
- """截取当前页面截图"""
50
- page = get_browser()
51
- try:
52
- screenshot = page.screenshot(
53
- type='jpeg',
54
- quality=80,
55
- full_page=False
56
- )
57
- return base64.b64encode(screenshot).decode('utf-8')
58
- except Exception as e:
59
- print(f"Screenshot error: {e}")
60
- return None
61
-
62
-
63
- @app.route('/')
64
- def index():
65
- """主页"""
66
- return render_template('index.html',
67
- viewport_width=VIEWPORT_WIDTH,
68
- viewport_height=VIEWPORT_HEIGHT)
69
-
70
-
71
- @app.route('/api/navigate', methods=['POST'])
72
- def navigate():
73
- """导航到指定网址"""
74
- data = request.get_json()
75
- url = data.get('url', '')
76
-
77
- if not url:
78
- return jsonify({'success': False, 'error': '请输入网址'})
79
-
80
- # 自动添加 https://
81
- if not url.startswith('http://') and not url.startswith('https://'):
82
- url = 'https://' + url
83
-
84
- try:
85
- page = get_browser()
86
- page.goto(url, timeout=30000, wait_until='domcontentloaded')
87
- time.sleep(0.5) # 等待页面渲染
88
-
89
- screenshot = take_screenshot()
90
- current_url = page.url
91
- title = page.title()
92
-
93
- return jsonify({
94
- 'success': True,
95
- 'screenshot': screenshot,
96
- 'url': current_url,
97
- 'title': title
98
- })
99
- except PlaywrightTimeoutError:
100
- return jsonify({'success': False, 'error': '页面加载超时'})
101
- except Exception as e:
102
- return jsonify({'success': False, 'error': str(e)})
103
-
104
-
105
- @app.route('/api/screenshot')
106
- def screenshot():
107
- """获取当前页面截图"""
108
- try:
109
- page = get_browser()
110
- screenshot = take_screenshot()
111
- current_url = page.url
112
- title = page.title()
113
-
114
- return jsonify({
115
- 'success': True,
116
- 'screenshot': screenshot,
117
- 'url': current_url,
118
- 'title': title
119
- })
120
- except Exception as e:
121
- return jsonify({'success': False, 'error': str(e)})
122
-
123
-
124
- @app.route('/api/click', methods=['POST'])
125
- def click():
126
- """处理点击事件"""
127
- data = request.get_json()
128
- x = data.get('x', 0)
129
- y = data.get('y', 0)
130
-
131
- try:
132
- page = get_browser()
133
- # 点击指定坐标
134
- page.mouse.click(x, y)
135
- time.sleep(0.3) # 等待页面响应
136
-
137
- screenshot = take_screenshot()
138
- current_url = page.url
139
- title = page.title()
140
-
141
- return jsonify({
142
- 'success': True,
143
- 'screenshot': screenshot,
144
- 'url': current_url,
145
- 'title': title
146
- })
147
- except Exception as e:
148
- return jsonify({'success': False, 'error': str(e)})
149
-
150
-
151
- @app.route('/api/type', methods=['POST'])
152
- def type_text():
153
- """处理键盘输入"""
154
- data = request.get_json()
155
- text = data.get('text', '')
156
-
157
- try:
158
- page = get_browser()
159
- page.keyboard.type(text)
160
- time.sleep(0.2)
161
-
162
- screenshot = take_screenshot()
163
- return jsonify({
164
- 'success': True,
165
- 'screenshot': screenshot
166
- })
167
- except Exception as e:
168
- return jsonify({'success': False, 'error': str(e)})
169
-
170
-
171
- @app.route('/api/keypress', methods=['POST'])
172
- def keypress():
173
- """处理特殊按键"""
174
- data = request.get_json()
175
- key = data.get('key', '')
176
-
177
- try:
178
- page = get_browser()
179
-
180
- # 处理特殊按键
181
- key_map = {
182
- 'Enter': 'Enter',
183
- 'Backspace': 'Backspace',
184
- 'Tab': 'Tab',
185
- 'Escape': 'Escape',
186
- 'ArrowUp': 'ArrowUp',
187
- 'ArrowDown': 'ArrowDown',
188
- 'ArrowLeft': 'ArrowLeft',
189
- 'ArrowRight': 'ArrowRight',
190
- ' ': 'Space',
191
- }
192
-
193
- if key in key_map:
194
- page.keyboard.press(key_map[key])
195
- elif len(key) == 1:
196
- page.keyboard.press(key)
197
-
198
- time.sleep(0.2)
199
- screenshot = take_screenshot()
200
-
201
- return jsonify({
202
- 'success': True,
203
- 'screenshot': screenshot
204
- })
205
- except Exception as e:
206
- return jsonify({'success': False, 'error': str(e)})
207
-
208
-
209
- @app.route('/api/scroll', methods=['POST'])
210
- def scroll():
211
- """处理滚动"""
212
- data = request.get_json()
213
- delta_x = data.get('deltaX', 0)
214
- delta_y = data.get('deltaY', 0)
215
-
216
- try:
217
- page = get_browser()
218
- page.mouse.wheel(delta_x, delta_y)
219
- time.sleep(0.2)
220
-
221
- screenshot = take_screenshot()
222
- return jsonify({
223
- 'success': True,
224
- 'screenshot': screenshot
225
- })
226
- except Exception as e:
227
- return jsonify({'success': False, 'error': str(e)})
228
-
229
-
230
- @app.route('/api/go-back', methods=['POST'])
231
- def go_back():
232
- """后退"""
233
- try:
234
- page = get_browser()
235
- page.go_back()
236
- time.sleep(0.5)
237
-
238
- screenshot = take_screenshot()
239
- current_url = page.url
240
- title = page.title()
241
-
242
- return jsonify({
243
- 'success': True,
244
- 'screenshot': screenshot,
245
- 'url': current_url,
246
- 'title': title
247
- })
248
- except Exception as e:
249
- return jsonify({'success': False, 'error': str(e)})
250
-
251
-
252
- @app.route('/api/go-forward', methods=['POST'])
253
- def go_forward():
254
- """前进"""
255
- try:
256
- page = get_browser()
257
- page.go_forward()
258
- time.sleep(0.5)
259
-
260
- screenshot = take_screenshot()
261
- current_url = page.url
262
- title = page.title()
263
-
264
- return jsonify({
265
- 'success': True,
266
- 'screenshot': screenshot,
267
- 'url': current_url,
268
- 'title': title
269
- })
270
- except Exception as e:
271
- return jsonify({'success': False, 'error': str(e)})
272
-
273
-
274
- @app.route('/api/reload', methods=['POST'])
275
- def reload():
276
- """刷新"""
277
- try:
278
- page = get_browser()
279
- page.reload()
280
- time.sleep(0.5)
281
-
282
- screenshot = take_screenshot()
283
- current_url = page.url
284
- title = page.title()
285
-
286
- return jsonify({
287
- 'success': True,
288
- 'screenshot': screenshot,
289
- 'url': current_url,
290
- 'title': title
291
- })
292
- except Exception as e:
293
- return jsonify({'success': False, 'error': str(e)})
294
-
295
-
296
- @app.route('/api/status')
297
- def status():
298
- """获取浏览器状态"""
299
- try:
300
- page = get_browser()
301
- return jsonify({
302
- 'success': True,
303
- 'ready': True,
304
- 'url': page.url,
305
- 'title': page.title()
306
- })
307
- except Exception as e:
308
- return jsonify({
309
- 'success': True,
310
- 'ready': False,
311
- 'error': str(e)
312
- })
313
-
314
-
315
- if __name__ == '__main__':
316
- port = int(os.environ.get('PORT', 7860))
317
- app.run(host='0.0.0.0', port=port, debug=False, threaded=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cloud-browser/requirements.txt DELETED
@@ -1,3 +0,0 @@
1
- Flask==3.0.0
2
- playwright==1.40.0
3
- Werkzeug==3.0.1
 
 
 
 
cloud-browser/static/css/style.css DELETED
@@ -1,427 +0,0 @@
1
- * {
2
- margin: 0;
3
- padding: 0;
4
- box-sizing: border-box;
5
- }
6
-
7
- body {
8
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
9
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
10
- min-height: 100vh;
11
- color: #fff;
12
- }
13
-
14
- .container {
15
- max-width: 1400px;
16
- margin: 0 auto;
17
- padding: 20px;
18
- }
19
-
20
- /* Header */
21
- .header {
22
- background: rgba(255, 255, 255, 0.05);
23
- backdrop-filter: blur(10px);
24
- border-radius: 16px;
25
- padding: 20px 24px;
26
- display: flex;
27
- justify-content: space-between;
28
- align-items: center;
29
- border: 1px solid rgba(255, 255, 255, 0.1);
30
- margin-bottom: 16px;
31
- }
32
-
33
- .header-left {
34
- display: flex;
35
- align-items: center;
36
- gap: 12px;
37
- }
38
-
39
- .header-icon {
40
- font-size: 32px;
41
- background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
42
- -webkit-background-clip: text;
43
- -webkit-text-fill-color: transparent;
44
- background-clip: text;
45
- }
46
-
47
- .header-title {
48
- font-size: 24px;
49
- font-weight: 700;
50
- background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
51
- -webkit-background-clip: text;
52
- -webkit-text-fill-color: transparent;
53
- background-clip: text;
54
- }
55
-
56
- .badge {
57
- background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
58
- color: white;
59
- padding: 6px 12px;
60
- border-radius: 20px;
61
- font-size: 12px;
62
- display: flex;
63
- align-items: center;
64
- gap: 6px;
65
- font-weight: 500;
66
- }
67
-
68
- /* Status Indicator */
69
- .status-indicator {
70
- display: flex;
71
- align-items: center;
72
- gap: 8px;
73
- padding: 8px 16px;
74
- background: rgba(255, 255, 255, 0.05);
75
- border-radius: 20px;
76
- font-size: 14px;
77
- }
78
-
79
- .status-dot {
80
- width: 8px;
81
- height: 8px;
82
- border-radius: 50%;
83
- background: #ffc107;
84
- animation: pulse 2s infinite;
85
- }
86
-
87
- .status-dot.connected {
88
- background: #4caf50;
89
- }
90
-
91
- .status-dot.error {
92
- background: #f44336;
93
- }
94
-
95
- @keyframes pulse {
96
- 0%, 100% { opacity: 1; }
97
- 50% { opacity: 0.5; }
98
- }
99
-
100
- /* Browser Toolbar */
101
- .browser-toolbar {
102
- background: rgba(255, 255, 255, 0.05);
103
- backdrop-filter: blur(10px);
104
- border-radius: 12px;
105
- padding: 12px 16px;
106
- display: flex;
107
- align-items: center;
108
- gap: 12px;
109
- border: 1px solid rgba(255, 255, 255, 0.1);
110
- margin-bottom: 16px;
111
- }
112
-
113
- .nav-buttons {
114
- display: flex;
115
- gap: 8px;
116
- }
117
-
118
- .nav-btn {
119
- width: 36px;
120
- height: 36px;
121
- border: none;
122
- border-radius: 8px;
123
- background: rgba(255, 255, 255, 0.1);
124
- color: #fff;
125
- cursor: pointer;
126
- transition: all 0.3s ease;
127
- display: flex;
128
- align-items: center;
129
- justify-content: center;
130
- }
131
-
132
- .nav-btn:hover {
133
- background: rgba(255, 255, 255, 0.2);
134
- transform: scale(1.05);
135
- }
136
-
137
- .nav-btn:active {
138
- transform: scale(0.95);
139
- }
140
-
141
- /* URL Bar */
142
- .url-bar {
143
- flex: 1;
144
- display: flex;
145
- align-items: center;
146
- background: rgba(255, 255, 255, 0.1);
147
- border-radius: 10px;
148
- padding: 4px 4px 4px 12px;
149
- border: 1px solid rgba(255, 255, 255, 0.1);
150
- transition: all 0.3s ease;
151
- }
152
-
153
- .url-bar:focus-within {
154
- border-color: rgba(0, 212, 255, 0.5);
155
- box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
156
- }
157
-
158
- .url-icon {
159
- color: #4caf50;
160
- margin-right: 10px;
161
- font-size: 14px;
162
- }
163
-
164
- .url-bar input {
165
- flex: 1;
166
- background: transparent;
167
- border: none;
168
- color: #fff;
169
- font-size: 14px;
170
- outline: none;
171
- padding: 8px 0;
172
- }
173
-
174
- .url-bar input::placeholder {
175
- color: rgba(255, 255, 255, 0.4);
176
- }
177
-
178
- .go-btn {
179
- width: 36px;
180
- height: 36px;
181
- border: none;
182
- border-radius: 8px;
183
- background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
184
- color: white;
185
- cursor: pointer;
186
- transition: all 0.3s ease;
187
- display: flex;
188
- align-items: center;
189
- justify-content: center;
190
- }
191
-
192
- .go-btn:hover {
193
- transform: scale(1.05);
194
- box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
195
- }
196
-
197
- /* Browser Viewport */
198
- .browser-viewport {
199
- position: relative;
200
- background: #000;
201
- border-radius: 12px;
202
- overflow: hidden;
203
- border: 2px solid rgba(255, 255, 255, 0.1);
204
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
205
- aspect-ratio: 1280 / 800;
206
- max-height: 70vh;
207
- cursor: crosshair;
208
- }
209
-
210
- .screenshot-container {
211
- width: 100%;
212
- height: 100%;
213
- display: flex;
214
- align-items: center;
215
- justify-content: center;
216
- }
217
-
218
- .screenshot-img {
219
- width: 100%;
220
- height: 100%;
221
- object-fit: contain;
222
- display: block;
223
- }
224
-
225
- /* Loading Overlay */
226
- .loading-overlay {
227
- position: absolute;
228
- top: 0;
229
- left: 0;
230
- width: 100%;
231
- height: 100%;
232
- background: rgba(0, 0, 0, 0.8);
233
- display: flex;
234
- align-items: center;
235
- justify-content: center;
236
- z-index: 10;
237
- transition: opacity 0.3s ease;
238
- }
239
-
240
- .loading-overlay.hidden {
241
- opacity: 0;
242
- pointer-events: none;
243
- }
244
-
245
- .loading-content {
246
- text-align: center;
247
- }
248
-
249
- .loading-icon {
250
- font-size: 48px;
251
- color: #00d4ff;
252
- margin-bottom: 16px;
253
- }
254
-
255
- .loading-text {
256
- font-size: 16px;
257
- color: rgba(255, 255, 255, 0.8);
258
- }
259
-
260
- /* Keyboard Capture (hidden) */
261
- .keyboard-capture {
262
- position: absolute;
263
- opacity: 0;
264
- pointer-events: none;
265
- }
266
-
267
- /* Info Bar */
268
- .info-bar {
269
- background: rgba(255, 255, 255, 0.05);
270
- backdrop-filter: blur(10px);
271
- border-radius: 12px;
272
- padding: 12px 20px;
273
- display: flex;
274
- gap: 24px;
275
- border: 1px solid rgba(255, 255, 255, 0.1);
276
- margin-top: 16px;
277
- flex-wrap: wrap;
278
- }
279
-
280
- .info-item {
281
- display: flex;
282
- align-items: center;
283
- gap: 8px;
284
- font-size: 13px;
285
- color: rgba(255, 255, 255, 0.7);
286
- }
287
-
288
- .info-item i {
289
- color: #00d4ff;
290
- }
291
-
292
- .info-item span {
293
- max-width: 300px;
294
- overflow: hidden;
295
- text-overflow: ellipsis;
296
- white-space: nowrap;
297
- }
298
-
299
- /* Tips Section */
300
- .tips-section {
301
- margin-top: 24px;
302
- }
303
-
304
- .tips-section h3 {
305
- font-size: 18px;
306
- margin-bottom: 16px;
307
- display: flex;
308
- align-items: center;
309
- gap: 10px;
310
- color: rgba(255, 255, 255, 0.9);
311
- }
312
-
313
- .tips-section h3 i {
314
- color: #ffc107;
315
- }
316
-
317
- .tips-grid {
318
- display: grid;
319
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
320
- gap: 16px;
321
- }
322
-
323
- .tip-card {
324
- background: rgba(255, 255, 255, 0.05);
325
- backdrop-filter: blur(10px);
326
- border-radius: 12px;
327
- padding: 20px;
328
- border: 1px solid rgba(255, 255, 255, 0.1);
329
- transition: all 0.3s ease;
330
- }
331
-
332
- .tip-card:hover {
333
- background: rgba(255, 255, 255, 0.08);
334
- transform: translateY(-2px);
335
- border-color: rgba(0, 212, 255, 0.3);
336
- }
337
-
338
- .tip-icon {
339
- font-size: 28px;
340
- margin-bottom: 12px;
341
- background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
342
- -webkit-background-clip: text;
343
- -webkit-text-fill-color: transparent;
344
- background-clip: text;
345
- }
346
-
347
- .tip-card h4 {
348
- font-size: 16px;
349
- margin-bottom: 8px;
350
- color: #fff;
351
- }
352
-
353
- .tip-card p {
354
- font-size: 13px;
355
- color: rgba(255, 255, 255, 0.6);
356
- line-height: 1.5;
357
- }
358
-
359
- /* Toast */
360
- .toast {
361
- position: fixed;
362
- top: 20px;
363
- right: 20px;
364
- z-index: 2000;
365
- transform: translateX(120%);
366
- transition: transform 0.3s ease;
367
- }
368
-
369
- .toast.show {
370
- transform: translateX(0);
371
- }
372
-
373
- .toast-content {
374
- background: rgba(0, 0, 0, 0.9);
375
- backdrop-filter: blur(10px);
376
- padding: 14px 20px;
377
- border-radius: 12px;
378
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
379
- display: flex;
380
- align-items: center;
381
- gap: 10px;
382
- font-size: 14px;
383
- font-weight: 500;
384
- border: 1px solid rgba(255, 255, 255, 0.1);
385
- }
386
-
387
- .toast-icon.success {
388
- color: #4caf50;
389
- }
390
-
391
- .toast-icon.error {
392
- color: #f44336;
393
- }
394
-
395
- .toast-icon.info {
396
- color: #00d4ff;
397
- }
398
-
399
- /* Responsive */
400
- @media (max-width: 768px) {
401
- .container {
402
- padding: 10px;
403
- }
404
-
405
- .header {
406
- flex-direction: column;
407
- gap: 12px;
408
- padding: 16px;
409
- }
410
-
411
- .browser-toolbar {
412
- flex-wrap: wrap;
413
- }
414
-
415
- .info-bar {
416
- flex-direction: column;
417
- gap: 8px;
418
- }
419
-
420
- .info-item span {
421
- max-width: 100%;
422
- }
423
-
424
- .tips-grid {
425
- grid-template-columns: 1fr;
426
- }
427
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cloud-browser/static/js/app.js DELETED
@@ -1,438 +0,0 @@
1
- // DOM Elements
2
- const screenshotImg = document.getElementById('screenshotImg');
3
- const browserViewport = document.getElementById('browserViewport');
4
- const loadingOverlay = document.getElementById('loadingOverlay');
5
- const loadingText = document.getElementById('loadingText');
6
- const urlInput = document.getElementById('urlInput');
7
- const goBtn = document.getElementById('goBtn');
8
- const backBtn = document.getElementById('backBtn');
9
- const forwardBtn = document.getElementById('forwardBtn');
10
- const reloadBtn = document.getElementById('reloadBtn');
11
- const keyboardCapture = document.getElementById('keyboardCapture');
12
- const statusDot = document.querySelector('.status-dot');
13
- const statusText = document.getElementById('statusText');
14
- const currentUrlEl = document.getElementById('currentUrl');
15
- const pageTitleEl = document.getElementById('pageTitle');
16
- const latencyEl = document.getElementById('latency');
17
-
18
- // Toast
19
- const toast = document.getElementById('toast');
20
- const toastIcon = document.getElementById('toastIcon');
21
- const toastMessage = document.getElementById('toastMessage');
22
-
23
- // State
24
- let isLoading = false;
25
- let lastRequestTime = 0;
26
- let isBrowserReady = false;
27
- let autoRefreshInterval = null;
28
-
29
- // Toast function
30
- function showToast(message, type = 'success') {
31
- toastMessage.textContent = message;
32
- toastIcon.className = 'toast-icon fas ';
33
-
34
- if (type === 'success') {
35
- toastIcon.classList.add('fa-check-circle', 'success');
36
- } else if (type === 'error') {
37
- toastIcon.classList.add('fa-exclamation-circle', 'error');
38
- } else {
39
- toastIcon.classList.add('fa-info-circle', 'info');
40
- }
41
-
42
- toast.classList.add('show');
43
-
44
- setTimeout(() => {
45
- toast.classList.remove('show');
46
- }, 3000);
47
- }
48
-
49
- // Update status
50
- function updateStatus(status, text) {
51
- statusDot.className = 'status-dot';
52
- if (status === 'connected') {
53
- statusDot.classList.add('connected');
54
- } else if (status === 'error') {
55
- statusDot.classList.add('error');
56
- }
57
- statusText.textContent = text;
58
- }
59
-
60
- // Show loading
61
- function showLoading(text = '加载中...') {
62
- loadingText.textContent = text;
63
- loadingOverlay.classList.remove('hidden');
64
- isLoading = true;
65
- }
66
-
67
- // Hide loading
68
- function hideLoading() {
69
- loadingOverlay.classList.add('hidden');
70
- isLoading = false;
71
- }
72
-
73
- // Update screenshot
74
- function updateScreenshot(data) {
75
- if (data.screenshot) {
76
- screenshotImg.src = 'data:image/jpeg;base64,' + data.screenshot;
77
- }
78
- if (data.url) {
79
- currentUrlEl.textContent = data.url;
80
- urlInput.value = data.url;
81
- }
82
- if (data.title) {
83
- pageTitleEl.textContent = data.title;
84
- }
85
-
86
- // Calculate latency
87
- if (lastRequestTime > 0) {
88
- const latency = Date.now() - lastRequestTime;
89
- latencyEl.textContent = latency + 'ms';
90
- }
91
- }
92
-
93
- // Navigate to URL
94
- async function navigateTo(url) {
95
- if (isLoading) return;
96
-
97
- showLoading('正在加载页面...');
98
- lastRequestTime = Date.now();
99
-
100
- try {
101
- const response = await fetch('/api/navigate', {
102
- method: 'POST',
103
- headers: {
104
- 'Content-Type': 'application/json'
105
- },
106
- body: JSON.stringify({ url: url })
107
- });
108
-
109
- const data = await response.json();
110
-
111
- if (data.success) {
112
- updateScreenshot(data);
113
- hideLoading();
114
- showToast('页面加载成功', 'success');
115
- } else {
116
- hideLoading();
117
- showToast(data.error || '加载失败', 'error');
118
- }
119
- } catch (error) {
120
- hideLoading();
121
- showToast('网络错误: ' + error.message, 'error');
122
- console.error('Navigate error:', error);
123
- }
124
- }
125
-
126
- // Get screenshot
127
- async function refreshScreenshot() {
128
- if (isLoading) return;
129
-
130
- lastRequestTime = Date.now();
131
-
132
- try {
133
- const response = await fetch('/api/screenshot');
134
- const data = await response.json();
135
-
136
- if (data.success) {
137
- updateScreenshot(data);
138
- return true;
139
- }
140
- return false;
141
- } catch (error) {
142
- console.error('Screenshot error:', error);
143
- return false;
144
- }
145
- }
146
-
147
- // Handle click
148
- async function handleClick(x, y) {
149
- if (isLoading || !isBrowserReady) return;
150
-
151
- lastRequestTime = Date.now();
152
-
153
- try {
154
- const response = await fetch('/api/click', {
155
- method: 'POST',
156
- headers: {
157
- 'Content-Type': 'application/json'
158
- },
159
- body: JSON.stringify({ x: x, y: y })
160
- });
161
-
162
- const data = await response.json();
163
-
164
- if (data.success) {
165
- updateScreenshot(data);
166
- }
167
- } catch (error) {
168
- console.error('Click error:', error);
169
- }
170
- }
171
-
172
- // Handle type
173
- async function handleType(text) {
174
- if (isLoading || !isBrowserReady) return;
175
-
176
- lastRequestTime = Date.now();
177
-
178
- try {
179
- const response = await fetch('/api/type', {
180
- method: 'POST',
181
- headers: {
182
- 'Content-Type': 'application/json'
183
- },
184
- body: JSON.stringify({ text: text })
185
- });
186
-
187
- const data = await response.json();
188
-
189
- if (data.success) {
190
- updateScreenshot(data);
191
- }
192
- } catch (error) {
193
- console.error('Type error:', error);
194
- }
195
- }
196
-
197
- // Handle keypress
198
- async function handleKeypress(key) {
199
- if (isLoading || !isBrowserReady) return;
200
-
201
- lastRequestTime = Date.now();
202
-
203
- try {
204
- const response = await fetch('/api/keypress', {
205
- method: 'POST',
206
- headers: {
207
- 'Content-Type': 'application/json'
208
- },
209
- body: JSON.stringify({ key: key })
210
- });
211
-
212
- const data = await response.json();
213
-
214
- if (data.success) {
215
- updateScreenshot(data);
216
- }
217
- } catch (error) {
218
- console.error('Keypress error:', error);
219
- }
220
- }
221
-
222
- // Handle scroll
223
- async function handleScroll(deltaX, deltaY) {
224
- if (isLoading || !isBrowserReady) return;
225
-
226
- lastRequestTime = Date.now();
227
-
228
- try {
229
- const response = await fetch('/api/scroll', {
230
- method: 'POST',
231
- headers: {
232
- 'Content-Type': 'application/json'
233
- },
234
- body: JSON.stringify({ deltaX: deltaX, deltaY: deltaY })
235
- });
236
-
237
- const data = await response.json();
238
-
239
- if (data.success) {
240
- updateScreenshot(data);
241
- }
242
- } catch (error) {
243
- console.error('Scroll error:', error);
244
- }
245
- }
246
-
247
- // Navigation functions
248
- async function goBack() {
249
- if (isLoading) return;
250
-
251
- showLoading('后退中...');
252
- lastRequestTime = Date.now();
253
-
254
- try {
255
- const response = await fetch('/api/go-back', { method: 'POST' });
256
- const data = await response.json();
257
-
258
- if (data.success) {
259
- updateScreenshot(data);
260
- hideLoading();
261
- } else {
262
- hideLoading();
263
- showToast(data.error || '操作失败', 'error');
264
- }
265
- } catch (error) {
266
- hideLoading();
267
- showToast('网络错误', 'error');
268
- }
269
- }
270
-
271
- async function goForward() {
272
- if (isLoading) return;
273
-
274
- showLoading('前进中...');
275
- lastRequestTime = Date.now();
276
-
277
- try {
278
- const response = await fetch('/api/go-forward', { method: 'POST' });
279
- const data = await response.json();
280
-
281
- if (data.success) {
282
- updateScreenshot(data);
283
- hideLoading();
284
- } else {
285
- hideLoading();
286
- showToast(data.error || '操作失败', 'error');
287
- }
288
- } catch (error) {
289
- hideLoading();
290
- showToast('网络错误', 'error');
291
- }
292
- }
293
-
294
- async function reload() {
295
- if (isLoading) return;
296
-
297
- showLoading('刷新中...');
298
- lastRequestTime = Date.now();
299
-
300
- try {
301
- const response = await fetch('/api/reload', { method: 'POST' });
302
- const data = await response.json();
303
-
304
- if (data.success) {
305
- updateScreenshot(data);
306
- hideLoading();
307
- } else {
308
- hideLoading();
309
- showToast(data.error || '刷新失败', 'error');
310
- }
311
- } catch (error) {
312
- hideLoading();
313
- showToast('网络错误', 'error');
314
- }
315
- }
316
-
317
- // Calculate click coordinates
318
- function getClickCoordinates(event) {
319
- const rect = screenshotImg.getBoundingClientRect();
320
- const x = event.clientX - rect.left;
321
- const y = event.clientY - rect.top;
322
-
323
- // Calculate scale
324
- const scaleX = viewportWidth / rect.width;
325
- const scaleY = viewportHeight / rect.height;
326
-
327
- // Use the smaller scale to maintain aspect ratio
328
- const scale = Math.min(scaleX, scaleY);
329
-
330
- // Calculate actual coordinates in the viewport
331
- const actualX = Math.round(x * scale);
332
- const actualY = Math.round(y * scale);
333
-
334
- return { x: actualX, y: actualY };
335
- }
336
-
337
- // Event Listeners
338
-
339
- // URL input
340
- goBtn.addEventListener('click', () => {
341
- const url = urlInput.value.trim();
342
- if (url) {
343
- navigateTo(url);
344
- }
345
- });
346
-
347
- urlInput.addEventListener('keypress', (e) => {
348
- if (e.key === 'Enter') {
349
- const url = urlInput.value.trim();
350
- if (url) {
351
- navigateTo(url);
352
- }
353
- }
354
- });
355
-
356
- // Navigation buttons
357
- backBtn.addEventListener('click', goBack);
358
- forwardBtn.addEventListener('click', goForward);
359
- reloadBtn.addEventListener('click', reload);
360
-
361
- // Viewport click
362
- browserViewport.addEventListener('click', (e) => {
363
- if (!isBrowserReady) return;
364
-
365
- // Focus the hidden input to capture keyboard
366
- keyboardCapture.focus();
367
-
368
- const coords = getClickCoordinates(e);
369
- handleClick(coords.x, coords.y);
370
- });
371
-
372
- // Keyboard input
373
- keyboardCapture.addEventListener('input', (e) => {
374
- const text = e.target.value;
375
- if (text) {
376
- handleType(text);
377
- e.target.value = '';
378
- }
379
- });
380
-
381
- keyboardCapture.addEventListener('keydown', (e) => {
382
- // Handle special keys
383
- const specialKeys = ['Enter', 'Backspace', 'Tab', 'Escape', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
384
-
385
- if (specialKeys.includes(e.key)) {
386
- e.preventDefault();
387
- handleKeypress(e.key);
388
- }
389
- });
390
-
391
- // Scroll
392
- browserViewport.addEventListener('wheel', (e) => {
393
- e.preventDefault();
394
- handleScroll(e.deltaX, e.deltaY);
395
- }, { passive: false });
396
-
397
- // Initialize
398
- async function init() {
399
- updateStatus('connecting', '连接中...');
400
- showLoading('正在启动云浏览器...');
401
-
402
- // Check browser status
403
- try {
404
- const response = await fetch('/api/status');
405
- const data = await response.json();
406
-
407
- if (data.ready) {
408
- isBrowserReady = true;
409
- updateStatus('connected', '已连接');
410
- currentUrlEl.textContent = data.url || '-';
411
- pageTitleEl.textContent = data.title || '-';
412
-
413
- // Get initial screenshot
414
- await refreshScreenshot();
415
- hideLoading();
416
- } else {
417
- // Try to initialize by getting screenshot
418
- showLoading('正在初始化浏览器...');
419
- const success = await refreshScreenshot();
420
-
421
- if (success) {
422
- isBrowserReady = true;
423
- updateStatus('connected', '已连接');
424
- hideLoading();
425
- } else {
426
- updateStatus('error', '连接失败');
427
- loadingText.textContent = '浏览器启动失败,请刷新页面重试';
428
- }
429
- }
430
- } catch (error) {
431
- updateStatus('error', '连接失败');
432
- loadingText.textContent = '无法连接到服务器';
433
- console.error('Init error:', error);
434
- }
435
- }
436
-
437
- // Start
438
- document.addEventListener('DOMContentLoaded', init);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cloud-browser/templates/index.html DELETED
@@ -1,128 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>云浏览器 - Cloud Browser</title>
7
- <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
- </head>
10
- <body>
11
- <div class="container">
12
- <!-- Header -->
13
- <header class="header">
14
- <div class="header-left">
15
- <i class="fas fa-cloud header-icon"></i>
16
- <h1 class="header-title">云浏览器</h1>
17
- <span class="badge">
18
- <i class="fas fa-server"></i>
19
- 云端运行
20
- </span>
21
- </div>
22
- <div class="header-right">
23
- <span class="status-indicator" id="statusIndicator">
24
- <span class="status-dot"></span>
25
- <span class="status-text" id="statusText">连接中...</span>
26
- </span>
27
- </div>
28
- </header>
29
-
30
- <!-- Browser Toolbar -->
31
- <div class="browser-toolbar">
32
- <div class="nav-buttons">
33
- <button class="nav-btn" id="backBtn" title="后退">
34
- <i class="fas fa-arrow-left"></i>
35
- </button>
36
- <button class="nav-btn" id="forwardBtn" title="前进">
37
- <i class="fas fa-arrow-right"></i>
38
- </button>
39
- <button class="nav-btn" id="reloadBtn" title="刷新">
40
- <i class="fas fa-redo"></i>
41
- </button>
42
- </div>
43
-
44
- <div class="url-bar">
45
- <i class="fas fa-lock url-icon"></i>
46
- <input type="text" id="urlInput" placeholder="输入网址,例如 google.com" value="">
47
- <button class="go-btn" id="goBtn">
48
- <i class="fas fa-arrow-right"></i>
49
- </button>
50
- </div>
51
- </div>
52
-
53
- <!-- Browser Viewport -->
54
- <div class="browser-viewport" id="browserViewport">
55
- <div class="loading-overlay" id="loadingOverlay">
56
- <div class="loading-content">
57
- <i class="fas fa-spinner fa-spin loading-icon"></i>
58
- <p class="loading-text" id="loadingText">正在启动云浏览器...</p>
59
- </div>
60
- </div>
61
-
62
- <div class="screenshot-container" id="screenshotContainer">
63
- <img id="screenshotImg" class="screenshot-img" alt="Browser screenshot">
64
- </div>
65
-
66
- <!-- Hidden input for keyboard capture -->
67
- <input type="text" id="keyboardCapture" class="keyboard-capture" autocomplete="off">
68
- </div>
69
-
70
- <!-- Info Bar -->
71
- <div class="info-bar">
72
- <div class="info-item">
73
- <i class="fas fa-globe"></i>
74
- <span id="currentUrl">等待加载...</span>
75
- </div>
76
- <div class="info-item">
77
- <i class="fas fa-file-alt"></i>
78
- <span id="pageTitle">-</span>
79
- </div>
80
- <div class="info-item">
81
- <i class="fas fa-tachometer-alt"></i>
82
- <span id="latency">-</span>
83
- </div>
84
- </div>
85
-
86
- <!-- Tips -->
87
- <div class="tips-section">
88
- <h3><i class="fas fa-lightbulb"></i> 使用提示</h3>
89
- <div class="tips-grid">
90
- <div class="tip-card">
91
- <i class="fas fa-mouse-pointer tip-icon"></i>
92
- <h4>点击操作</h4>
93
- <p>直接在画面上点击,就像操作真实浏览器一样</p>
94
- </div>
95
- <div class="tip-card">
96
- <i class="fas fa-keyboard tip-icon"></i>
97
- <h4>键盘输入</h4>
98
- <p>点击画面后直接打字,文字会输入到云端浏览器</p>
99
- </div>
100
- <div class="tip-card">
101
- <i class="fas fa-mouse tip-icon"></i>
102
- <h4>滚动页面</h4>
103
- <p>使用鼠标滚轮在画面上滚动</p>
104
- </div>
105
- <div class="tip-card">
106
- <i class="fas fa-gamepad tip-icon"></i>
107
- <h4>玩网页游戏</h4>
108
- <p>输入游戏网址,在云端运行,本地不占资源</p>
109
- </div>
110
- </div>
111
- </div>
112
- </div>
113
-
114
- <!-- Toast -->
115
- <div class="toast" id="toast">
116
- <div class="toast-content">
117
- <i class="toast-icon" id="toastIcon"></i>
118
- <span id="toastMessage"></span>
119
- </div>
120
- </div>
121
-
122
- <script>
123
- const viewportWidth = {{ viewport_width }};
124
- const viewportHeight = {{ viewport_height }};
125
- </script>
126
- <script src="{{ url_for('static', filename='js/app.js') }}"></script>
127
- </body>
128
- </html>