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

Upload 10 files

Browse files
Files changed (4) hide show
  1. app.py +50 -15
  2. static/js/main.js +28 -9
  3. static/js/player.js +490 -402
  4. static/js/storage.js +72 -68
app.py CHANGED
@@ -2,6 +2,7 @@ from flask import Flask, render_template, jsonify, request
2
  import requests
3
  import logging
4
  import os
 
5
 
6
  app = Flask(__name__)
7
 
@@ -14,6 +15,10 @@ 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"""
@@ -21,7 +26,7 @@ def index():
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))
@@ -30,23 +35,53 @@ def get_videos():
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
 
2
  import requests
3
  import logging
4
  import os
5
+ import random
6
 
7
  app = Flask(__name__)
8
 
 
15
  'User-Agent': 'xiaoxiaoapi/1.0.0 (https://xxapi.cn)'
16
  }
17
 
18
+ # Video URL cache
19
+ video_url_cache = []
20
+ CACHE_SIZE = 20
21
+
22
  @app.route('/')
23
  def index():
24
  """Render the main application page"""
 
26
 
27
  @app.route('/api/videos', methods=['GET'])
28
  def get_videos():
29
+ """Fetch videos from the external API with caching"""
30
  try:
31
  # Get count parameter (default to 1)
32
  count = int(request.args.get('count', 1))
 
35
  count = min(count, 5)
36
 
37
  videos = []
38
+
39
+ # If cache has enough videos, return from cache
40
+ if len(video_url_cache) >= count:
41
+ # Randomly select videos instead of sequential order
42
+ selected_videos = random.sample(video_url_cache, count)
43
+ for video_url in selected_videos:
44
+ videos.append({
45
+ 'url': video_url,
46
+ 'timestamp': None
47
+ })
48
+ # Remove used videos from cache to avoid repetition
49
+ if video_url in video_url_cache:
50
+ video_url_cache.remove(video_url)
51
+
52
+ # If we need more videos (either for response or to refill cache)
53
+ fetch_count = max(count - len(videos), CACHE_SIZE - len(video_url_cache))
54
+
55
+ if fetch_count > 0:
56
+ new_urls = []
57
+ for _ in range(fetch_count):
58
+ try:
59
+ response = requests.get(API_URL, headers=HEADERS, timeout=3)
60
+
61
+ if response.status_code == 200:
62
+ data = response.json()
63
+ if data.get('code') == 200 and data.get('data'):
64
+ video_url = data.get('data')
65
+ new_urls.append(video_url)
66
+ else:
67
+ logger.warning(f"API returned status code: {response.status_code}")
68
+ except Exception as e:
69
+ logger.error(f"Error in API request: {str(e)}")
70
 
71
+ # Add to response if needed
72
+ needed_for_response = count - len(videos)
73
+ if needed_for_response > 0 and new_urls:
74
+ for i in range(min(needed_for_response, len(new_urls))):
 
75
  videos.append({
76
+ 'url': new_urls[i],
77
+ 'timestamp': None
78
  })
79
+ # Don't add these to cache since we're using them now
80
+ new_urls[i] = None
81
+
82
+ # Add remaining new URLs to cache
83
+ video_url_cache.extend([url for url in new_urls if url is not None])
84
+
85
  return jsonify({
86
  'success': True,
87
  'videos': videos
static/js/main.js CHANGED
@@ -51,8 +51,8 @@ const App = (function() {
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';
@@ -61,7 +61,12 @@ const App = (function() {
61
  setTimeout(() => {
62
  swipeIndicator.style.display = 'none';
63
  }, 500);
64
- }, 5000);
 
 
 
 
 
65
  }, 1000);
66
  });
67
 
@@ -212,12 +217,17 @@ const App = (function() {
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">
@@ -350,11 +360,20 @@ const App = (function() {
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
 
 
51
  // Set up event listeners
52
  setupEventListeners();
53
 
54
+ // Load initial video and start playing immediately
55
+ VideoPlayer.loadVideos(1, true).then(() => {
56
  // Show swipe indicator after initial load
57
  setTimeout(() => {
58
  swipeIndicator.style.display = 'flex';
 
61
  setTimeout(() => {
62
  swipeIndicator.style.display = 'none';
63
  }, 500);
64
+ }, 3000);
65
+ }, 500);
66
+
67
+ // Load more videos in background for smooth experience
68
+ setTimeout(() => {
69
+ VideoPlayer.loadVideos(3, false);
70
  }, 1000);
71
  });
72
 
 
217
  videoItem.className = 'video-item';
218
  videoItem.setAttribute('data-id', videoData.id);
219
 
220
+ // Use thumbnail if available, otherwise use placeholder
221
+ let thumbnailUrl;
222
+ if (videoData.thumbnail) {
223
+ thumbnailUrl = videoData.thumbnail;
224
+ } else {
225
+ thumbnailUrl = generatePlaceholderImage(videoData.id);
226
+ }
227
 
228
  videoItem.innerHTML = `
229
  <div class="video-thumbnail">
230
+ <img src="${thumbnailUrl}" alt="${videoData.title}" onerror="this.src='${generatePlaceholderImage(videoData.id)}'">
231
  <div class="video-duration">${formatDuration(videoData.duration)}</div>
232
  </div>
233
  <div class="video-item-info">
 
360
  }
361
 
362
  /**
363
+ * Generate consistent placeholder image URL based on ID
364
+ * @param {String} id Item ID for consistent image generation
365
  * @returns {String} Image URL
366
  */
367
+ function generatePlaceholderImage(id) {
368
+ // Generate a deterministic number from the string ID
369
+ let hash = 0;
370
+ if (id && id.length > 0) {
371
+ for (let i = 0; i < id.length; i++) {
372
+ hash = ((hash << 5) - hash) + id.charCodeAt(i);
373
+ hash |= 0; // Convert to 32bit integer
374
+ }
375
+ }
376
+ const randomId = Math.abs(hash % 1000); // Ensure positive
377
  return `https://picsum.photos/seed/${randomId}/300/500`;
378
  }
379
 
static/js/player.js CHANGED
@@ -25,6 +25,7 @@ const VideoPlayer = (function() {
25
  let touchEndY = 0;
26
  let isLoading = false;
27
  let isMuted = false;
 
28
 
29
  /**
30
  * Initialize the video player
@@ -70,11 +71,32 @@ const VideoPlayer = (function() {
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 => {
@@ -85,13 +107,25 @@ const VideoPlayer = (function() {
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');
@@ -99,11 +133,14 @@ const VideoPlayer = (function() {
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
 
@@ -152,397 +189,448 @@ const VideoPlayer = (function() {
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
  })();
 
25
  let touchEndY = 0;
26
  let isLoading = false;
27
  let isMuted = false;
28
+ let videoLoadTimeout;
29
 
30
  /**
31
  * Initialize the video player
 
71
  /**
72
  * Load videos from the API
73
  * @param {Number} count Number of videos to load
74
+ * @param {Boolean} showLoading Whether to show loading screen
75
  * @returns {Promise} Promise that resolves when videos are loaded
76
  */
77
+ function loadVideos(count = 1, showLoading = true) {
78
+ if (isLoading) return Promise.resolve();
79
+
80
  isLoading = true;
81
+ if (showLoading) {
82
+ App.showLoading();
83
+ }
84
+
85
+ // Clear any existing timeout
86
+ if (videoLoadTimeout) {
87
+ clearTimeout(videoLoadTimeout);
88
+ }
89
+
90
+ // Set a timeout to ensure loading doesn't hang
91
+ videoLoadTimeout = setTimeout(() => {
92
+ if (isLoading) {
93
+ isLoading = false;
94
+ if (showLoading) {
95
+ App.hideLoading();
96
+ }
97
+ App.showToast('加载超时,请检查网络后重试');
98
+ }
99
+ }, 8000);
100
 
101
  return fetch(`/api/videos?count=${count}`)
102
  .then(response => {
 
107
  })
108
  .then(data => {
109
  if (data.success && data.videos && data.videos.length > 0) {
110
+ // Immediately create and start playing the first video
111
+ const firstVideo = data.videos[0];
112
+ createVideoElement(firstVideo.url);
113
 
114
+ // If this is the first video, play it immediately
115
+ if (videos.length === 1) {
116
  playCurrentVideo();
117
+ if (showLoading) {
118
+ App.hideLoading();
119
+ }
120
+ }
121
+
122
+ // Asynchronously load remaining videos without blocking UI
123
+ if (data.videos.length > 1) {
124
+ setTimeout(() => {
125
+ for (let i = 1; i < data.videos.length; i++) {
126
+ createVideoElement(data.videos[i].url);
127
+ }
128
+ }, 300);
129
  }
130
  } else {
131
  throw new Error('No videos returned from API');
 
133
  })
134
  .catch(error => {
135
  console.error('Error loading videos:', error);
136
+ App.showToast('加载视频失败,请重试');
137
  })
138
  .finally(() => {
139
+ clearTimeout(videoLoadTimeout);
140
  isLoading = false;
141
+ if (showLoading) {
142
+ App.hideLoading();
143
+ }
144
  });
145
  }
146
 
 
189
  overlay.className = 'video-overlay';
190
 
191
  const videoInfo = document.createElement('div');
192
+ videoInfo.className = 'video-info';
193
+
194
+ const titleEl = document.createElement('div');
195
+ titleEl.className = 'video-title';
196
+ titleEl.textContent = videoObj.title;
197
+
198
+ const descEl = document.createElement('div');
199
+ descEl.className = 'video-desc';
200
+ descEl.textContent = videoObj.desc;
201
+
202
+ const metaEl = document.createElement('div');
203
+ metaEl.className = 'video-meta';
204
+
205
+ const durationEl = document.createElement('div');
206
+ durationEl.className = 'video-meta-item duration';
207
+ durationEl.innerHTML = `
208
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
209
+ <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"/>
210
+ </svg>
211
+ <span>00:00</span>
212
+ `;
213
+
214
+ metaEl.appendChild(durationEl);
215
+ videoInfo.appendChild(titleEl);
216
+ videoInfo.appendChild(descEl);
217
+ videoInfo.appendChild(metaEl);
218
+ overlay.appendChild(videoInfo);
219
+
220
+ // Add elements to DOM
221
+ videoCard.appendChild(video);
222
+ videoCard.appendChild(overlay);
223
+ videoFeed.appendChild(videoCard);
224
+
225
+ // Add to videos array
226
+ videos.push(videoObj);
227
+
228
+ // Set up video event listeners
229
+ setupVideoEvents(video, videoObj, durationEl);
230
+ }
231
+
232
+ /**
233
+ * Set up event listeners for a video
234
+ * @param {HTMLElement} video Video element
235
+ * @param {Object} videoObj Video object
236
+ * @param {HTMLElement} durationEl Duration display element
237
+ */
238
+ function setupVideoEvents(video, videoObj, durationEl) {
239
+ // Use video's first frame as thumbnail when available
240
+ video.addEventListener('loadeddata', function() {
241
+ videoObj.hasLoadedData = true;
242
+ // Capture first frame as thumbnail
243
+ videoObj.thumbnail = captureVideoFrame(video);
244
+ });
245
+
246
+ // Update duration when metadata is loaded
247
+ video.addEventListener('loadedmetadata', function() {
248
+ videoObj.duration = Math.round(video.duration);
249
+ const durationSpan = durationEl.querySelector('span');
250
+ if (durationSpan) {
251
+ durationSpan.textContent = formatDuration(videoObj.duration);
252
+ }
253
+ });
254
+
255
+ // Update progress bar
256
+ video.addEventListener('timeupdate', function() {
257
+ if (videos.indexOf(videoObj) === currentIndex) {
258
+ const progress = (video.currentTime / video.duration) * 100;
259
+ progressFill.style.width = `${progress}%`;
260
+ }
261
+ });
262
+
263
+ // Handle playback end (for non-looping videos)
264
+ video.addEventListener('ended', function() {
265
+ if (!video.loop) {
266
+ // Automatically advance to next video
267
+ switchToVideo(currentIndex + 1);
268
+ }
269
+ });
270
+
271
+ // Handle video errors
272
+ video.addEventListener('error', function() {
273
+ console.error('Video load error:', videoObj.url);
274
+ App.showToast('视频加载失败,正在尝试下一个');
275
+ if (videos.indexOf(videoObj) === currentIndex) {
276
+ switchToVideo(currentIndex + 1);
277
+ }
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Capture a frame from video to use as thumbnail
283
+ * @param {HTMLVideoElement} video Video element
284
+ * @returns {String} Data URL of thumbnail
285
+ */
286
+ function captureVideoFrame(video) {
287
+ try {
288
+ const canvas = document.createElement('canvas');
289
+ canvas.width = video.videoWidth;
290
+ canvas.height = video.videoHeight;
291
+
292
+ const ctx = canvas.getContext('2d');
293
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
294
+
295
+ return canvas.toDataURL('image/jpeg', 0.7);
296
+ } catch (e) {
297
+ console.error('Error capturing video frame:', e);
298
+ return null;
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Switch to a different video
304
+ * @param {Number} index Index of video to switch to
305
+ */
306
+ function switchToVideo(index) {
307
+ if (index < 0 || isLoading) return;
308
+
309
+ // Handle case where requested index exceeds available videos
310
+ if (index >= videos.length) {
311
+ // Show loading toast and load more videos
312
+ App.showToast('正在加载新视频...');
313
+ loadVideos(1, false).then(() => {
314
+ // After loading, try again if video exists now
315
+ if (index < videos.length) {
316
+ switchToVideo(index);
317
+ }
318
+ });
319
+ return;
320
+ }
321
+
322
+ // Pause all videos
323
+ videos.forEach(item => {
324
+ item.video.pause();
325
+ });
326
+
327
+ // Update current index
328
+ currentIndex = index;
329
+
330
+ // Update video positions
331
+ updateVideoPositions();
332
+
333
+ // Play current video
334
+ playCurrentVideo();
335
+
336
+ // Reset progress bar
337
+ progressFill.style.width = '0';
338
+
339
+ // Update action buttons
340
+ updateActionButtons();
341
+
342
+ // Add to history
343
+ if (videos[currentIndex]) {
344
+ StorageManager.addToHistory(videos[currentIndex]);
345
+ }
346
+
347
+ // Preload more videos when close to end
348
+ if (currentIndex >= videos.length - 1) {
349
+ loadVideos(2, false);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Play the current video
355
+ */
356
+ function playCurrentVideo() {
357
+ if (!videos[currentIndex]) return;
358
+
359
+ const video = videos[currentIndex].video;
360
+
361
+ // Try to play and handle autoplay restrictions
362
+ const playPromise = video.play();
363
+
364
+ if (playPromise !== undefined) {
365
+ playPromise.catch(error => {
366
+ console.log('Autoplay prevented, trying muted playback');
367
+ // Auto-play was prevented, set muted and try again
368
+ video.muted = true;
369
+ isMuted = true;
370
+ updateVolumeUI();
371
+ video.play().catch(e => {
372
+ console.error('Failed to play video even with mute:', e);
373
+ // If even muted autoplay fails, show a play button overlay
374
+ App.showToast('点击视频开始播放');
375
+ });
376
+ });
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Update positions of all videos
382
+ */
383
+ function updateVideoPositions() {
384
+ videos.forEach((item, idx) => {
385
+ item.element.style.transform = `translateY(${(idx - currentIndex) * 100}%)`;
386
+ });
387
+ }
388
+
389
+ /**
390
+ * Update action buttons state based on current video
391
+ */
392
+ function updateActionButtons() {
393
+ if (!videos[currentIndex]) return;
394
+
395
+ const videoId = videos[currentIndex].id;
396
+
397
+ // Update like button
398
+ if (StorageManager.isLiked(videoId)) {
399
+ likeButton.classList.add('active');
400
+ } else {
401
+ likeButton.classList.remove('active');
402
+ }
403
+
404
+ // Update favorite button
405
+ if (StorageManager.isFavorite(videoId)) {
406
+ favoriteButton.classList.add('active');
407
+ } else {
408
+ favoriteButton.classList.remove('active');
409
+ }
410
+
411
+ // Update counts
412
+ likeCount.textContent = Math.floor(videos[currentIndex].likes);
413
+ favoriteCount.textContent = StorageManager.isFavorite(videoId) ? '1' : '0';
414
+ }
415
+
416
+ /**
417
+ * Handle touch start event
418
+ * @param {Event} e Touch event
419
+ */
420
+ function handleTouchStart(e) {
421
+ touchStartY = e.touches[0].clientY;
422
+ }
423
+
424
+ /**
425
+ * Handle touch end event
426
+ * @param {Event} e Touch event
427
+ */
428
+ function handleTouchEnd(e) {
429
+ // Only process touch events on home screen
430
+ if (!document.getElementById('homeScreen').classList.contains('active')) {
431
+ return;
432
+ }
433
+
434
+ touchEndY = e.changedTouches[0].clientY;
435
+ const deltaY = touchStartY - touchEndY;
436
+
437
+ // If swipe distance is significant
438
+ if (Math.abs(deltaY) > 50) {
439
+ if (deltaY > 0) {
440
+ // Swipe up - next video
441
+ switchToVideo(currentIndex + 1);
442
+ } else {
443
+ // Swipe down - previous video
444
+ switchToVideo(currentIndex - 1);
445
+ }
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Handle click on video feed
451
+ * @param {Event} e Click event
452
+ */
453
+ function handleVideoClick(e) {
454
+ // Ignore clicks on controls
455
+ if (e.target.closest('.action-button') ||
456
+ e.target.closest('.volume-control')) {
457
+ return;
458
+ }
459
+
460
+ // Toggle play/pause
461
+ if (!videos[currentIndex]) return;
462
+
463
+ const video = videos[currentIndex].video;
464
+
465
+ if (video.paused) {
466
+ video.play();
467
+ playIcon.style.display = 'block';
468
+ pauseIcon.style.display = 'none';
469
+ } else {
470
+ video.pause();
471
+ playIcon.style.display = 'none';
472
+ pauseIcon.style.display = 'block';
473
+ }
474
+
475
+ // Show play/pause overlay
476
+ playPauseOverlay.classList.add('visible');
477
+ setTimeout(() => {
478
+ playPauseOverlay.classList.remove('visible');
479
+ }, 800);
480
+ }
481
+
482
+ /**
483
+ * Toggle mute state
484
+ */
485
+ function toggleMute() {
486
+ isMuted = !isMuted;
487
+
488
+ // Update all videos
489
+ videos.forEach(item => {
490
+ item.video.muted = isMuted;
491
+ });
492
+
493
+ // Update UI
494
+ updateVolumeUI();
495
+
496
+ // Show toast
497
+ App.showToast(isMuted ? '已静音' : '已开启声音');
498
+ }
499
+
500
+ /**
501
+ * Update volume control UI
502
+ */
503
+ function updateVolumeUI() {
504
+ if (isMuted) {
505
+ volumeOnIcon.style.display = 'none';
506
+ volumeOffIcon.style.display = 'block';
507
+ } else {
508
+ volumeOnIcon.style.display = 'block';
509
+ volumeOffIcon.style.display = 'none';
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Toggle like for current video
515
+ */
516
+ function toggleLike() {
517
+ if (!videos[currentIndex]) return;
518
+
519
+ const videoId = videos[currentIndex].id;
520
+ const newLikeState = StorageManager.toggleLike(videoId);
521
+
522
+ // Update UI
523
+ if (newLikeState) {
524
+ likeButton.classList.add('active');
525
+ videos[currentIndex].likes++;
526
+ App.showToast('已添加到喜欢');
527
+ } else {
528
+ likeButton.classList.remove('active');
529
+ videos[currentIndex].likes--;
530
+ App.showToast('已取消喜欢');
531
+ }
532
+
533
+ // Update count
534
+ likeCount.textContent = Math.floor(videos[currentIndex].likes);
535
+
536
+ // Update profile stats
537
+ App.updateStats();
538
+ }
539
+
540
+ /**
541
+ * Toggle favorite for current video
542
+ */
543
+ function toggleFavorite() {
544
+ if (!videos[currentIndex]) return;
545
+
546
+ const newFavoriteState = StorageManager.toggleFavorite(videos[currentIndex]);
547
+
548
+ // Update UI
549
+ if (newFavoriteState) {
550
+ favoriteButton.classList.add('active');
551
+ favoriteCount.textContent = '1';
552
+ App.showToast('已添加到收藏');
553
+ } else {
554
+ favoriteButton.classList.remove('active');
555
+ favoriteCount.textContent = '0';
556
+ App.showToast('已取消收藏');
557
+ }
558
+
559
+ // Update profile stats
560
+ App.updateStats();
561
+ }
562
+
563
+ /**
564
+ * Play a video from the history or favorites
565
+ * @param {Object} videoData Video data from storage
566
+ */
567
+ function playVideoFromLibrary(videoData) {
568
+ if (!videoData || !videoData.url) return;
569
+
570
+ // Check if video is already loaded
571
+ const existingIndex = videos.findIndex(v => v.id === videoData.id);
572
+
573
+ if (existingIndex !== -1) {
574
+ // Switch to existing video
575
+ switchToVideo(existingIndex);
576
+ } else {
577
+ // Create new video element
578
+ createVideoElement(videoData.url);
579
+
580
+ // Switch to the new video
581
+ switchToVideo(videos.length - 1);
582
+ }
583
+
584
+ // Switch to home screen
585
+ App.switchToScreen('homeScreen');
586
+ }
587
+
588
+ /**
589
+ * Generate a unique video ID
590
+ * @returns {String} Unique ID
591
+ */
592
+ function generateVideoId() {
593
+ return 'v_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
594
+ }
595
+
596
+ /**
597
+ * Format duration in seconds to MM:SS
598
+ * @param {Number} seconds Duration in seconds
599
+ * @returns {String} Formatted duration
600
+ */
601
+ function formatDuration(seconds) {
602
+ if (!seconds) return '00:00';
603
+
604
+ const minutes = Math.floor(seconds / 60);
605
+ const remainingSeconds = Math.floor(seconds % 60);
606
+ return `${minutes < 10 ? '0' : ''}${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
607
+ }
608
+
609
+ /**
610
+ * Get random video description
611
+ * @returns {String} Random description
612
+ */
613
+ function getRandomDescription() {
614
+ const descriptions = [
615
+ "精选高质量视频,带给你视���享受",
616
+ "每日精选,让你流连忘返",
617
+ "热门推荐,不容错过的精彩瞬间",
618
+ "探索更多精彩内容,尽在VideoCharm",
619
+ "发现生活中的美好瞬间",
620
+ "精彩纷呈,尽在眼前",
621
+ "让心情放松的精选内容",
622
+ "视觉盛宴,触手可及"
623
+ ];
624
+
625
+ return descriptions[Math.floor(Math.random() * descriptions.length)];
626
+ }
627
+
628
+ // Public API
629
+ return {
630
+ init: init,
631
+ loadVideos: loadVideos,
632
+ switchToVideo: switchToVideo,
633
+ playVideoFromLibrary: playVideoFromLibrary,
634
+ updateVideoPositions: updateVideoPositions
635
+ };
636
  })();
static/js/storage.js CHANGED
@@ -122,6 +122,10 @@ const StorageManager = (function() {
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 = {
@@ -191,76 +195,76 @@ const StorageManager = (function() {
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
  })();
 
122
  if (existingIndex !== -1) {
123
  // Update timestamp of existing item
124
  history[existingIndex].timestamp = new Date().getTime();
125
+ // Update thumbnail if available
126
+ if (video.thumbnail) {
127
+ history[existingIndex].thumbnail = video.thumbnail;
128
+ }
129
  } else {
130
  // Create simplified record for storage
131
  const historyItem = {
 
195
  }
196
 
197
  /**
198
+ * Toggle like status for a video
199
+ * @param {String} videoId Video ID
200
+ * @returns {Boolean} New like status
201
+ */
202
+ function toggleLike(videoId) {
203
+ if (!videoId) return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
+ const likes = getLikes();
206
+ const index = likes.indexOf(videoId);
 
 
 
 
 
 
 
 
 
207
 
208
+ if (index !== -1) {
209
+ // Remove like
210
+ likes.splice(index, 1);
211
+ localStorage.setItem(KEYS.LIKES, JSON.stringify(likes));
212
+ return false;
213
+ } else {
214
+ // Add like
215
+ likes.push(videoId);
216
+ localStorage.setItem(KEYS.LIKES, JSON.stringify(likes));
217
+ return true;
 
 
 
 
 
 
 
 
 
 
218
  }
219
+ }
220
+
221
+ /**
222
+ * Check if a video is liked
223
+ * @param {String} videoId Video ID to check
224
+ * @returns {Boolean} Whether video is liked
225
+ */
226
+ function isLiked(videoId) {
227
+ if (!videoId) return false;
228
 
229
+ const likes = getLikes();
230
+ return likes.includes(videoId);
231
+ }
232
+
233
+ /**
234
+ * Increment view count
235
+ */
236
+ function incrementViews() {
237
+ const views = getViews();
238
+ localStorage.setItem(KEYS.VIEWS, JSON.stringify(views + 1));
239
+ }
240
+
241
+ /**
242
+ * Clear all history
243
+ */
244
+ function clearHistory() {
245
+ localStorage.setItem(KEYS.HISTORY, JSON.stringify([]));
246
+ }
247
+
248
+ /**
249
+ * Clear all favorites
250
+ */
251
+ function clearFavorites() {
252
+ localStorage.setItem(KEYS.FAVORITES, JSON.stringify([]));
253
+ }
254
+
255
+ // Public API
256
+ return {
257
+ init: initStorage,
258
+ getHistory: getHistory,
259
+ getFavorites: getFavorites,
260
+ getLikes: getLikes,
261
+ getViews: getViews,
262
+ addToHistory: addToHistory,
263
+ toggleFavorite: toggleFavorite,
264
+ isFavorite: isFavorite,
265
+ toggleLike: toggleLike,
266
+ isLiked: isLiked,
267
+ clearHistory: clearHistory,
268
+ clearFavorites: clearFavorites
269
+ };
270
  })();