duqing2026 commited on
Commit
031a9c5
·
1 Parent(s): 8a93016

Initial commit: Code Snapshot Studio MVP

Browse files
Files changed (6) hide show
  1. .gitignore +6 -0
  2. Dockerfile +17 -0
  3. README.md +59 -4
  4. app.py +15 -0
  5. requirements.txt +2 -0
  6. templates/index.html +294 -0
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
4
+ .venv
5
+ env/
6
+ venv/
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ # Create a non-root user for Hugging Face Spaces
11
+ RUN useradd -m -u 1000 user
12
+ USER user
13
+
14
+ ENV HOME=/home/user \
15
+ PATH=/home/user/.local/bin:$PATH
16
+
17
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md CHANGED
@@ -1,11 +1,66 @@
1
  ---
2
  title: Code Snapshot Studio
3
- emoji: 💻
4
  colorFrom: indigo
5
- colorTo: blue
6
  sdk: docker
 
7
  pinned: false
8
- short_description: 穷
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Code Snapshot Studio
3
+ emoji: 📸
4
  colorFrom: indigo
5
+ colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
 
9
  ---
10
 
11
+ # Code Snapshot Studio (代码截图工坊)
12
+
13
+ Code Snapshot Studio 是一个专为开发者设计的代码截图生成工具,帮助你创建美观、高清晰度的代码图片,用于技术博客、社交媒体分享或演示文档。
14
+
15
+ ## ✨ 核心功能
16
+
17
+ - **多语言支持**:支持 Python, JavaScript, Go, Rust, Java 等多种主流编程语言的语法高亮。
18
+ - **主题切换**:内置 Tomorrow (Dark), Okaidia, Solarized Light, Twilight 等多种经典配色主题。
19
+ - **高度定制**:
20
+ - 调整内边距 (Padding)
21
+ - 切换背景风格 (渐变色/纯色)
22
+ - 窗口控件开关 (Mac 风格红绿灯)
23
+ - 行号显示开关
24
+ - 自定义窗口标题
25
+ - **高清导出**:基于 HTML2Canvas,一键导出 2x 分辨率的高清 PNG 图片。
26
+ - **实时预览**:所见即所得的编辑体验。
27
+
28
+ ## 🚀 快速开始
29
+
30
+ ### 本地运行
31
+
32
+ 1. 克隆仓库:
33
+ ```bash
34
+ git clone https://github.com/yourusername/code-snapshot-studio.git
35
+ cd code-snapshot-studio
36
+ ```
37
+
38
+ 2. 安装依赖:
39
+ ```bash
40
+ pip install -r requirements.txt
41
+ ```
42
+
43
+ 3. 启动应用:
44
+ ```bash
45
+ python app.py
46
+ ```
47
+
48
+ 4. 打开浏览器访问:`http://localhost:7860`
49
+
50
+ ### Docker 部署
51
+
52
+ ```bash
53
+ docker build -t code-snapshot-studio .
54
+ docker run -p 7860:7860 code-snapshot-studio
55
+ ```
56
+
57
+ ## 🛠️ 技术栈
58
+
59
+ - **Backend**: Flask (Python)
60
+ - **Frontend**: Vue.js 3, Tailwind CSS
61
+ - **Syntax Highlighting**: Prism.js
62
+ - **Image Generation**: html2canvas
63
+
64
+ ## 📝 License
65
+
66
+ MIT License
app.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, send_from_directory
2
+ import os
3
+
4
+ app = Flask(__name__)
5
+
6
+ @app.route('/')
7
+ def index():
8
+ return render_template('index.html')
9
+
10
+ @app.route('/static/<path:path>')
11
+ def send_static(path):
12
+ return send_from_directory('static', path)
13
+
14
+ if __name__ == '__main__':
15
+ app.run(host='0.0.0.0', port=7860)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask
2
+ gunicorn
templates/index.html ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Code Snapshot Studio - 代码截图工坊</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
11
+ <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
12
+ <!-- Default Theme -->
13
+ <link id="prism-theme" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
14
+ <!-- Icons -->
15
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
16
+ <style>
17
+ body {
18
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
19
+ background-color: #f3f4f6;
20
+ }
21
+ .preview-container {
22
+ transition: all 0.3s ease;
23
+ overflow: hidden; /* Ensure content doesn't spill out */
24
+ }
25
+ /* Custom Scrollbar */
26
+ ::-webkit-scrollbar {
27
+ width: 8px;
28
+ height: 8px;
29
+ }
30
+ ::-webkit-scrollbar-track {
31
+ background: transparent;
32
+ }
33
+ ::-webkit-scrollbar-thumb {
34
+ background: #cbd5e1;
35
+ border-radius: 4px;
36
+ }
37
+ ::-webkit-scrollbar-thumb:hover {
38
+ background: #94a3b8;
39
+ }
40
+ .code-font {
41
+ font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body class="h-screen flex flex-col overflow-hidden">
46
+ <div id="app" class="flex-1 flex flex-col h-full">
47
+ <!-- Header -->
48
+ <header class="bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between shadow-sm z-10">
49
+ <div class="flex items-center gap-3">
50
+ <div class="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center text-white font-bold">
51
+ <i class="fa-solid fa-camera"></i>
52
+ </div>
53
+ <h1 class="text-xl font-bold text-gray-800 tracking-tight">Code Snapshot Studio</h1>
54
+ </div>
55
+ <div class="flex items-center gap-4">
56
+ <button @click="exportImage" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 shadow-sm">
57
+ <i class="fa-solid fa-download"></i>
58
+ <span>导出 PNG</span>
59
+ </button>
60
+ </div>
61
+ </header>
62
+
63
+ <!-- Main Content -->
64
+ <main class="flex-1 flex overflow-hidden">
65
+ <!-- Sidebar Controls -->
66
+ <aside class="w-80 bg-white border-r border-gray-200 overflow-y-auto p-6 flex flex-col gap-6 shadow-[4px_0_24px_rgba(0,0,0,0.02)] z-10">
67
+
68
+ <!-- Code Input -->
69
+ <div class="flex flex-col gap-2">
70
+ <label class="text-sm font-semibold text-gray-700">代码内容</label>
71
+ <textarea v-model="code" class="w-full h-48 p-3 border border-gray-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none resize-none bg-gray-50" placeholder="在此粘贴你的代码..."></textarea>
72
+ </div>
73
+
74
+ <!-- Language & Theme -->
75
+ <div class="grid grid-cols-2 gap-4">
76
+ <div class="flex flex-col gap-2">
77
+ <label class="text-sm font-semibold text-gray-700">语言</label>
78
+ <select v-model="language" @change="refreshHighlight" class="w-full p-2 border border-gray-300 rounded-lg text-sm bg-white focus:ring-2 focus:ring-indigo-500 outline-none">
79
+ <option value="javascript">JavaScript</option>
80
+ <option value="python">Python</option>
81
+ <option value="html">HTML</option>
82
+ <option value="css">CSS</option>
83
+ <option value="java">Java</option>
84
+ <option value="go">Go</option>
85
+ <option value="rust">Rust</option>
86
+ <option value="sql">SQL</option>
87
+ <option value="bash">Bash</option>
88
+ <option value="json">JSON</option>
89
+ <option value="typescript">TypeScript</option>
90
+ </select>
91
+ </div>
92
+ <div class="flex flex-col gap-2">
93
+ <label class="text-sm font-semibold text-gray-700">主题</label>
94
+ <select v-model="theme" @change="changeTheme" class="w-full p-2 border border-gray-300 rounded-lg text-sm bg-white focus:ring-2 focus:ring-indigo-500 outline-none">
95
+ <option value="tomorrow">Dark (Tomorrow)</option>
96
+ <option value="okaidia">Okaidia</option>
97
+ <option value="solarizedlight">Solarized Light</option>
98
+ <option value="twilight">Twilight</option>
99
+ <option value="default">Default</option>
100
+ </select>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Background -->
105
+ <div class="flex flex-col gap-2">
106
+ <label class="text-sm font-semibold text-gray-700">背景风格</label>
107
+ <div class="grid grid-cols-5 gap-2">
108
+ <button v-for="(bg, index) in backgrounds" :key="index"
109
+ @click="currentBg = bg"
110
+ :class="['w-8 h-8 rounded-full border-2 transition-all', currentBg.value === bg.value ? 'border-indigo-600 scale-110 shadow-md' : 'border-transparent hover:scale-105']"
111
+ :style="{ background: bg.value }">
112
+ </button>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Settings -->
117
+ <div class="space-y-4 border-t border-gray-100 pt-4">
118
+ <div class="flex items-center justify-between">
119
+ <label class="text-sm font-medium text-gray-700">窗口控件</label>
120
+ <input type="checkbox" v-model="showWindowControls" class="w-4 h-4 text-indigo-600 rounded focus:ring-indigo-500">
121
+ </div>
122
+ <div class="flex items-center justify-between">
123
+ <label class="text-sm font-medium text-gray-700">行号</label>
124
+ <input type="checkbox" v-model="showLineNumbers" class="w-4 h-4 text-indigo-600 rounded focus:ring-indigo-500">
125
+ </div>
126
+ <div class="flex items-center justify-between">
127
+ <label class="text-sm font-medium text-gray-700">窗口阴影</label>
128
+ <input type="checkbox" v-model="showShadow" class="w-4 h-4 text-indigo-600 rounded focus:ring-indigo-500">
129
+ </div>
130
+ <div class="flex flex-col gap-2">
131
+ <div class="flex justify-between">
132
+ <label class="text-sm font-medium text-gray-700">内边距 (Padding)</label>
133
+ <span class="text-xs text-gray-500">{{ padding }}px</span>
134
+ </div>
135
+ <input type="range" v-model="padding" min="16" max="128" step="8" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
136
+ </div>
137
+ <div class="flex flex-col gap-2">
138
+ <label class="text-sm font-medium text-gray-700">窗口标题</label>
139
+ <input type="text" v-model="windowTitle" class="w-full p-2 border border-gray-300 rounded-lg text-sm outline-none focus:ring-2 focus:ring-indigo-500" placeholder="例如: app.py">
140
+ </div>
141
+ </div>
142
+ </aside>
143
+
144
+ <!-- Preview Area -->
145
+ <section class="flex-1 bg-gray-100 flex items-center justify-center p-8 overflow-auto relative">
146
+ <!-- Grid Background Pattern -->
147
+ <div class="absolute inset-0 opacity-[0.03]"
148
+ style="background-image: radial-gradient(#000 1px, transparent 1px); background-size: 20px 20px;">
149
+ </div>
150
+
151
+ <!-- The Snapshot Card -->
152
+ <div ref="snapshotCard" class="preview-container relative flex items-center justify-center min-w-[400px]"
153
+ :style="{ background: currentBg.value, padding: padding + 'px' }">
154
+
155
+ <!-- Window -->
156
+ <div class="bg-[#1e1e1e] rounded-xl overflow-hidden min-w-[400px] max-w-4xl w-auto"
157
+ :class="{'shadow-2xl': showShadow, 'shadow-none': !showShadow}"
158
+ :style="{ backgroundColor: themeBgColor }">
159
+
160
+ <!-- Window Header -->
161
+ <div v-if="showWindowControls || windowTitle" class="px-4 py-3 flex items-center relative bg-white/5 border-b border-white/5">
162
+ <!-- Controls -->
163
+ <div v-if="showWindowControls" class="flex gap-2 absolute left-4">
164
+ <div class="w-3 h-3 rounded-full bg-[#ff5f56] border border-[#e0443e]"></div>
165
+ <div class="w-3 h-3 rounded-full bg-[#ffbd2e] border border-[#dea123]"></div>
166
+ <div class="w-3 h-3 rounded-full bg-[#27c93f] border border-[#1aab29]"></div>
167
+ </div>
168
+ <!-- Title -->
169
+ <div class="w-full text-center text-xs font-medium text-gray-400 font-sans select-none">
170
+ {{ windowTitle }}
171
+ </div>
172
+ </div>
173
+
174
+ <!-- Code Content -->
175
+ <div class="p-0 overflow-hidden">
176
+ <pre class="!m-0 !p-6 !bg-transparent text-sm leading-relaxed outline-none code-font"
177
+ :class="{'line-numbers': showLineNumbers}"><code :class="'language-' + language" ref="codeBlock">{{ code }}</code></pre>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </section>
182
+ </main>
183
+ </div>
184
+
185
+ <script>
186
+ const { createApp, ref, onMounted, watch, nextTick } = Vue;
187
+
188
+ createApp({
189
+ setup() {
190
+ const code = ref(`def hello_world():
191
+ print("Hello, Trae!")
192
+ return True
193
+
194
+ # Build something amazing
195
+ if __name__ == "__main__":
196
+ hello_world()`);
197
+ const language = ref('python');
198
+ const theme = ref('tomorrow');
199
+ const padding = ref(64);
200
+ const showWindowControls = ref(true);
201
+ const showLineNumbers = ref(true);
202
+ const showShadow = ref(true);
203
+ const windowTitle = ref('main.py');
204
+ const snapshotCard = ref(null);
205
+ const codeBlock = ref(null);
206
+
207
+ // Background presets
208
+ const backgrounds = [
209
+ { name: 'Gradient 1', value: 'linear-gradient(135deg, #FF9D6C 0%, #BB4E75 100%)' },
210
+ { name: 'Gradient 2', value: 'linear-gradient(135deg, #85FFBD 0%, #FFFB7D 100%)' },
211
+ { name: 'Gradient 3', value: 'linear-gradient(135deg, #8EC5FC 0%, #E0C3FC 100%)' },
212
+ { name: 'Gradient 4', value: 'linear-gradient(135deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%)' },
213
+ { name: 'Solid Dark', value: '#1e293b' },
214
+ ];
215
+ const currentBg = ref(backgrounds[3]);
216
+
217
+ const themeBgColor = ref('#2d2d2d'); // Default for Tomorrow
218
+
219
+ const changeTheme = () => {
220
+ const link = document.getElementById('prism-theme');
221
+ const baseUrl = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism';
222
+ if (theme.value === 'default') {
223
+ link.href = `${baseUrl}.min.css`;
224
+ themeBgColor.value = '#f5f2f0';
225
+ } else {
226
+ link.href = `${baseUrl}-${theme.value}.min.css`;
227
+ // Update bg color approximation
228
+ if(theme.value === 'tomorrow') themeBgColor.value = '#2d2d2d';
229
+ if(theme.value === 'okaidia') themeBgColor.value = '#272822';
230
+ if(theme.value === 'solarizedlight') themeBgColor.value = '#fdf6e3';
231
+ if(theme.value === 'twilight') themeBgColor.value = '#141414';
232
+ }
233
+ };
234
+
235
+ const refreshHighlight = () => {
236
+ nextTick(() => {
237
+ if (window.Prism) {
238
+ Prism.highlightAll();
239
+ }
240
+ });
241
+ };
242
+
243
+ watch([code, language, showLineNumbers], () => {
244
+ refreshHighlight();
245
+ });
246
+
247
+ onMounted(() => {
248
+ refreshHighlight();
249
+ });
250
+
251
+ const exportImage = async () => {
252
+ if (!snapshotCard.value) return;
253
+
254
+ try {
255
+ const canvas = await html2canvas(snapshotCard.value, {
256
+ scale: 2, // Retina support
257
+ useCORS: true,
258
+ backgroundColor: null,
259
+ logging: false
260
+ });
261
+
262
+ const link = document.createElement('a');
263
+ link.download = `code-snapshot-${Date.now()}.png`;
264
+ link.href = canvas.toDataURL('image/png');
265
+ link.click();
266
+ } catch (err) {
267
+ console.error('Export failed:', err);
268
+ alert('导出失败,请重试');
269
+ }
270
+ };
271
+
272
+ return {
273
+ code,
274
+ language,
275
+ theme,
276
+ padding,
277
+ showWindowControls,
278
+ showLineNumbers,
279
+ showShadow,
280
+ windowTitle,
281
+ backgrounds,
282
+ currentBg,
283
+ snapshotCard,
284
+ codeBlock,
285
+ exportImage,
286
+ changeTheme,
287
+ refreshHighlight,
288
+ themeBgColor
289
+ };
290
+ }
291
+ }).mount('#app');
292
+ </script>
293
+ </body>
294
+ </html>