Trae Assistant commited on
Commit
c038eac
·
1 Parent(s): 33f7d9a

Initial commit of Stream Schedule Maker

Browse files
Files changed (5) hide show
  1. Dockerfile +15 -0
  2. README.md +60 -4
  3. app.py +15 -0
  4. requirements.txt +2 -0
  5. templates/index.html +329 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 user to avoid running as root (good practice for HF Spaces)
11
+ RUN useradd -m -u 1000 user
12
+ USER user
13
+ ENV PATH="/home/user/.local/bin:$PATH"
14
+
15
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md CHANGED
@@ -1,10 +1,66 @@
1
  ---
2
  title: Stream Schedule Maker
3
- emoji: 📉
4
- colorFrom: yellow
5
- colorTo: yellow
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Stream Schedule Maker
3
+ emoji: 📅
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ app_port: 7860
9
  ---
10
 
11
+ # 直播日程表生成器 (Stream Schedule Maker)
12
+
13
+ 这是一个专为内容创作者(主播、UP主、博主)设计的**直播日程表/周计划生成工具**。
14
+
15
+ 👉 **在线体验**: [Hugging Face Space](https://huggingface.co/spaces/duqing26/stream-schedule-maker)
16
+
17
+ ## ✨ 核心功能
18
+
19
+ * **可视化编辑**: 左侧面板实时编辑,右侧即时预览。
20
+ * **多主题支持**:
21
+ * ⚪ **现代极简 (Modern)**: 清爽干净,适合大多数场景。
22
+ * ⚫ **黑金科技 (Dark)**: 酷炫深色模式,适合游戏/科技博主。
23
+ * 🌸 **元气粉嫩 (Cute)**: 可爱风格,适合生活/二次元博主。
24
+ * 🤖 **赛博朋克 (Cyber)**: 霓虹风格,个性十足。
25
+ * **高清导出**: 一键生成高清 PNG 图片,直接发布到 Bilibili 动态、朋友圈或 Twitter。
26
+ * **隐私安全**: 所有生成过程在浏览器端(Canvas)完成,数据不上传服务器。
27
+
28
+ ## 🛠️ 技术栈
29
+
30
+ * **Frontend**: Vue 3 + Tailwind CSS + HTML5 Canvas
31
+ * **Backend**: Flask (用于静态资源托管)
32
+ * **Deployment**: Docker
33
+
34
+ ## 🚀 快速开始
35
+
36
+ ### 本地运行
37
+
38
+ 1. 克隆项目:
39
+ ```bash
40
+ git clone https://github.com/duqing26/stream-schedule-maker.git
41
+ cd stream-schedule-maker
42
+ ```
43
+
44
+ 2. 安装依赖:
45
+ ```bash
46
+ pip install -r requirements.txt
47
+ ```
48
+
49
+ 3. 运行:
50
+ ```bash
51
+ python app.py
52
+ ```
53
+
54
+ 4. 访问 `http://localhost:7860`
55
+
56
+ ### Docker 运行
57
+
58
+ ```bash
59
+ docker build -t stream-schedule-maker .
60
+ docker run -p 7860:7860 stream-schedule-maker
61
+ ```
62
+
63
+ ## 📝 关于
64
+
65
+ 本项目由 [Trae AI](https://trae.ai) 辅助开发。
66
+ 旨在帮助创作者更高效地管理和预告自己的内容计划。
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,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>直播日程表生成器 | Stream Schedule Maker</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
12
+ body {
13
+ font-family: 'Noto Sans SC', sans-serif;
14
+ }
15
+ .preview-container {
16
+ background-image: repeating-linear-gradient(45deg, #f0f0f0 25%, transparent 25%, transparent 75%, #f0f0f0 75%, #f0f0f0), repeating-linear-gradient(45deg, #f0f0f0 25%, #ffffff 25%, #ffffff 75%, #f0f0f0 75%, #f0f0f0);
17
+ background-position: 0 0, 10px 10px;
18
+ background-size: 20px 20px;
19
+ }
20
+ </style>
21
+ </head>
22
+ <body class="bg-gray-50 text-gray-800 h-screen overflow-hidden flex flex-col">
23
+
24
+ <div id="app" class="flex-1 flex flex-col h-full">
25
+ <!-- Header -->
26
+ <header class="bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center shadow-sm z-10">
27
+ <div class="flex items-center gap-3">
28
+ <div class="w-10 h-10 bg-indigo-600 rounded-lg flex items-center justify-center text-white text-xl">
29
+ <i class="fa-solid fa-calendar-week"></i>
30
+ </div>
31
+ <div>
32
+ <h1 class="text-xl font-bold text-gray-900">直播日程表生成器</h1>
33
+ <p class="text-xs text-gray-500">Stream Schedule Maker</p>
34
+ </div>
35
+ </div>
36
+ <div class="flex gap-3">
37
+ <a href="https://github.com/duqing26" target="_blank" class="text-gray-500 hover:text-gray-900 transition">
38
+ <i class="fa-brands fa-github text-xl"></i>
39
+ </a>
40
+ </div>
41
+ </header>
42
+
43
+ <!-- Main Content -->
44
+ <main class="flex-1 flex overflow-hidden">
45
+ <!-- Sidebar: Editor -->
46
+ <div class="w-1/3 bg-white border-r border-gray-200 flex flex-col h-full shadow-lg z-0">
47
+ <div class="p-4 border-b border-gray-100">
48
+ <h2 class="font-bold text-gray-700 mb-4 flex items-center gap-2">
49
+ <i class="fa-solid fa-sliders"></i> 全局设置
50
+ </h2>
51
+ <div class="grid grid-cols-2 gap-4">
52
+ <div>
53
+ <label class="block text-xs font-medium text-gray-500 mb-1">主标题</label>
54
+ <input v-model="config.title" type="text" class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition">
55
+ </div>
56
+ <div>
57
+ <label class="block text-xs font-medium text-gray-500 mb-1">日期/副标题</label>
58
+ <input v-model="config.subtitle" type="text" class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition">
59
+ </div>
60
+ <div>
61
+ <label class="block text-xs font-medium text-gray-500 mb-1">主题风格</label>
62
+ <select v-model="config.theme" class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition">
63
+ <option value="modern">现代极简 (Modern)</option>
64
+ <option value="dark">黑金科技 (Dark)</option>
65
+ <option value="cute">元气粉嫩 (Cute)</option>
66
+ <option value="cyber">赛博朋克 (Cyber)</option>
67
+ </select>
68
+ </div>
69
+ <div>
70
+ <label class="block text-xs font-medium text-gray-500 mb-1">作者/主播名</label>
71
+ <input v-model="config.author" type="text" class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition">
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <div class="flex-1 overflow-y-auto p-4 custom-scrollbar">
77
+ <h2 class="font-bold text-gray-700 mb-4 flex items-center gap-2">
78
+ <i class="fa-solid fa-list-check"></i> 日程安排
79
+ </h2>
80
+ <div class="space-y-4">
81
+ <div v-for="(day, index) in days" :key="index" class="bg-gray-50 p-3 rounded-lg border border-gray-200 hover:border-indigo-300 transition group">
82
+ <div class="flex justify-between items-center mb-2">
83
+ <span class="font-bold text-sm text-gray-700 bg-white px-2 py-0.5 rounded shadow-sm">{{ day.name }}</span>
84
+ <label class="flex items-center gap-2 text-xs text-gray-500 cursor-pointer">
85
+ <input type="checkbox" v-model="day.active" class="rounded text-indigo-600 focus:ring-indigo-500">
86
+ 启用
87
+ </label>
88
+ </div>
89
+ <div v-if="day.active" class="space-y-2 animate-fade-in">
90
+ <div class="flex gap-2">
91
+ <input v-model="day.time" placeholder="时间 (e.g. 20:00)" class="w-1/3 px-2 py-1.5 border rounded text-sm focus:border-indigo-500 outline-none">
92
+ <input v-model="day.game" placeholder="内容/游戏 (e.g. 杂谈)" class="w-2/3 px-2 py-1.5 border rounded text-sm focus:border-indigo-500 outline-none">
93
+ </div>
94
+ <textarea v-model="day.desc" placeholder="详细描述/备注" rows="2" class="w-full px-2 py-1.5 border rounded text-sm focus:border-indigo-500 outline-none resize-none"></textarea>
95
+ </div>
96
+ <div v-else class="text-xs text-gray-400 italic text-center py-2">
97
+ (该日无直播计划)
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Main: Preview -->
105
+ <div class="flex-1 bg-gray-100 flex flex-col relative">
106
+ <!-- Toolbar -->
107
+ <div class="absolute top-4 right-4 z-20 flex gap-2">
108
+ <button @click="downloadImage" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 transition transform hover:scale-105">
109
+ <i class="fa-solid fa-download"></i> 下载 PNG
110
+ </button>
111
+ </div>
112
+
113
+ <!-- Canvas Container -->
114
+ <div class="flex-1 overflow-auto flex items-center justify-center p-8 preview-container" id="canvas-wrapper">
115
+ <canvas ref="canvas" class="shadow-2xl rounded-sm max-h-full max-w-full"></canvas>
116
+ </div>
117
+ </div>
118
+ </main>
119
+ </div>
120
+
121
+ <script>
122
+ const { createApp, ref, reactive, onMounted, watch } = Vue;
123
+
124
+ createApp({
125
+ setup() {
126
+ const canvas = ref(null);
127
+
128
+ // Configuration
129
+ const config = reactive({
130
+ title: 'WEEKLY SCHEDULE',
131
+ subtitle: '2023.10.01 - 10.07',
132
+ theme: 'modern', // modern, dark, cute, cyber
133
+ author: '@你的名字',
134
+ });
135
+
136
+ // Days Data
137
+ const days = reactive([
138
+ { name: 'MON', label: '周一', active: true, time: '20:00', game: '杂谈', desc: '聊聊最近发生的趣事' },
139
+ { name: 'TUE', label: '周二', active: false, time: '', game: '休息', desc: '' },
140
+ { name: 'WED', label: '周三', active: true, time: '21:00', game: '恐怖游戏', desc: '不要被吓到哦' },
141
+ { name: 'THU', label: '周四', active: false, time: '', game: '休息', desc: '' },
142
+ { name: 'FRI', label: '周五', active: true, time: '20:00', game: '歌回', desc: '点歌环节' },
143
+ { name: 'SAT', label: '周六', active: true, time: '14:00', game: '耐久直播', desc: '不通关不下播!' },
144
+ { name: 'SUN', label: '周日', active: true, time: '19:00', game: '总结', desc: '本周总结 & 下周预告' },
145
+ ]);
146
+
147
+ // Theme Definitions
148
+ const themes = {
149
+ modern: {
150
+ bg: '#ffffff',
151
+ text: '#333333',
152
+ accent: '#6366f1', // Indigo
153
+ secondary: '#9ca3af',
154
+ cardBg: '#f9fafb',
155
+ cardBorder: '#e5e7eb',
156
+ font: 'Noto Sans SC'
157
+ },
158
+ dark: {
159
+ bg: '#111827',
160
+ text: '#f3f4f6',
161
+ accent: '#fbbf24', // Amber
162
+ secondary: '#9ca3af',
163
+ cardBg: '#1f2937',
164
+ cardBorder: '#374151',
165
+ font: 'Noto Sans SC'
166
+ },
167
+ cute: {
168
+ bg: '#fff1f2',
169
+ text: '#881337',
170
+ accent: '#fb7185', // Rose
171
+ secondary: '#fda4af',
172
+ cardBg: '#ffffb1',
173
+ cardBorder: '#fecdd3',
174
+ font: 'Noto Sans SC'
175
+ },
176
+ cyber: {
177
+ bg: '#09090b',
178
+ text: '#00f2ff',
179
+ accent: '#ff0055', // Neon Pink
180
+ secondary: '#71717a',
181
+ cardBg: '#18181b',
182
+ cardBorder: '#27272a',
183
+ font: 'Noto Sans SC'
184
+ }
185
+ };
186
+
187
+ // Draw Function
188
+ const draw = () => {
189
+ const ctx = canvas.value.getContext('2d');
190
+ const width = 1200;
191
+ const height = 1600; // Vertical aspect ratio fits mobile better
192
+
193
+ // Set canvas resolution
194
+ canvas.value.width = width;
195
+ canvas.value.height = height;
196
+
197
+ const theme = themes[config.theme];
198
+
199
+ // 1. Background
200
+ ctx.fillStyle = theme.bg;
201
+ ctx.fillRect(0, 0, width, height);
202
+
203
+ // Optional: Draw grid or pattern based on theme
204
+ if (config.theme === 'cyber') {
205
+ ctx.strokeStyle = '#27272a';
206
+ ctx.lineWidth = 2;
207
+ for(let i=0; i<width; i+=50) { ctx.beginPath(); ctx.moveTo(i,0); ctx.lineTo(i,height); ctx.stroke(); }
208
+ for(let i=0; i<height; i+=50) { ctx.beginPath(); ctx.moveTo(0,i); ctx.lineTo(width,i); ctx.stroke(); }
209
+ }
210
+
211
+ // 2. Header
212
+ ctx.textAlign = 'center';
213
+
214
+ // Title
215
+ ctx.fillStyle = theme.text;
216
+ ctx.font = `bold 80px ${theme.font}`;
217
+ ctx.fillText(config.title, width / 2, 120);
218
+
219
+ // Subtitle
220
+ ctx.fillStyle = theme.accent;
221
+ ctx.font = `bold 40px ${theme.font}`;
222
+ ctx.fillText(config.subtitle, width / 2, 190);
223
+
224
+ // Author (Top Right or Bottom)
225
+ // Let's put it top left for branding
226
+ ctx.textAlign = 'left';
227
+ ctx.font = `30px ${theme.font}`;
228
+ ctx.fillStyle = theme.secondary;
229
+ ctx.fillText(config.author, 50, 60);
230
+
231
+
232
+ // 3. Days Grid
233
+ const startY = 250;
234
+ const gap = 30;
235
+ const cardHeight = (height - startY - 100) / 7 - gap; // Distribute vertically
236
+ const cardWidth = width - 100; // Full width with padding
237
+ const cardX = 50;
238
+
239
+ days.forEach((day, index) => {
240
+ const y = startY + index * (cardHeight + gap);
241
+
242
+ // Card Background
243
+ ctx.fillStyle = theme.cardBg;
244
+ // Rounded rect simulation
245
+ ctx.fillRect(cardX, y, cardWidth, cardHeight);
246
+
247
+ // Border
248
+ ctx.strokeStyle = theme.cardBorder;
249
+ ctx.lineWidth = 4;
250
+ ctx.strokeRect(cardX, y, cardWidth, cardHeight);
251
+
252
+ // Day Label Box (Left)
253
+ const labelWidth = 180;
254
+ ctx.fillStyle = theme.accent;
255
+ ctx.fillRect(cardX, y, labelWidth, cardHeight);
256
+
257
+ // Day Text
258
+ ctx.fillStyle = (config.theme === 'dark' || config.theme === 'cyber') ? '#000' : '#fff';
259
+ ctx.textAlign = 'center';
260
+ ctx.textBaseline = 'middle';
261
+ ctx.font = `bold 40px ${theme.font}`;
262
+ ctx.fillText(day.name, cardX + labelWidth/2, y + cardHeight/2 - 15);
263
+ ctx.font = `24px ${theme.font}`;
264
+ ctx.fillText(day.label, cardX + labelWidth/2, y + cardHeight/2 + 25);
265
+
266
+ // Content
267
+ ctx.textAlign = 'left';
268
+
269
+ if (day.active) {
270
+ // Time
271
+ ctx.fillStyle = theme.accent;
272
+ ctx.font = `bold 36px ${theme.font}`;
273
+ ctx.fillText(day.time, cardX + labelWidth + 40, y + 50);
274
+
275
+ // Game/Content Title
276
+ ctx.fillStyle = theme.text;
277
+ ctx.font = `bold 40px ${theme.font}`;
278
+ ctx.fillText(day.game, cardX + labelWidth + 180, y + 50);
279
+
280
+ // Description
281
+ ctx.fillStyle = theme.secondary;
282
+ ctx.font = `30px ${theme.font}`;
283
+ ctx.fillText(day.desc, cardX + labelWidth + 40, y + 100);
284
+ } else {
285
+ // Rest Day
286
+ ctx.fillStyle = theme.secondary;
287
+ ctx.font = `italic 40px ${theme.font}`;
288
+ ctx.fillText("OFF / 休息", cardX + labelWidth + 40, y + cardHeight/2);
289
+ }
290
+ });
291
+
292
+ // 4. Footer
293
+ ctx.textAlign = 'center';
294
+ ctx.fillStyle = theme.secondary;
295
+ ctx.font = `24px ${theme.font}`;
296
+ ctx.fillText("Generated by Stream Schedule Maker", width/2, height - 30);
297
+ };
298
+
299
+ // Watchers
300
+ watch([config, days], () => {
301
+ // Use nextTick equivalent or small timeout to ensure font loaded (simplified)
302
+ setTimeout(draw, 50);
303
+ }, { deep: true });
304
+
305
+ onMounted(() => {
306
+ // Load fonts first then draw
307
+ document.fonts.ready.then(() => {
308
+ draw();
309
+ });
310
+ });
311
+
312
+ const downloadImage = () => {
313
+ const link = document.createElement('a');
314
+ link.download = `schedule-${config.subtitle}.png`;
315
+ link.href = canvas.value.toDataURL('image/png');
316
+ link.click();
317
+ };
318
+
319
+ return {
320
+ config,
321
+ days,
322
+ canvas,
323
+ downloadImage
324
+ };
325
+ }
326
+ }).mount('#app');
327
+ </script>
328
+ </body>
329
+ </html>