perryrperry commited on
Commit
71f9056
·
verified ·
1 Parent(s): dac133a

Upload 10 files

Browse files
app.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, jsonify, request
2
+ import requests
3
+ import logging
4
+ import os
5
+
6
+ app = Flask(__name__)
7
+
8
+ # Configure logging
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ API_URL = "https://v2.xxapi.cn/api/meinv"
13
+ HEADERS = {
14
+ 'User-Agent': 'xiaoxiaoapi/1.0.0 (https://xxapi.cn)'
15
+ }
16
+
17
+ @app.route('/')
18
+ def index():
19
+ """Render the main application page"""
20
+ return render_template('index.html')
21
+
22
+ @app.route('/api/videos', methods=['GET'])
23
+ def get_videos():
24
+ """Fetch videos from the external API"""
25
+ try:
26
+ # Get count parameter (default to 1)
27
+ count = int(request.args.get('count', 1))
28
+
29
+ # Limit count to reasonable number to prevent abuse
30
+ count = min(count, 5)
31
+
32
+ videos = []
33
+ for _ in range(count):
34
+ response = requests.get(API_URL, headers=HEADERS)
35
+
36
+ # Check if request was successful
37
+ if response.status_code == 200:
38
+ data = response.json()
39
+ if data.get('code') == 200 and data.get('data'):
40
+ video_url = data.get('data')
41
+ videos.append({
42
+ 'url': video_url,
43
+ 'timestamp': None # Client will set this
44
+ })
45
+ else:
46
+ logger.warning(f"API returned unexpected data format: {data}")
47
+ else:
48
+ logger.error(f"API request failed with status code: {response.status_code}")
49
+
50
+ return jsonify({
51
+ 'success': True,
52
+ 'videos': videos
53
+ })
54
+
55
+ except Exception as e:
56
+ logger.error(f"Error fetching videos: {str(e)}")
57
+ return jsonify({
58
+ 'success': False,
59
+ 'error': 'Failed to fetch videos',
60
+ 'message': str(e)
61
+ }), 500
62
+
63
+ @app.errorhandler(404)
64
+ def page_not_found(e):
65
+ """Handle 404 errors"""
66
+ return render_template('error.html', error='Page not found'), 404
67
+
68
+ @app.errorhandler(500)
69
+ def server_error(e):
70
+ """Handle 500 errors"""
71
+ return render_template('error.html', error='Server error'), 500
72
+
73
+ if __name__ == '__main__':
74
+ # Get port from environment variable or default to 7860
75
+ port = int(os.environ.get('PORT', 7860))
76
+ app.run(host='0.0.0.0', port=port, debug=False)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask
2
+ requests
3
+ Werkzeug
4
+ certifi
5
+ charset-normalizer
6
+ click
7
+ idna
8
+ itsdangerous
9
+ Jinja2
10
+ MarkupSafe
11
+ urllib3
static/css/styles.css ADDED
@@ -0,0 +1,753 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #FF2C55;
3
+ --secondary-color: #00F2EA;
4
+ --dark-color: #121212;
5
+ --darker-color: #0A0A0A;
6
+ --light-color: #ffffff;
7
+ --gray-color: #888888;
8
+ --light-gray-color: #CCCCCC;
9
+ --overlay-color: rgba(0, 0, 0, 0.4);
10
+ --gradient-overlay: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, transparent 40%);
11
+ --card-background: rgba(255, 255, 255, 0.08);
12
+ --blur-effect: blur(10px);
13
+ --transition-normal: all 0.3s cubic-bezier(0.19, 1, 0.22, 1);
14
+ --transition-slow: all 0.5s cubic-bezier(0.19, 1, 0.22, 1);
15
+ --shadow-normal: 0 4px 12px rgba(0, 0, 0, 0.15);
16
+ --shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.2);
17
+ --border-radius: 12px;
18
+ }
19
+
20
+ * {
21
+ margin: 0;
22
+ padding: 0;
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ font-family: 'Nunito', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
28
+ background-color: var(--dark-color);
29
+ color: var(--light-color);
30
+ overflow: hidden;
31
+ touch-action: pan-y;
32
+ position: fixed;
33
+ width: 100%;
34
+ height: 100%;
35
+ -webkit-font-smoothing: antialiased;
36
+ -moz-osx-font-smoothing: grayscale;
37
+ }
38
+
39
+ .app-container {
40
+ width: 100%;
41
+ height: 100vh;
42
+ overflow: hidden;
43
+ position: relative;
44
+ }
45
+
46
+ /* Main screens */
47
+ .screen {
48
+ width: 100%;
49
+ height: calc(100vh - 60px);
50
+ position: absolute;
51
+ left: 0;
52
+ top: 0;
53
+ transition: var(--transition-normal);
54
+ opacity: 0;
55
+ visibility: hidden;
56
+ overflow-y: auto;
57
+ overflow-x: hidden;
58
+ -webkit-overflow-scrolling: touch;
59
+ background-color: var(--darker-color);
60
+ }
61
+
62
+ .screen.active {
63
+ opacity: 1;
64
+ visibility: visible;
65
+ }
66
+
67
+ /* Video Feed Screen */
68
+ .video-feed {
69
+ width: 100%;
70
+ height: 100%;
71
+ position: relative;
72
+ overflow: hidden;
73
+ background-color: black;
74
+ }
75
+
76
+ .video-card {
77
+ width: 100%;
78
+ height: 100%;
79
+ position: absolute;
80
+ left: 0;
81
+ top: 0;
82
+ display: flex;
83
+ justify-content: center;
84
+ align-items: center;
85
+ transition: var(--transition-slow);
86
+ }
87
+
88
+ video {
89
+ width: 100%;
90
+ height: 100%;
91
+ object-fit: cover;
92
+ }
93
+
94
+ .video-overlay {
95
+ position: absolute;
96
+ bottom: 0;
97
+ left: 0;
98
+ width: 100%;
99
+ height: 40%;
100
+ background: var(--gradient-overlay);
101
+ z-index: 5;
102
+ display: flex;
103
+ flex-direction: column;
104
+ justify-content: flex-end;
105
+ padding: 20px;
106
+ }
107
+
108
+ .action-buttons {
109
+ position: absolute;
110
+ right: 16px;
111
+ bottom: 120px;
112
+ display: flex;
113
+ flex-direction: column;
114
+ align-items: center;
115
+ z-index: 10;
116
+ }
117
+
118
+ .action-button {
119
+ width: 48px;
120
+ height: 48px;
121
+ background-color: rgba(255, 255, 255, 0.15);
122
+ border-radius: 50%;
123
+ display: flex;
124
+ justify-content: center;
125
+ align-items: center;
126
+ margin-bottom: 20px;
127
+ cursor: pointer;
128
+ backdrop-filter: var(--blur-effect);
129
+ -webkit-backdrop-filter: var(--blur-effect);
130
+ border: 1px solid rgba(255, 255, 255, 0.2);
131
+ box-shadow: var(--shadow-normal);
132
+ transition: var(--transition-normal);
133
+ position: relative;
134
+ }
135
+
136
+ .action-button:hover, .action-button:active {
137
+ transform: scale(1.1);
138
+ background-color: rgba(255, 255, 255, 0.25);
139
+ }
140
+
141
+ .action-button.like {
142
+ background-color: rgba(255, 44, 85, 0.2);
143
+ }
144
+
145
+ .action-button.like.active {
146
+ background-color: rgba(255, 44, 85, 0.6);
147
+ }
148
+
149
+ .action-button.favorite {
150
+ background-color: rgba(255, 215, 0, 0.2);
151
+ }
152
+
153
+ .action-button.favorite.active {
154
+ background-color: rgba(255, 215, 0, 0.6);
155
+ }
156
+
157
+ .action-count {
158
+ position: absolute;
159
+ bottom: -6px;
160
+ font-size: 12px;
161
+ font-weight: 600;
162
+ }
163
+
164
+ .action-label {
165
+ font-size: 12px;
166
+ margin-top: 5px;
167
+ color: var(--light-color);
168
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
169
+ }
170
+
171
+ .loading-screen {
172
+ position: fixed;
173
+ top: 0;
174
+ left: 0;
175
+ width: 100%;
176
+ height: 100%;
177
+ background-color: var(--dark-color);
178
+ display: flex;
179
+ flex-direction: column;
180
+ justify-content: center;
181
+ align-items: center;
182
+ z-index: 1000;
183
+ }
184
+
185
+ .loading-spinner {
186
+ width: 50px;
187
+ height: 50px;
188
+ border: 3px solid transparent;
189
+ border-top: 3px solid var(--primary-color);
190
+ border-right: 3px solid var(--secondary-color);
191
+ border-radius: 50%;
192
+ animation: spin 1s linear infinite;
193
+ margin-bottom: 20px;
194
+ }
195
+
196
+ @keyframes spin {
197
+ 0% { transform: rotate(0deg); }
198
+ 100% { transform: rotate(360deg); }
199
+ }
200
+
201
+ .loading-text {
202
+ font-size: 16px;
203
+ font-weight: 600;
204
+ color: var(--light-color);
205
+ text-align: center;
206
+ }
207
+
208
+ .swipe-indicator {
209
+ position: absolute;
210
+ bottom: 80px;
211
+ left: 50%;
212
+ transform: translateX(-50%);
213
+ display: flex;
214
+ flex-direction: column;
215
+ align-items: center;
216
+ opacity: 0.8;
217
+ z-index: 10;
218
+ animation: pulse 2s infinite;
219
+ }
220
+
221
+ @keyframes pulse {
222
+ 0% { opacity: 0.4; transform: translateX(-50%) scale(0.95); }
223
+ 50% { opacity: 0.8; transform: translateX(-50%) scale(1); }
224
+ 100% { opacity: 0.4; transform: translateX(-50%) scale(0.95); }
225
+ }
226
+
227
+ .swipe-icon {
228
+ width: 36px;
229
+ height: 36px;
230
+ margin-bottom: 8px;
231
+ }
232
+
233
+ .swipe-text {
234
+ font-size: 14px;
235
+ color: var(--light-color);
236
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
237
+ font-weight: 600;
238
+ }
239
+
240
+ .header {
241
+ position: absolute;
242
+ top: 0;
243
+ left: 0;
244
+ width: 100%;
245
+ height: 60px;
246
+ display: flex;
247
+ justify-content: space-between;
248
+ align-items: center;
249
+ padding: 0 16px;
250
+ z-index: 100;
251
+ background: linear-gradient(to bottom, rgba(0,0,0,0.4) 0%, transparent 100%);
252
+ }
253
+
254
+ .app-title {
255
+ display: flex;
256
+ align-items: center;
257
+ font-size: 22px;
258
+ font-weight: 700;
259
+ color: var(--light-color);
260
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
261
+ }
262
+
263
+ .app-title .accent {
264
+ color: var(--primary-color);
265
+ }
266
+
267
+ .logo-small {
268
+ width: 28px;
269
+ height: 28px;
270
+ margin-right: 8px;
271
+ }
272
+
273
+ .progress-bar {
274
+ position: absolute;
275
+ top: 60px;
276
+ left: 0;
277
+ width: 100%;
278
+ height: 3px;
279
+ background-color: rgba(255, 255, 255, 0.2);
280
+ z-index: 100;
281
+ }
282
+
283
+ .progress-fill {
284
+ height: 100%;
285
+ width: 0;
286
+ background-color: var(--primary-color);
287
+ transition: width 0.1s linear;
288
+ }
289
+
290
+ .video-info {
291
+ padding: 12px 16px;
292
+ position: relative;
293
+ z-index: 10;
294
+ }
295
+
296
+ .video-title {
297
+ font-size: 18px;
298
+ font-weight: 700;
299
+ margin-bottom: 8px;
300
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
301
+ }
302
+
303
+ .video-desc {
304
+ font-size: 14px;
305
+ opacity: 0.9;
306
+ margin-bottom: 12px;
307
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
308
+ }
309
+
310
+ .video-meta {
311
+ display: flex;
312
+ align-items: center;
313
+ font-size: 12px;
314
+ opacity: 0.7;
315
+ }
316
+
317
+ .video-meta-item {
318
+ margin-right: 16px;
319
+ display: flex;
320
+ align-items: center;
321
+ }
322
+
323
+ .video-meta-item svg {
324
+ margin-right: 4px;
325
+ width: 14px;
326
+ height: 14px;
327
+ }
328
+
329
+ .play-pause-overlay {
330
+ position: absolute;
331
+ top: 50%;
332
+ left: 50%;
333
+ transform: translate(-50%, -50%) scale(0);
334
+ width: 80px;
335
+ height: 80px;
336
+ background-color: rgba(0, 0, 0, 0.5);
337
+ border-radius: 50%;
338
+ display: flex;
339
+ justify-content: center;
340
+ align-items: center;
341
+ z-index: 15;
342
+ opacity: 0;
343
+ transition: var(--transition-normal);
344
+ backdrop-filter: var(--blur-effect);
345
+ -webkit-backdrop-filter: var(--blur-effect);
346
+ }
347
+
348
+ .play-pause-overlay.visible {
349
+ transform: translate(-50%, -50%) scale(1);
350
+ opacity: 1;
351
+ }
352
+
353
+ .volume-control {
354
+ position: absolute;
355
+ right: 16px;
356
+ top: 80px;
357
+ width: 40px;
358
+ height: 40px;
359
+ background-color: rgba(255, 255, 255, 0.15);
360
+ border-radius: 50%;
361
+ display: flex;
362
+ justify-content: center;
363
+ align-items: center;
364
+ z-index: 10;
365
+ cursor: pointer;
366
+ backdrop-filter: var(--blur-effect);
367
+ -webkit-backdrop-filter: var(--blur-effect);
368
+ border: 1px solid rgba(255, 255, 255, 0.2);
369
+ box-shadow: var(--shadow-normal);
370
+ }
371
+
372
+ /* Navigation Bar */
373
+ .nav-bar {
374
+ position: fixed;
375
+ bottom: 0;
376
+ left: 0;
377
+ width: 100%;
378
+ height: 60px;
379
+ background-color: rgba(18, 18, 18, 0.95);
380
+ backdrop-filter: var(--blur-effect);
381
+ -webkit-backdrop-filter: var(--blur-effect);
382
+ display: flex;
383
+ justify-content: space-around;
384
+ align-items: center;
385
+ z-index: 1000;
386
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
387
+ }
388
+
389
+ .nav-item {
390
+ display: flex;
391
+ flex-direction: column;
392
+ align-items: center;
393
+ justify-content: center;
394
+ padding: 8px 0;
395
+ width: 25%;
396
+ cursor: pointer;
397
+ }
398
+
399
+ .nav-icon {
400
+ width: 24px;
401
+ height: 24px;
402
+ margin-bottom: 4px;
403
+ transition: var(--transition-normal);
404
+ color: var(--gray-color);
405
+ }
406
+
407
+ .nav-label {
408
+ font-size: 12px;
409
+ transition: var(--transition-normal);
410
+ color: var(--gray-color);
411
+ }
412
+
413
+ .nav-item.active .nav-label {
414
+ color: var(--primary-color);
415
+ }
416
+
417
+ .nav-item.active .nav-icon {
418
+ color: var(--primary-color);
419
+ }
420
+
421
+ /* History Screen */
422
+ .history-screen, .favorites-screen {
423
+ background-color: var(--darker-color);
424
+ padding: 70px 0 20px 0;
425
+ }
426
+
427
+ .screen-header {
428
+ display: flex;
429
+ justify-content: space-between;
430
+ align-items: center;
431
+ padding: 0 16px;
432
+ margin-bottom: 20px;
433
+ }
434
+
435
+ .section-title {
436
+ font-size: 24px;
437
+ font-weight: 700;
438
+ }
439
+
440
+ .clear-btn {
441
+ background-color: rgba(255, 255, 255, 0.1);
442
+ border: none;
443
+ padding: 8px 16px;
444
+ border-radius: 20px;
445
+ color: var(--light-gray-color);
446
+ font-size: 14px;
447
+ cursor: pointer;
448
+ transition: var(--transition-normal);
449
+ }
450
+
451
+ .clear-btn:hover {
452
+ background-color: rgba(255, 255, 255, 0.2);
453
+ }
454
+
455
+ .video-grid {
456
+ display: grid;
457
+ grid-template-columns: repeat(2, 1fr);
458
+ gap: 16px;
459
+ padding: 0 16px 80px 16px;
460
+ }
461
+
462
+ .video-item {
463
+ background-color: var(--card-background);
464
+ border-radius: var(--border-radius);
465
+ overflow: hidden;
466
+ box-shadow: var(--shadow-normal);
467
+ transition: var(--transition-normal);
468
+ border: 1px solid rgba(255, 255, 255, 0.1);
469
+ }
470
+
471
+ .video-item:hover, .video-item:active {
472
+ transform: translateY(-5px);
473
+ box-shadow: var(--shadow-elevated);
474
+ }
475
+
476
+ .video-thumbnail {
477
+ position: relative;
478
+ width: 100%;
479
+ padding-top: 177.77%; /* 16:9 aspect ratio */
480
+ overflow: hidden;
481
+ background-color: #333;
482
+ }
483
+
484
+ .video-thumbnail img {
485
+ position: absolute;
486
+ top: 0;
487
+ left: 0;
488
+ width: 100%;
489
+ height: 100%;
490
+ object-fit: cover;
491
+ }
492
+
493
+ .video-duration {
494
+ position: absolute;
495
+ bottom: 8px;
496
+ right: 8px;
497
+ background-color: rgba(0, 0, 0, 0.7);
498
+ color: white;
499
+ padding: 2px 6px;
500
+ border-radius: 4px;
501
+ font-size: 12px;
502
+ }
503
+
504
+ .video-item-info {
505
+ padding: 10px;
506
+ }
507
+
508
+ .video-item-title {
509
+ font-size: 14px;
510
+ font-weight: 600;
511
+ margin-bottom: 6px;
512
+ white-space: nowrap;
513
+ overflow: hidden;
514
+ text-overflow: ellipsis;
515
+ }
516
+
517
+ .video-item-meta {
518
+ display: flex;
519
+ justify-content: space-between;
520
+ align-items: center;
521
+ font-size: 12px;
522
+ color: var(--gray-color);
523
+ }
524
+
525
+ .time-ago {
526
+ display: flex;
527
+ align-items: center;
528
+ }
529
+
530
+ .time-ago svg {
531
+ width: 12px;
532
+ height: 12px;
533
+ margin-right: 4px;
534
+ }
535
+
536
+ /* Empty state */
537
+ .empty-state {
538
+ display: flex;
539
+ flex-direction: column;
540
+ align-items: center;
541
+ justify-content: center;
542
+ height: 50vh;
543
+ text-align: center;
544
+ padding: 20px;
545
+ color: var(--gray-color);
546
+ }
547
+
548
+ .empty-icon {
549
+ width: 80px;
550
+ height: 80px;
551
+ margin-bottom: 20px;
552
+ opacity: 0.5;
553
+ }
554
+
555
+ .empty-title {
556
+ font-size: 20px;
557
+ font-weight: 600;
558
+ margin-bottom: 10px;
559
+ }
560
+
561
+ .empty-desc {
562
+ font-size: 14px;
563
+ max-width: 280px;
564
+ }
565
+
566
+ /* Profile Screen */
567
+ .profile-screen {
568
+ background-color: var(--darker-color);
569
+ padding-top: 0;
570
+ }
571
+
572
+ .profile-header {
573
+ height: 200px;
574
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
575
+ position: relative;
576
+ display: flex;
577
+ flex-direction: column;
578
+ justify-content: flex-end;
579
+ align-items: center;
580
+ padding-bottom: 24px;
581
+ }
582
+
583
+ .profile-avatar {
584
+ width: 100px;
585
+ height: 100px;
586
+ border-radius: 50%;
587
+ border: 4px solid white;
588
+ overflow: hidden;
589
+ margin-bottom: 12px;
590
+ box-shadow: var(--shadow-elevated);
591
+ }
592
+
593
+ .profile-avatar img {
594
+ width: 100%;
595
+ height: 100%;
596
+ object-fit: cover;
597
+ }
598
+
599
+ .profile-name {
600
+ font-size: 22px;
601
+ font-weight: 700;
602
+ margin-bottom: 4px;
603
+ }
604
+
605
+ .profile-stats {
606
+ display: flex;
607
+ justify-content: space-around;
608
+ align-items: center;
609
+ width: 100%;
610
+ padding: 20px 16px;
611
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
612
+ }
613
+
614
+ .stat-item {
615
+ display: flex;
616
+ flex-direction: column;
617
+ align-items: center;
618
+ }
619
+
620
+ .stat-value {
621
+ font-size: 18px;
622
+ font-weight: 700;
623
+ margin-bottom: 4px;
624
+ }
625
+
626
+ .stat-label {
627
+ font-size: 12px;
628
+ color: var(--gray-color);
629
+ }
630
+
631
+ .profile-actions {
632
+ display: flex;
633
+ flex-direction: column;
634
+ padding: 20px 16px 80px 16px;
635
+ }
636
+
637
+ .action-row {
638
+ display: flex;
639
+ align-items: center;
640
+ padding: 16px 0;
641
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
642
+ cursor: pointer;
643
+ }
644
+
645
+ .action-icon {
646
+ width: 24px;
647
+ height: 24px;
648
+ margin-right: 16px;
649
+ opacity: 0.8;
650
+ }
651
+
652
+ .action-text {
653
+ flex: 1;
654
+ font-size: 16px;
655
+ }
656
+
657
+ .action-arrow {
658
+ width: 20px;
659
+ height: 20px;
660
+ opacity: 0.5;
661
+ }
662
+
663
+ /* Toast notification */
664
+ .toast {
665
+ position: fixed;
666
+ top: 80px;
667
+ left: 50%;
668
+ transform: translateX(-50%);
669
+ background-color: rgba(0, 0, 0, 0.8);
670
+ color: white;
671
+ padding: 10px 20px;
672
+ border-radius: 30px;
673
+ font-size: 14px;
674
+ z-index: 2000;
675
+ backdrop-filter: var(--blur-effect);
676
+ -webkit-backdrop-filter: var(--blur-effect);
677
+ opacity: 0;
678
+ transition: var(--transition-normal);
679
+ pointer-events: none;
680
+ }
681
+
682
+ .toast.show {
683
+ opacity: 1;
684
+ }
685
+
686
+ /* Error page */
687
+ .error-container {
688
+ display: flex;
689
+ justify-content: center;
690
+ align-items: center;
691
+ height: 100vh;
692
+ background-color: var(--darker-color);
693
+ }
694
+
695
+ .error-card {
696
+ background-color: var(--dark-color);
697
+ border-radius: var(--border-radius);
698
+ padding: 40px;
699
+ max-width: 90%;
700
+ width: 400px;
701
+ text-align: center;
702
+ box-shadow: var(--shadow-elevated);
703
+ }
704
+
705
+ .logo {
706
+ width: 80px;
707
+ height: 80px;
708
+ margin-bottom: 20px;
709
+ }
710
+
711
+ .button {
712
+ display: inline-block;
713
+ margin-top: 20px;
714
+ padding: 10px 20px;
715
+ background-color: var(--primary-color);
716
+ color: white;
717
+ text-decoration: none;
718
+ border-radius: 30px;
719
+ font-weight: 600;
720
+ transition: var(--transition-normal);
721
+ }
722
+
723
+ .button:hover {
724
+ background-color: #ff4d6e;
725
+ transform: translateY(-2px);
726
+ }
727
+
728
+ /* Animation for card transition */
729
+ @keyframes slideInUp {
730
+ from {
731
+ transform: translateY(100%);
732
+ }
733
+ to {
734
+ transform: translateY(0);
735
+ }
736
+ }
737
+
738
+ .slide-in {
739
+ animation: slideInUp 0.5s cubic-bezier(0.19, 1, 0.22, 1);
740
+ }
741
+
742
+ /* Responsive adjustments */
743
+ @media (min-width: 768px) {
744
+ .video-grid {
745
+ grid-template-columns: repeat(3, 1fr);
746
+ }
747
+ }
748
+
749
+ @media (min-width: 1024px) {
750
+ .video-grid {
751
+ grid-template-columns: repeat(4, 1fr);
752
+ }
753
+ }
static/img/logo.svg ADDED
static/js/main.js ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * VideoCharm - Main Application
3
+ * Core application logic and initialization
4
+ */
5
+
6
+ const App = (function() {
7
+ // DOM Elements
8
+ let loadingScreen;
9
+ let toast;
10
+ let navItems;
11
+ let screens;
12
+ let historyList;
13
+ let favoritesList;
14
+ let emptyHistory;
15
+ let emptyFavorites;
16
+ let clearHistoryBtn;
17
+ let clearFavoritesBtn;
18
+ let likesCount;
19
+ let favoritesCount;
20
+ let viewsCount;
21
+ let swipeIndicator;
22
+ let profileActions;
23
+
24
+ /**
25
+ * Initialize the application
26
+ */
27
+ function init() {
28
+ // Get DOM elements
29
+ loadingScreen = document.getElementById('loadingScreen');
30
+ toast = document.getElementById('toast');
31
+ navItems = document.querySelectorAll('.nav-item');
32
+ screens = document.querySelectorAll('.screen');
33
+ historyList = document.getElementById('historyList');
34
+ favoritesList = document.getElementById('favoritesList');
35
+ emptyHistory = document.getElementById('emptyHistory');
36
+ emptyFavorites = document.getElementById('emptyFavorites');
37
+ clearHistoryBtn = document.getElementById('clearHistoryBtn');
38
+ clearFavoritesBtn = document.getElementById('clearFavoritesBtn');
39
+ likesCount = document.getElementById('likesCount');
40
+ favoritesCount = document.getElementById('favoritesCount');
41
+ viewsCount = document.getElementById('viewsCount');
42
+ swipeIndicator = document.getElementById('swipeIndicator');
43
+ profileActions = document.querySelectorAll('.action-row');
44
+
45
+ // Initialize storage
46
+ StorageManager.init();
47
+
48
+ // Initialize video player
49
+ VideoPlayer.init();
50
+
51
+ // Set up event listeners
52
+ setupEventListeners();
53
+
54
+ // Load initial videos
55
+ VideoPlayer.loadVideos(3).then(() => {
56
+ // Show swipe indicator after initial load
57
+ setTimeout(() => {
58
+ swipeIndicator.style.display = 'flex';
59
+ setTimeout(() => {
60
+ swipeIndicator.style.opacity = '0';
61
+ setTimeout(() => {
62
+ swipeIndicator.style.display = 'none';
63
+ }, 500);
64
+ }, 5000);
65
+ }, 1000);
66
+ });
67
+
68
+ // Update statistics
69
+ updateStats();
70
+ }
71
+
72
+ /**
73
+ * Set up event listeners
74
+ */
75
+ function setupEventListeners() {
76
+ // Navigation
77
+ navItems.forEach(item => {
78
+ item.addEventListener('click', function() {
79
+ const targetScreen = this.getAttribute('data-screen');
80
+ switchToScreen(targetScreen);
81
+ });
82
+ });
83
+
84
+ // Clear history button
85
+ clearHistoryBtn.addEventListener('click', function() {
86
+ if (confirm('确定要清空所有观看历史吗?')) {
87
+ StorageManager.clearHistory();
88
+ loadHistoryList();
89
+ updateStats();
90
+ showToast('历史记录已清空');
91
+ }
92
+ });
93
+
94
+ // Clear favorites button
95
+ clearFavoritesBtn.addEventListener('click', function() {
96
+ if (confirm('确定要清空所有收藏吗?')) {
97
+ StorageManager.clearFavorites();
98
+ loadFavoritesList();
99
+ updateStats();
100
+ showToast('收藏夹已清空');
101
+ }
102
+ });
103
+
104
+ // Profile actions
105
+ profileActions.forEach(action => {
106
+ action.addEventListener('click', function() {
107
+ const actionType = this.getAttribute('data-action');
108
+ handleProfileAction(actionType);
109
+ });
110
+ });
111
+
112
+ // Comment button
113
+ document.getElementById('commentButton').addEventListener('click', function() {
114
+ showToast('评论功能开发中,敬请期待!');
115
+ });
116
+
117
+ // Share button
118
+ document.getElementById('shareButton').addEventListener('click', function() {
119
+ showToast('分享功能开发中,敬请期待!');
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Switch to a different screen
125
+ * @param {String} screenId ID of screen to switch to
126
+ */
127
+ function switchToScreen(screenId) {
128
+ // Update navigation
129
+ navItems.forEach(item => {
130
+ item.classList.remove('active');
131
+ if (item.getAttribute('data-screen') === screenId) {
132
+ item.classList.add('active');
133
+ }
134
+ });
135
+
136
+ // Update screens
137
+ screens.forEach(screen => {
138
+ screen.classList.remove('active');
139
+ if (screen.id === screenId) {
140
+ screen.classList.add('active');
141
+ }
142
+ });
143
+
144
+ // Handle specific screen actions
145
+ if (screenId === 'historyScreen') {
146
+ loadHistoryList();
147
+ } else if (screenId === 'favoritesScreen') {
148
+ loadFavoritesList();
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Load history list from storage
154
+ */
155
+ function loadHistoryList() {
156
+ const history = StorageManager.getHistory();
157
+
158
+ // Clear list
159
+ historyList.innerHTML = '';
160
+
161
+ if (history.length === 0) {
162
+ // Show empty state
163
+ emptyHistory.style.display = 'flex';
164
+ historyList.style.display = 'none';
165
+ } else {
166
+ // Hide empty state
167
+ emptyHistory.style.display = 'none';
168
+ historyList.style.display = 'grid';
169
+
170
+ // Add history items
171
+ history.forEach(item => {
172
+ const videoItem = createVideoListItem(item, 'history');
173
+ historyList.appendChild(videoItem);
174
+ });
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Load favorites list from storage
180
+ */
181
+ function loadFavoritesList() {
182
+ const favorites = StorageManager.getFavorites();
183
+
184
+ // Clear list
185
+ favoritesList.innerHTML = '';
186
+
187
+ if (favorites.length === 0) {
188
+ // Show empty state
189
+ emptyFavorites.style.display = 'flex';
190
+ favoritesList.style.display = 'none';
191
+ } else {
192
+ // Hide empty state
193
+ emptyFavorites.style.display = 'none';
194
+ favoritesList.style.display = 'grid';
195
+
196
+ // Add favorite items
197
+ favorites.forEach(item => {
198
+ const videoItem = createVideoListItem(item, 'favorite');
199
+ favoritesList.appendChild(videoItem);
200
+ });
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Create a video list item for history or favorites
206
+ * @param {Object} videoData Video data
207
+ * @param {String} type Item type ('history' or 'favorite')
208
+ * @returns {HTMLElement} Video list item element
209
+ */
210
+ function createVideoListItem(videoData, type) {
211
+ const videoItem = document.createElement('div');
212
+ videoItem.className = 'video-item';
213
+ videoItem.setAttribute('data-id', videoData.id);
214
+
215
+ // Generate thumbnail (in real app, would use actual video thumbnails)
216
+ const thumbnailUrl = videoData.thumbnail || generatePlaceholderImage();
217
+
218
+ videoItem.innerHTML = `
219
+ <div class="video-thumbnail">
220
+ <img src="${thumbnailUrl}" alt="${videoData.title}">
221
+ <div class="video-duration">${formatDuration(videoData.duration)}</div>
222
+ </div>
223
+ <div class="video-item-info">
224
+ <div class="video-item-title">${videoData.title}</div>
225
+ <div class="video-item-meta">
226
+ <div class="time-ago">
227
+ <svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
228
+ <path d="M11.99 2C6.47 2 2 6.48 2 12C2 17.52 6.47 22 11.99 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 11.99 2ZM12 20C7.58 20 4 16.42 4 12C4 7.58 7.58 4 12 4C16.42 4 20 7.58 20 12C20 16.42 16.42 20 12 20ZM12.5 7H11V13L16.2 16.2L17 14.9L12.5 12.2V7Z"/>
229
+ </svg>
230
+ ${formatTimeAgo(videoData.timestamp)}
231
+ </div>
232
+ </div>
233
+ </div>
234
+ `;
235
+
236
+ // Add click event
237
+ videoItem.addEventListener('click', function() {
238
+ VideoPlayer.playVideoFromLibrary(videoData);
239
+ showToast(`正在播放${type === 'favorite' ? '收藏' : '历史'}视频`);
240
+ });
241
+
242
+ return videoItem;
243
+ }
244
+
245
+ /**
246
+ * Update profile statistics
247
+ */
248
+ function updateStats() {
249
+ const likes = StorageManager.getLikes().length;
250
+ const favorites = StorageManager.getFavorites().length;
251
+ const views = StorageManager.getViews();
252
+
253
+ likesCount.textContent = likes;
254
+ favoritesCount.textContent = favorites;
255
+ viewsCount.textContent = views;
256
+ }
257
+
258
+ /**
259
+ * Handle profile action
260
+ * @param {String} actionType Type of action
261
+ */
262
+ function handleProfileAction(actionType) {
263
+ switch (actionType) {
264
+ case 'settings':
265
+ case 'preferences':
266
+ case 'about':
267
+ showToast(`${actionType} 功能开发中,敬请期待`);
268
+ break;
269
+ case 'logout':
270
+ if (confirm('确定要退出登录吗?')) {
271
+ showToast('退出登录成功');
272
+ }
273
+ break;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Show loading screen
279
+ */
280
+ function showLoading() {
281
+ loadingScreen.style.display = 'flex';
282
+ }
283
+
284
+ /**
285
+ * Hide loading screen
286
+ */
287
+ function hideLoading() {
288
+ loadingScreen.style.display = 'none';
289
+ }
290
+
291
+ /**
292
+ * Show toast message
293
+ * @param {String} message Message to show
294
+ * @param {Number} duration Duration in ms
295
+ */
296
+ function showToast(message, duration = 2000) {
297
+ toast.textContent = message;
298
+ toast.classList.add('show');
299
+
300
+ setTimeout(() => {
301
+ toast.classList.remove('show');
302
+ }, duration);
303
+ }
304
+
305
+ /**
306
+ * Format duration in seconds to MM:SS
307
+ * @param {Number} seconds Duration in seconds
308
+ * @returns {String} Formatted duration
309
+ */
310
+ function formatDuration(seconds) {
311
+ if (!seconds) return '00:00';
312
+
313
+ const minutes = Math.floor(seconds / 60);
314
+ const remainingSeconds = Math.floor(seconds % 60);
315
+ return `${minutes < 10 ? '0' : ''}${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
316
+ }
317
+
318
+ /**
319
+ * Format timestamp to relative time
320
+ * @param {Number} timestamp Timestamp
321
+ * @returns {String} Formatted time
322
+ */
323
+ function formatTimeAgo(timestamp) {
324
+ const now = new Date().getTime();
325
+ const diff = now - timestamp;
326
+
327
+ // Less than 1 minute
328
+ if (diff < 60 * 1000) {
329
+ return "刚刚";
330
+ }
331
+
332
+ // Less than 1 hour
333
+ if (diff < 60 * 60 * 1000) {
334
+ return Math.floor(diff / (60 * 1000)) + "分钟前";
335
+ }
336
+
337
+ // Less than 24 hours
338
+ if (diff < 24 * 60 * 60 * 1000) {
339
+ return Math.floor(diff / (60 * 60 * 1000)) + "小时前";
340
+ }
341
+
342
+ // Less than 7 days
343
+ if (diff < 7 * 24 * 60 * 60 * 1000) {
344
+ return Math.floor(diff / (24 * 60 * 60 * 1000)) + "天前";
345
+ }
346
+
347
+ // Format as date
348
+ const date = new Date(timestamp);
349
+ return `${date.getMonth() + 1}月${date.getDate()}日`;
350
+ }
351
+
352
+ /**
353
+ * Generate placeholder image URL
354
+ * @returns {String} Image URL
355
+ */
356
+ function generatePlaceholderImage() {
357
+ const randomId = Math.floor(Math.random() * 1000);
358
+ return `https://picsum.photos/seed/${randomId}/300/500`;
359
+ }
360
+
361
+ // Public API
362
+ return {
363
+ init: init,
364
+ showLoading: showLoading,
365
+ hideLoading: hideLoading,
366
+ showToast: showToast,
367
+ switchToScreen: switchToScreen,
368
+ updateStats: updateStats
369
+ };
370
+ })();
371
+
372
+ // Initialize application when DOM is loaded
373
+ document.addEventListener('DOMContentLoaded', function() {
374
+ App.init();
375
+ });
static/js/player.js ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * VideoCharm - Video Player
3
+ * Handles video playback and interaction
4
+ */
5
+
6
+ const VideoPlayer = (function() {
7
+ // DOM Elements
8
+ let videoFeed;
9
+ let progressFill;
10
+ let playPauseOverlay;
11
+ let playIcon;
12
+ let pauseIcon;
13
+ let volumeControl;
14
+ let volumeOnIcon;
15
+ let volumeOffIcon;
16
+ let likeButton;
17
+ let favoriteButton;
18
+ let likeCount;
19
+ let favoriteCount;
20
+
21
+ // State
22
+ let videos = [];
23
+ let currentIndex = 0;
24
+ let touchStartY = 0;
25
+ let touchEndY = 0;
26
+ let isLoading = false;
27
+ let isMuted = false;
28
+
29
+ /**
30
+ * Initialize the video player
31
+ */
32
+ function init() {
33
+ // Get DOM elements
34
+ videoFeed = document.getElementById('videoFeed');
35
+ progressFill = document.getElementById('progressFill');
36
+ playPauseOverlay = document.getElementById('playPauseOverlay');
37
+ playIcon = document.getElementById('playIcon');
38
+ pauseIcon = document.getElementById('pauseIcon');
39
+ volumeControl = document.getElementById('volumeControl');
40
+ volumeOnIcon = document.getElementById('volumeOnIcon');
41
+ volumeOffIcon = document.getElementById('volumeOffIcon');
42
+ likeButton = document.getElementById('likeButton');
43
+ favoriteButton = document.getElementById('favoriteButton');
44
+ likeCount = document.getElementById('likeCount');
45
+ favoriteCount = document.getElementById('favoriteCount');
46
+
47
+ // Set up event listeners
48
+ setupEventListeners();
49
+ }
50
+
51
+ /**
52
+ * Set up event listeners for player controls
53
+ */
54
+ function setupEventListeners() {
55
+ // Touch swipe detection
56
+ document.addEventListener('touchstart', handleTouchStart);
57
+ document.addEventListener('touchend', handleTouchEnd);
58
+
59
+ // Video feed click
60
+ videoFeed.addEventListener('click', handleVideoClick);
61
+
62
+ // Volume control
63
+ volumeControl.addEventListener('click', toggleMute);
64
+
65
+ // Action buttons
66
+ likeButton.addEventListener('click', toggleLike);
67
+ favoriteButton.addEventListener('click', toggleFavorite);
68
+ }
69
+
70
+ /**
71
+ * Load videos from the API
72
+ * @param {Number} count Number of videos to load
73
+ * @returns {Promise} Promise that resolves when videos are loaded
74
+ */
75
+ function loadVideos(count = 1) {
76
+ isLoading = true;
77
+ App.showLoading();
78
+
79
+ return fetch(`/api/videos?count=${count}`)
80
+ .then(response => {
81
+ if (!response.ok) {
82
+ throw new Error('Network response was not ok');
83
+ }
84
+ return response.json();
85
+ })
86
+ .then(data => {
87
+ if (data.success && data.videos && data.videos.length > 0) {
88
+ data.videos.forEach(videoData => {
89
+ createVideoElement(videoData.url);
90
+ });
91
+
92
+ // Start playing first video if this is initial load
93
+ if (videos.length === count) {
94
+ playCurrentVideo();
95
+ }
96
+ } else {
97
+ throw new Error('No videos returned from API');
98
+ }
99
+ })
100
+ .catch(error => {
101
+ console.error('Error loading videos:', error);
102
+ App.showToast('加载视频失败,请检查网络连接');
103
+ })
104
+ .finally(() => {
105
+ isLoading = false;
106
+ App.hideLoading();
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Create a new video element
112
+ * @param {String} videoUrl URL of the video
113
+ */
114
+ function createVideoElement(videoUrl) {
115
+ if (!videoUrl) return;
116
+
117
+ // Create container
118
+ const videoCard = document.createElement('div');
119
+ videoCard.className = 'video-card';
120
+ videoCard.style.transform = `translateY(${100 * videos.length}%)`;
121
+
122
+ // Create video element
123
+ const video = document.createElement('video');
124
+ video.src = videoUrl;
125
+ video.loop = true;
126
+ video.preload = 'auto';
127
+ video.muted = isMuted;
128
+ video.playsInline = true;
129
+ video.setAttribute('webkit-playsinline', '');
130
+
131
+ // Generate random data for the video
132
+ const videoId = generateVideoId();
133
+ const videoTitle = `精彩视频 #${videos.length + 1}`;
134
+ const videoDesc = getRandomDescription();
135
+ const randomLikes = Math.floor(Math.random() * 1000) + 1;
136
+
137
+ // Create video object
138
+ const videoObj = {
139
+ id: videoId,
140
+ url: videoUrl,
141
+ title: videoTitle,
142
+ desc: videoDesc,
143
+ element: videoCard,
144
+ video: video,
145
+ likes: randomLikes,
146
+ timestamp: new Date().getTime(),
147
+ duration: 0
148
+ };
149
+
150
+ // Create video overlay with info
151
+ const overlay = document.createElement('div');
152
+ overlay.className = 'video-overlay';
153
+
154
+ const videoInfo = document.createElement('div');
155
+ videoInfo.className = 'video-info';
156
+
157
+ const titleEl = document.createElement('div');
158
+ titleEl.className = 'video-title';
159
+ titleEl.textContent = videoObj.title;
160
+
161
+ const descEl = document.createElement('div');
162
+ descEl.className = 'video-desc';
163
+ descEl.textContent = videoObj.desc;
164
+
165
+ const metaEl = document.createElement('div');
166
+ metaEl.className = 'video-meta';
167
+
168
+ const durationEl = document.createElement('div');
169
+ durationEl.className = 'video-meta-item duration';
170
+ durationEl.innerHTML = `
171
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
172
+ <path d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM12.5 7H11V13L16.2 16.2L17 14.9L12.5 12.2V7Z"/>
173
+ </svg>
174
+ <span>00:00</span>
175
+ `;
176
+
177
+ metaEl.appendChild(durationEl);
178
+ videoInfo.appendChild(titleEl);
179
+ videoInfo.appendChild(descEl);
180
+ videoInfo.appendChild(metaEl);
181
+ overlay.appendChild(videoInfo);
182
+
183
+ // Add elements to DOM
184
+ videoCard.appendChild(video);
185
+ videoCard.appendChild(overlay);
186
+ videoFeed.appendChild(videoCard);
187
+
188
+ // Add to videos array
189
+ videos.push(videoObj);
190
+
191
+ // Set up video event listeners
192
+ setupVideoEvents(video, videoObj, durationEl);
193
+ }
194
+
195
+ /**
196
+ * Set up event listeners for a video
197
+ * @param {HTMLElement} video Video element
198
+ * @param {Object} videoObj Video object
199
+ * @param {HTMLElement} durationEl Duration display element
200
+ */
201
+ function setupVideoEvents(video, videoObj, durationEl) {
202
+ // Update duration when metadata is loaded
203
+ video.addEventListener('loadedmetadata', function() {
204
+ videoObj.duration = Math.round(video.duration);
205
+ const durationSpan = durationEl.querySelector('span');
206
+ if (durationSpan) {
207
+ durationSpan.textContent = formatDuration(videoObj.duration);
208
+ }
209
+ });
210
+
211
+ // Update progress bar
212
+ video.addEventListener('timeupdate', function() {
213
+ if (videos.indexOf(videoObj) === currentIndex) {
214
+ const progress = (video.currentTime / video.duration) * 100;
215
+ progressFill.style.width = `${progress}%`;
216
+ }
217
+ });
218
+
219
+ // Handle video errors
220
+ video.addEventListener('error', function() {
221
+ console.error('Video load error:', videoObj.url);
222
+ App.showToast('视频加载失败,正在尝试下一个');
223
+ if (videos.indexOf(videoObj) === currentIndex) {
224
+ switchToVideo(currentIndex + 1);
225
+ }
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Switch to a different video
231
+ * @param {Number} index Index of video to switch to
232
+ */
233
+ function switchToVideo(index) {
234
+ if (index < 0 || index >= videos.length || isLoading) return;
235
+
236
+ // Pause all videos
237
+ videos.forEach(item => {
238
+ item.video.pause();
239
+ });
240
+
241
+ // Update current index
242
+ currentIndex = index;
243
+
244
+ // Update video positions
245
+ updateVideoPositions();
246
+
247
+ // Play current video
248
+ playCurrentVideo();
249
+
250
+ // Reset progress bar
251
+ progressFill.style.width = '0';
252
+
253
+ // Update action buttons
254
+ updateActionButtons();
255
+
256
+ // Add to history
257
+ if (videos[currentIndex]) {
258
+ StorageManager.addToHistory(videos[currentIndex]);
259
+ }
260
+
261
+ // Load more videos if needed
262
+ if (currentIndex >= videos.length - 2) {
263
+ loadVideos(2);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Play the current video
269
+ */
270
+ function playCurrentVideo() {
271
+ if (!videos[currentIndex]) return;
272
+
273
+ const video = videos[currentIndex].video;
274
+
275
+ // Try to play and handle autoplay restrictions
276
+ const playPromise = video.play();
277
+
278
+ if (playPromise !== undefined) {
279
+ playPromise.catch(error => {
280
+ // Auto-play was prevented, set muted and try again
281
+ console.log('Autoplay prevented, trying muted playback');
282
+ video.muted = true;
283
+ isMuted = true;
284
+ updateVolumeUI();
285
+ video.play().catch(e => {
286
+ console.error('Failed to play video even with mute:', e);
287
+ });
288
+ });
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Update positions of all videos
294
+ */
295
+ function updateVideoPositions() {
296
+ videos.forEach((item, idx) => {
297
+ item.element.style.transform = `translateY(${(idx - currentIndex) * 100}%)`;
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Update action buttons state based on current video
303
+ */
304
+ function updateActionButtons() {
305
+ if (!videos[currentIndex]) return;
306
+
307
+ const videoId = videos[currentIndex].id;
308
+
309
+ // Update like button
310
+ if (StorageManager.isLiked(videoId)) {
311
+ likeButton.classList.add('active');
312
+ } else {
313
+ likeButton.classList.remove('active');
314
+ }
315
+
316
+ // Update favorite button
317
+ if (StorageManager.isFavorite(videoId)) {
318
+ favoriteButton.classList.add('active');
319
+ } else {
320
+ favoriteButton.classList.remove('active');
321
+ }
322
+
323
+ // Update counts
324
+ likeCount.textContent = Math.floor(videos[currentIndex].likes);
325
+ favoriteCount.textContent = StorageManager.isFavorite(videoId) ? '1' : '0';
326
+ }
327
+
328
+ /**
329
+ * Handle touch start event
330
+ * @param {Event} e Touch event
331
+ */
332
+ function handleTouchStart(e) {
333
+ touchStartY = e.touches[0].clientY;
334
+ }
335
+
336
+ /**
337
+ * Handle touch end event
338
+ * @param {Event} e Touch event
339
+ */
340
+ function handleTouchEnd(e) {
341
+ // Only process touch events on home screen
342
+ if (!document.getElementById('homeScreen').classList.contains('active')) {
343
+ return;
344
+ }
345
+
346
+ touchEndY = e.changedTouches[0].clientY;
347
+ const deltaY = touchStartY - touchEndY;
348
+
349
+ // If swipe distance is significant
350
+ if (Math.abs(deltaY) > 50) {
351
+ if (deltaY > 0) {
352
+ // Swipe up - next video
353
+ switchToVideo(currentIndex + 1);
354
+ } else {
355
+ // Swipe down - previous video
356
+ switchToVideo(currentIndex - 1);
357
+ }
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Handle click on video feed
363
+ * @param {Event} e Click event
364
+ */
365
+ function handleVideoClick(e) {
366
+ // Ignore clicks on controls
367
+ if (e.target.closest('.action-button') ||
368
+ e.target.closest('.volume-control')) {
369
+ return;
370
+ }
371
+
372
+ // Toggle play/pause
373
+ if (!videos[currentIndex]) return;
374
+
375
+ const video = videos[currentIndex].video;
376
+
377
+ if (video.paused) {
378
+ video.play();
379
+ playIcon.style.display = 'block';
380
+ pauseIcon.style.display = 'none';
381
+ } else {
382
+ video.pause();
383
+ playIcon.style.display = 'none';
384
+ pauseIcon.style.display = 'block';
385
+ }
386
+
387
+ // Show play/pause overlay
388
+ playPauseOverlay.classList.add('visible');
389
+ setTimeout(() => {
390
+ playPauseOverlay.classList.remove('visible');
391
+ }, 800);
392
+ }
393
+
394
+ /**
395
+ * Toggle mute state
396
+ */
397
+ function toggleMute() {
398
+ isMuted = !isMuted;
399
+
400
+ // Update all videos
401
+ videos.forEach(item => {
402
+ item.video.muted = isMuted;
403
+ });
404
+
405
+ // Update UI
406
+ updateVolumeUI();
407
+
408
+ // Show toast
409
+ App.showToast(isMuted ? '已静音' : '已开启声音');
410
+ }
411
+
412
+ /**
413
+ * Update volume control UI
414
+ */
415
+ function updateVolumeUI() {
416
+ if (isMuted) {
417
+ volumeOnIcon.style.display = 'none';
418
+ volumeOffIcon.style.display = 'block';
419
+ } else {
420
+ volumeOnIcon.style.display = 'block';
421
+ volumeOffIcon.style.display = 'none';
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Toggle like for current video
427
+ */
428
+ function toggleLike() {
429
+ if (!videos[currentIndex]) return;
430
+
431
+ const videoId = videos[currentIndex].id;
432
+ const newLikeState = StorageManager.toggleLike(videoId);
433
+
434
+ // Update UI
435
+ if (newLikeState) {
436
+ likeButton.classList.add('active');
437
+ videos[currentIndex].likes++;
438
+ App.showToast('已添加到喜欢');
439
+ } else {
440
+ likeButton.classList.remove('active');
441
+ videos[currentIndex].likes--;
442
+ App.showToast('已取消喜欢');
443
+ }
444
+
445
+ // Update count
446
+ likeCount.textContent = Math.floor(videos[currentIndex].likes);
447
+
448
+ // Update profile stats
449
+ App.updateStats();
450
+ }
451
+
452
+ /**
453
+ * Toggle favorite for current video
454
+ */
455
+ function toggleFavorite() {
456
+ if (!videos[currentIndex]) return;
457
+
458
+ const newFavoriteState = StorageManager.toggleFavorite(videos[currentIndex]);
459
+
460
+ // Update UI
461
+ if (newFavoriteState) {
462
+ favoriteButton.classList.add('active');
463
+ favoriteCount.textContent = '1';
464
+ App.showToast('已添加到收藏');
465
+ } else {
466
+ favoriteButton.classList.remove('active');
467
+ favoriteCount.textContent = '0';
468
+ App.showToast('已取消收藏');
469
+ }
470
+
471
+ // Update profile stats
472
+ App.updateStats();
473
+ }
474
+
475
+ /**
476
+ * Play a video from the history or favorites
477
+ * @param {Object} videoData Video data from storage
478
+ */
479
+ function playVideoFromLibrary(videoData) {
480
+ if (!videoData || !videoData.url) return;
481
+
482
+ // Check if video is already loaded
483
+ const existingIndex = videos.findIndex(v => v.id === videoData.id);
484
+
485
+ if (existingIndex !== -1) {
486
+ // Switch to existing video
487
+ switchToVideo(existingIndex);
488
+ } else {
489
+ // Create new video element
490
+ createVideoElement(videoData.url);
491
+
492
+ // Switch to the new video
493
+ switchToVideo(videos.length - 1);
494
+ }
495
+
496
+ // Switch to home screen
497
+ App.switchToScreen('homeScreen');
498
+ }
499
+
500
+ /**
501
+ * Generate a unique video ID
502
+ * @returns {String} Unique ID
503
+ */
504
+ function generateVideoId() {
505
+ return 'v_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
506
+ }
507
+
508
+ /**
509
+ * Format duration in seconds to MM:SS
510
+ * @param {Number} seconds Duration in seconds
511
+ * @returns {String} Formatted duration
512
+ */
513
+ function formatDuration(seconds) {
514
+ if (!seconds) return '00:00';
515
+
516
+ const minutes = Math.floor(seconds / 60);
517
+ const remainingSeconds = Math.floor(seconds % 60);
518
+ return `${minutes < 10 ? '0' : ''}${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
519
+ }
520
+
521
+ /**
522
+ * Get random video description
523
+ * @returns {String} Random description
524
+ */
525
+ function getRandomDescription() {
526
+ const descriptions = [
527
+ "精选高质量视频,带给你视觉享受",
528
+ "每日精选,让你流连忘返",
529
+ "热门推荐,不容错过的精彩瞬间",
530
+ "探索更多精彩内容,尽在VideoCharm",
531
+ "发现生活中的美好瞬间",
532
+ "精彩纷呈,尽在眼前",
533
+ "让心情放松的精选内容",
534
+ "视觉盛宴,触手可及"
535
+ ];
536
+
537
+ return descriptions[Math.floor(Math.random() * descriptions.length)];
538
+ }
539
+
540
+ // Public API
541
+ return {
542
+ init: init,
543
+ loadVideos: loadVideos,
544
+ switchToVideo: switchToVideo,
545
+ playVideoFromLibrary: playVideoFromLibrary,
546
+ updateVideoPositions: updateVideoPositions
547
+ };
548
+ })();
static/js/storage.js ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * VideoCharm - Local Storage Management
3
+ * Handles all local storage operations for history and favorites
4
+ */
5
+
6
+ const StorageManager = (function() {
7
+ // Storage keys
8
+ const KEYS = {
9
+ HISTORY: 'videocharm_history',
10
+ FAVORITES: 'videocharm_favorites',
11
+ LIKES: 'videocharm_likes',
12
+ VIEWS: 'videocharm_views'
13
+ };
14
+
15
+ // Maximum storage age (7 days in milliseconds)
16
+ const MAX_HISTORY_AGE = 7 * 24 * 60 * 60 * 1000;
17
+
18
+ /**
19
+ * Initialize storage with default values if not exists
20
+ */
21
+ function initStorage() {
22
+ if (!localStorage.getItem(KEYS.HISTORY)) {
23
+ localStorage.setItem(KEYS.HISTORY, JSON.stringify([]));
24
+ }
25
+
26
+ if (!localStorage.getItem(KEYS.FAVORITES)) {
27
+ localStorage.setItem(KEYS.FAVORITES, JSON.stringify([]));
28
+ }
29
+
30
+ if (!localStorage.getItem(KEYS.LIKES)) {
31
+ localStorage.setItem(KEYS.LIKES, JSON.stringify([]));
32
+ }
33
+
34
+ if (!localStorage.getItem(KEYS.VIEWS)) {
35
+ localStorage.setItem(KEYS.VIEWS, JSON.stringify(0));
36
+ }
37
+
38
+ // Clean up old history entries
39
+ cleanupHistory();
40
+ }
41
+
42
+ /**
43
+ * Remove history items older than MAX_HISTORY_AGE
44
+ */
45
+ function cleanupHistory() {
46
+ const history = getHistory();
47
+ const now = new Date().getTime();
48
+
49
+ const filteredHistory = history.filter(item => {
50
+ return (now - item.timestamp) < MAX_HISTORY_AGE;
51
+ });
52
+
53
+ if (filteredHistory.length !== history.length) {
54
+ localStorage.setItem(KEYS.HISTORY, JSON.stringify(filteredHistory));
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get all history items
60
+ * @returns {Array} History items
61
+ */
62
+ function getHistory() {
63
+ try {
64
+ return JSON.parse(localStorage.getItem(KEYS.HISTORY)) || [];
65
+ } catch (e) {
66
+ console.error('Error parsing history from localStorage:', e);
67
+ return [];
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get all favorites
73
+ * @returns {Array} Favorite items
74
+ */
75
+ function getFavorites() {
76
+ try {
77
+ return JSON.parse(localStorage.getItem(KEYS.FAVORITES)) || [];
78
+ } catch (e) {
79
+ console.error('Error parsing favorites from localStorage:', e);
80
+ return [];
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get all liked video IDs
86
+ * @returns {Array} Liked video IDs
87
+ */
88
+ function getLikes() {
89
+ try {
90
+ return JSON.parse(localStorage.getItem(KEYS.LIKES)) || [];
91
+ } catch (e) {
92
+ console.error('Error parsing likes from localStorage:', e);
93
+ return [];
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get view count
99
+ * @returns {Number} Total view count
100
+ */
101
+ function getViews() {
102
+ try {
103
+ return parseInt(JSON.parse(localStorage.getItem(KEYS.VIEWS))) || 0;
104
+ } catch (e) {
105
+ console.error('Error parsing views from localStorage:', e);
106
+ return 0;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Add a video to history
112
+ * @param {Object} video Video object to add
113
+ */
114
+ function addToHistory(video) {
115
+ if (!video || !video.id || !video.url) return;
116
+
117
+ const history = getHistory();
118
+
119
+ // Check if video already exists in history
120
+ const existingIndex = history.findIndex(item => item.id === video.id);
121
+
122
+ if (existingIndex !== -1) {
123
+ // Update timestamp of existing item
124
+ history[existingIndex].timestamp = new Date().getTime();
125
+ } else {
126
+ // Create simplified record for storage
127
+ const historyItem = {
128
+ id: video.id,
129
+ url: video.url,
130
+ title: video.title || '精彩视频',
131
+ timestamp: new Date().getTime(),
132
+ duration: video.duration || 0,
133
+ thumbnail: video.thumbnail || null
134
+ };
135
+
136
+ // Add to beginning of array
137
+ history.unshift(historyItem);
138
+ }
139
+
140
+ // Save updated history
141
+ localStorage.setItem(KEYS.HISTORY, JSON.stringify(history));
142
+
143
+ // Increment view count
144
+ incrementViews();
145
+ }
146
+
147
+ /**
148
+ * Toggle favorite status for a video
149
+ * @param {Object} video Video object
150
+ * @returns {Boolean} New favorite status
151
+ */
152
+ function toggleFavorite(video) {
153
+ if (!video || !video.id) return false;
154
+
155
+ const favorites = getFavorites();
156
+ const existingIndex = favorites.findIndex(item => item.id === video.id);
157
+
158
+ if (existingIndex !== -1) {
159
+ // Remove from favorites
160
+ favorites.splice(existingIndex, 1);
161
+ localStorage.setItem(KEYS.FAVORITES, JSON.stringify(favorites));
162
+ return false;
163
+ } else {
164
+ // Create simplified record for storage
165
+ const favoriteItem = {
166
+ id: video.id,
167
+ url: video.url,
168
+ title: video.title || '精彩视频',
169
+ timestamp: new Date().getTime(),
170
+ duration: video.duration || 0,
171
+ thumbnail: video.thumbnail || null
172
+ };
173
+
174
+ // Add to favorites
175
+ favorites.unshift(favoriteItem);
176
+ localStorage.setItem(KEYS.FAVORITES, JSON.stringify(favorites));
177
+ return true;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Check if a video is favorited
183
+ * @param {String} videoId Video ID to check
184
+ * @returns {Boolean} Whether video is favorited
185
+ */
186
+ function isFavorite(videoId) {
187
+ if (!videoId) return false;
188
+
189
+ const favorites = getFavorites();
190
+ return favorites.some(item => item.id === videoId);
191
+ }
192
+
193
+ /**
194
+ * Toggle like status for a video
195
+ * @param {String} videoId Video ID
196
+ * @returns {Boolean} New like status
197
+ */
198
+ function toggleLike(videoId) {
199
+ if (!videoId) return false;
200
+
201
+ const likes = getLikes();
202
+ const index = likes.indexOf(videoId);
203
+
204
+ if (index !== -1) {
205
+ // Remove like
206
+ likes.splice(index, 1);
207
+ localStorage.setItem(KEYS.LIKES, JSON.stringify(likes));
208
+ return false;
209
+ } else {
210
+ // Add like
211
+ likes.push(videoId);
212
+ localStorage.setItem(KEYS.LIKES, JSON.stringify(likes));
213
+ return true;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Check if a video is liked
219
+ * @param {String} videoId Video ID to check
220
+ * @returns {Boolean} Whether video is liked
221
+ */
222
+ function isLiked(videoId) {
223
+ if (!videoId) return false;
224
+
225
+ const likes = getLikes();
226
+ return likes.includes(videoId);
227
+ }
228
+
229
+ /**
230
+ * Increment view count
231
+ */
232
+ function incrementViews() {
233
+ const views = getViews();
234
+ localStorage.setItem(KEYS.VIEWS, JSON.stringify(views + 1));
235
+ }
236
+
237
+ /**
238
+ * Clear all history
239
+ */
240
+ function clearHistory() {
241
+ localStorage.setItem(KEYS.HISTORY, JSON.stringify([]));
242
+ }
243
+
244
+ /**
245
+ * Clear all favorites
246
+ */
247
+ function clearFavorites() {
248
+ localStorage.setItem(KEYS.FAVORITES, JSON.stringify([]));
249
+ }
250
+
251
+ // Public API
252
+ return {
253
+ init: initStorage,
254
+ getHistory: getHistory,
255
+ getFavorites: getFavorites,
256
+ getLikes: getLikes,
257
+ getViews: getViews,
258
+ addToHistory: addToHistory,
259
+ toggleFavorite: toggleFavorite,
260
+ isFavorite: isFavorite,
261
+ toggleLike: toggleLike,
262
+ isLiked: isLiked,
263
+ clearHistory: clearHistory,
264
+ clearFavorites: clearFavorites
265
+ };
266
+ })();
templates/error.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block content %}
4
+ <div class="error-container">
5
+ <div class="error-card">
6
+ <img src="{{ url_for('static', filename='img/logo.svg') }}" alt="VideoCharm" class="logo">
7
+ <h1>{{ error }}</h1>
8
+ <p>很抱歉,发生了一些错误。</p>
9
+ <a href="/" class="button">返回首页</a>
10
+ </div>
11
+ </div>
12
+ {% endblock %}
templates/index.html ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block content %}
4
+ <div class="app-container">
5
+ <div class="loading-screen" id="loadingScreen">
6
+ <div class="loading-spinner"></div>
7
+ <div class="loading-text">加载精彩内容中...</div>
8
+ </div>
9
+
10
+ <div class="toast" id="toast"></div>
11
+
12
+ <!-- Home/Feed Screen -->
13
+ <div class="screen active" id="homeScreen">
14
+ <div class="header">
15
+ <div class="app-title">
16
+ <img src="{{ url_for('static', filename='img/logo.svg') }}" alt="VideoCharm" class="logo-small">
17
+ <span>Video<span class="accent">Charm</span></span>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="progress-bar">
22
+ <div class="progress-fill" id="progressFill"></div>
23
+ </div>
24
+
25
+ <div class="video-feed" id="videoFeed"></div>
26
+
27
+ <div class="play-pause-overlay" id="playPauseOverlay">
28
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" id="playPauseIcon">
29
+ <path d="M8 5V19L19 12L8 5Z" fill="white" id="playIcon"/>
30
+ <path d="M6 19H10V5H6V19ZM14 5V19H18V5H14Z" fill="white" id="pauseIcon" style="display: none;"/>
31
+ </svg>
32
+ </div>
33
+
34
+ <div class="volume-control" id="volumeControl">
35
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
36
+ <path d="M3 9V15H7L12 20V4L7 9H3Z" fill="white" id="volumeOffIcon" style="display: none;"/>
37
+ <path d="M3 9V15H7L12 20V4L7 9H3ZM16.5 12C16.5 10.23 15.48 8.71 14 7.97V16.02C15.48 15.29 16.5 13.77 16.5 12ZM14 3.23V5.29C16.89 6.15 19 8.83 19 12C19 15.17 16.89 17.85 14 18.71V20.77C17.95 19.86 21 16.28 21 12C21 7.72 17.95 4.14 14 3.23Z" fill="white" id="volumeOnIcon"/>
38
+ </svg>
39
+ </div>
40
+
41
+ <div class="action-buttons">
42
+ <div class="action-button like" id="likeButton">
43
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
44
+ <path d="M12 21.35L10.55 20.03C5.4 15.36 2 12.28 2 8.5C2 5.42 4.42 3 7.5 3C9.24 3 10.91 3.81 12 5.09C13.09 3.81 14.76 3 16.5 3C19.58 3 22 5.42 22 8.5C22 12.28 18.6 15.36 13.45 20.04L12 21.35Z" fill="#FF2C55"/>
45
+ </svg>
46
+ <span class="action-count" id="likeCount">0</span>
47
+ </div>
48
+ <div class="action-button favorite" id="favoriteButton">
49
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
50
+ <path d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z" fill="#FFD700"/>
51
+ </svg>
52
+ <span class="action-count" id="favoriteCount">0</span>
53
+ </div>
54
+ <div class="action-button" id="commentButton">
55
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
56
+ <path d="M21 6H19V15H6V17C6 17.55 6.45 18 7 18H18L22 22V7C22 6.45 21.55 6 21 6ZM17 12V3C17 2.45 16.55 2 16 2H3C2.45 2 2 2.45 2 3V17L6 13H16C16.55 13 17 12.55 17 12Z" fill="white"/>
57
+ </svg>
58
+ </div>
59
+ <div class="action-button" id="shareButton">
60
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
61
+ <path d="M18 16.08C17.24 16.08 16.56 16.38 16.04 16.85L8.91 12.7C8.96 12.47 9 12.24 9 12C9 11.76 8.96 11.53 8.91 11.3L15.96 7.19C16.5 7.69 17.21 8 18 8C19.66 8 21 6.66 21 5C21 3.34 19.66 2 18 2C16.34 2 15 3.34 15 5C15 5.24 15.04 5.47 15.09 5.7L8.04 9.81C7.5 9.31 6.79 9 6 9C4.34 9 3 10.34 3 12C3 13.66 4.34 15 6 15C6.79 15 7.5 14.69 8.04 14.19L15.16 18.35C15.11 18.56 15.08 18.78 15.08 19C15.08 20.61 16.39 21.92 18 21.92C19.61 21.92 20.92 20.61 20.92 19C20.92 17.39 19.61 16.08 18 16.08Z" fill="white"/>
62
+ </svg>
63
+ </div>
64
+ </div>
65
+
66
+ <div class="swipe-indicator" id="swipeIndicator">
67
+ <svg class="swipe-icon" width="36" height="36" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
68
+ <path d="M7.41 15.41L12 10.83L16.59 15.41L18 14L12 8L6 14L7.41 15.41Z" fill="white"/>
69
+ </svg>
70
+ <div class="swipe-text">上滑查看下一个</div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- History Screen -->
75
+ <div class="screen" id="historyScreen">
76
+ <div class="screen-header">
77
+ <h2 class="section-title">我的观看历史</h2>
78
+ <button id="clearHistoryBtn" class="clear-btn">清空历史</button>
79
+ </div>
80
+ <div id="historyList" class="video-grid"></div>
81
+ <div class="empty-state" id="emptyHistory">
82
+ <svg class="empty-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
83
+ <path d="M13 3C8.03 3 4 7.03 4 12H1L4.89 15.89L4.96 16.03L9 12H6C6 8.13 9.13 5 13 5C16.87 5 20 8.13 20 12C20 15.87 16.87 19 13 19C11.07 19 9.32 18.21 8.06 16.94L6.64 18.36C8.27 19.99 10.51 21 13 21C17.97 21 22 16.97 22 12C22 7.03 17.97 3 13 3ZM12 8V13L16.28 15.54L17 14.33L13.5 12.25V8H12Z" fill="currentColor"/>
84
+ </svg>
85
+ <div class="empty-title">暂无观看历史</div>
86
+ <div class="empty-desc">您浏览过的视频将会显示在这里</div>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Favorites Screen -->
91
+ <div class="screen" id="favoritesScreen">
92
+ <div class="screen-header">
93
+ <h2 class="section-title">我的收藏</h2>
94
+ <button id="clearFavoritesBtn" class="clear-btn">清空收藏</button>
95
+ </div>
96
+ <div id="favoritesList" class="video-grid"></div>
97
+ <div class="empty-state" id="emptyFavorites">
98
+ <svg class="empty-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
99
+ <path d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z" fill="currentColor"/>
100
+ </svg>
101
+ <div class="empty-title">暂无收藏内容</div>
102
+ <div class="empty-desc">点击视频右侧的星标图标将视频添加到收藏夹</div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Profile Screen -->
107
+ <div class="screen" id="profileScreen">
108
+ <div class="profile-header">
109
+ <div class="profile-avatar">
110
+ <img src="https://randomuser.me/api/portraits/women/44.jpg" alt="Profile">
111
+ </div>
112
+ <div class="profile-name">用户123456</div>
113
+ </div>
114
+
115
+ <div class="profile-stats">
116
+ <div class="stat-item">
117
+ <div class="stat-value" id="favoritesCount">0</div>
118
+ <div class="stat-label">收藏</div>
119
+ </div>
120
+ <div class="stat-item">
121
+ <div class="stat-value" id="likesCount">0</div>
122
+ <div class="stat-label">点赞</div>
123
+ </div>
124
+ <div class="stat-item">
125
+ <div class="stat-value" id="viewsCount">0</div>
126
+ <div class="stat-label">观看</div>
127
+ </div>
128
+ </div>
129
+
130
+ <div class="profile-actions">
131
+ <div class="action-row" data-action="settings">
132
+ <svg class="action-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
133
+ <path d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z" fill="white"/>
134
+ </svg>
135
+ <div class="action-text">账号设置</div>
136
+ <svg class="action-arrow" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
137
+ <path d="M8.59 16.59L13.17 12L8.59 7.41L10 6L16 12L10 18L8.59 16.59Z" fill="white"/>
138
+ </svg>
139
+ </div>
140
+ <div class="action-row" data-action="preferences">
141
+ <svg class="action-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
142
+ <path d="M19.14 12.94C19.18 12.64 19.2 12.33 19.2 12C19.2 11.68 19.18 11.36 19.13 11.06L21.16 9.48C21.34 9.34 21.39 9.07 21.28 8.87L19.36 5.55C19.24 5.33 18.99 5.26 18.77 5.33L16.38 6.29C15.88 5.91 15.35 5.59 14.76 5.35L14.4 2.81C14.36 2.57 14.16 2.4 13.92 2.4H10.08C9.84 2.4 9.65 2.57 9.61 2.81L9.25 5.35C8.66 5.59 8.12 5.92 7.63 6.29L5.24 5.33C5.02 5.25 4.77 5.33 4.65 5.55L2.74 8.87C2.62 9.08 2.66 9.34 2.86 9.48L4.89 11.06C4.84 11.36 4.8 11.69 4.8 12C4.8 12.31 4.82 12.64 4.87 12.94L2.84 14.52C2.66 14.66 2.61 14.93 2.72 15.13L4.64 18.45C4.76 18.67 5.01 18.74 5.23 18.67L7.62 17.71C8.12 18.09 8.65 18.41 9.24 18.65L9.6 21.19C9.65 21.43 9.84 21.6 10.08 21.6H13.92C14.16 21.6 14.36 21.43 14.39 21.19L14.75 18.65C15.34 18.41 15.88 18.09 16.37 17.71L18.76 18.67C18.98 18.75 19.23 18.67 19.35 18.45L21.27 15.13C21.39 14.91 21.34 14.66 21.15 14.52L19.14 12.94ZM12 15.6C10.02 15.6 8.4 13.98 8.4 12C8.4 10.02 10.02 8.4 12 8.4C13.98 8.4 15.6 10.02 15.6 12C15.6 13.98 13.98 15.6 12 15.6Z" fill="white"/>
143
+ </svg>
144
+ <div class="action-text">偏好设置</div>
145
+ <svg class="action-arrow" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
146
+ <path d="M8.59 16.59L13.17 12L8.59 7.41L10 6L16 12L10 18L8.59 16.59Z" fill="white"/>
147
+ </svg>
148
+ </div>
149
+ <div class="action-row" data-action="about">
150
+ <svg class="action-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
151
+ <path d="M11.99 2C6.47 2 2 6.48 2 12C2 17.52 6.47 22 11.99 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 11.99 2ZM12 20C7.58 20 4 16.42 4 12C4 7.58 7.58 4 12 4C16.42 4 20 7.58 20 12C20 16.42 16.42 20 12 20ZM11 15H13V17H11V15ZM11 7H13V13H11V7Z" fill="white"/>
152
+ </svg>
153
+ <div class="action-text">关于我们</div>
154
+ <svg class="action-arrow" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
155
+ <path d="M8.59 16.59L13.17 12L8.59 7.41L10 6L16 12L10 18L8.59 16.59Z" fill="white"/>
156
+ </svg>
157
+ </div>
158
+ <div class="action-row" data-action="logout">
159
+ <svg class="action-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="color: #FF4D4F;">
160
+ <path d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z" fill="currentColor"/>
161
+ </svg>
162
+ <div class="action-text" style="color: #FF4D4F;">退出登录</div>
163
+ <svg class="action-arrow" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
164
+ <path d="M8.59 16.59L13.17 12L8.59 7.41L10 6L16 12L10 18L8.59 16.59Z" fill="white"/>
165
+ </svg>
166
+ </div>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Navigation Bar -->
171
+ <div class="nav-bar">
172
+ <div class="nav-item active" data-screen="homeScreen">
173
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
174
+ <path d="M10 20V14H14V20H19V12H22L12 3L2 12H5V20H10Z"/>
175
+ </svg>
176
+ <div class="nav-label">首页</div>
177
+ </div>
178
+ <div class="nav-item" data-screen="historyScreen">
179
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
180
+ <path d="M13 3C8.03 3 4 7.03 4 12H1L4.89 15.89L4.96 16.03L9 12H6C6 8.13 9.13 5 13 5C16.87 5 20 8.13 20 12C20 15.87 16.87 19 13 19C11.07 19 9.32 18.21 8.06 16.94L6.64 18.36C8.27 19.99 10.51 21 13 21C17.97 21 22 16.97 22 12C22 7.03 17.97 3 13 3ZM12 8V13L16.28 15.54L17 14.33L13.5 12.25V8H12Z"/>
181
+ </svg>
182
+ <div class="nav-label">历史</div>
183
+ </div>
184
+ <div class="nav-item" data-screen="favoritesScreen">
185
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
186
+ <path d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z"/>
187
+ </svg>
188
+ <div class="nav-label">收藏</div>
189
+ </div>
190
+ <div class="nav-item" data-screen="profileScreen">
191
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
192
+ <path d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z"/>
193
+ </svg>
194
+ <div class="nav-label">我的</div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ {% endblock %}
templates/layout.html ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, maximum-scale=1.0, user-scalable=no">
6
+ <title>VideoCharm - 精致短视频</title>
7
+ <meta name="description" content="VideoCharm - 精致视觉体验的短视频应用">
8
+
9
+ <!-- Favicon -->
10
+ <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
11
+
12
+ <!-- Google Fonts -->
13
+ <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
14
+
15
+ <!-- Main CSS -->
16
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
17
+
18
+ {% block head %}{% endblock %}
19
+ </head>
20
+ <body>
21
+ {% block content %}{% endblock %}
22
+
23
+ <!-- Load scripts at end of body for performance -->
24
+ <script src="{{ url_for('static', filename='js/storage.js') }}"></script>
25
+ <script src="{{ url_for('static', filename='js/player.js') }}"></script>
26
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
27
+
28
+ {% block scripts %}{% endblock %}
29
+ </body>
30
+ </html>