diff --git a/app.py b/app.py index 7c0a7f1..ec2beac 100644 --- a/app.py +++ b/app.py @@ -95,6 +95,8 @@ def get_local_timestamp(): TIMESTAMP_FIELD = None +POSTS_PER_PAGE = 10 + def resolve_timestamp_field(): global TIMESTAMP_FIELD @@ -175,6 +177,27 @@ def check_persistent_login(): print(f"Error checking persistent login: {e}") return redirect(url_for("home")) +def fetch_video_data(video_id: int): + try: + video_resp = ( + supabase_client.table("videos") + .select("filepath", "filename", "status") + .eq("id", video_id) + .single() + .execute() + ) + video_record = video_resp.data + if video_record: + video_data = { + "id": video_id, + "filename": video_record.get("filename"), + "filepath": video_record.get("filepath"), + "status": video_record.get("status"), + "url": video_record.get("filepath"), + } + return video_data + except Exception as e: + print(f"Error fetching video info for video_id={video_id}: {e}") @app.route("/") def home(): @@ -192,6 +215,7 @@ def home(): supabase_client.table("posts") .select(select_fields) .order(TIMESTAMP_FIELD if TIMESTAMP_FIELD else "id", desc=True) + .limit(POSTS_PER_PAGE) .execute() ) posts_data = response.data or [] @@ -217,28 +241,7 @@ def home(): else: formatted_timestamp = "" - videodata = None video_id = post.get("video_id") - if video_id: - try: - video_resp = ( - supabase_client.table("videos") - .select("filepath", "filename", "status") - .eq("id", video_id) - .single() - .execute() - ) - video_record = video_resp.data - if video_record: - videodata = { - "id": video_id, - "filename": video_record.get("filename"), - "filepath": video_record.get("filepath"), - "status": video_record.get("status"), - "url": video_record.get("filepath"), - } - except Exception as e: - print(f"Error fetching video info for video_id={video_id}: {e}") posts.append( ( @@ -247,7 +250,7 @@ def home(): Markup(post.get("content", "")), post.get("image"), formatted_timestamp, - videodata, + fetch_video_data(video_id) if video_id else None, ) ) @@ -313,7 +316,6 @@ def home(): dark_mode=True, ) - @app.route("/filter", methods=["GET"]) def filter_posts(): year = request.args.get("year", "any") @@ -862,28 +864,7 @@ def view_post(post_id): image_url = post.get("image") - videodata = None video_id = post.get("video_id") - if video_id: - try: - video_resp = ( - supabase_client.table("videos") - .select("filepath", "filename", "status") - .eq("id", video_id) - .single() - .execute() - ) - video_record = video_resp.data - if video_record: - videodata = { - "id": video_id, - "filename": video_record.get("filename"), - "filepath": video_record.get("filepath"), - "status": video_record.get("status"), - "url": video_record.get("filepath"), - } - except Exception as e: - print(f"Error fetching video info for video_id={video_id}: {e}") return render_template( "post.html", @@ -893,7 +874,7 @@ def view_post(post_id): "content": Markup(post.get("content", "")), "image": image_url, "timestamp": formatted_timestamp, - "video": videodata, + "video": fetch_video_data(video_id) if video_id else None, }, dark_mode=True, ) @@ -904,7 +885,7 @@ def view_post(post_id): @app.route("/admin/inspect") def admin_inspect(): if "admin" not in session: - return jsonify({"error": "admin only"}), 403 + return jsonify({"is_admin": False, "error": "admin only"}), 403 # Mask the key for safety in logs/UI masked_key = None @@ -939,7 +920,10 @@ def admin_inspect(): except Exception as e: info["posts_count_error"] = str(e) - return jsonify(info) + return jsonify({ + "is_admin": True, + "info": info + }) @app.route("/uploads/") @@ -948,6 +932,64 @@ def uploaded_file(filename): os.makedirs(UPLOAD_FOLDER, exist_ok=True) return send_from_directory(UPLOAD_FOLDER, filename) +# --- API ENDPOINTS --- # +@app.route("/api/posts", methods=["GET"]) +def api_post(): + try: + page = int(request.args.get("page", 1)) + except (TypeError, ValueError): + page = 1 + + offset = max(0, (page - 1) * POSTS_PER_PAGE) + + select_fields = ( + f"id, title, content, image, {TIMESTAMP_FIELD}, video_id" + if TIMESTAMP_FIELD + else "id, title, content, image, video_id" + ) + + try: + # Fetch posts for requested page + res = supabase_client.table("posts").select(select_fields).order(TIMESTAMP_FIELD, desc=True).offset(offset).limit(POSTS_PER_PAGE).execute() + posts_data = res.data or [] + + for post in posts_data: + ts_value = post.get(TIMESTAMP_FIELD) if TIMESTAMP_FIELD else None + if ts_value: + try: + dt_object = parser.parse(ts_value).replace(tzinfo=None) + formatted_timestamp = dt_object.strftime("%Y-%m-%d %I:%M %p") + except Exception: + formatted_timestamp = str(ts_value) + else: + formatted_timestamp = "" + post["formatted_timestamp"] = formatted_timestamp + + video_id = post.get("video_id") + if video_id: + video_data = fetch_video_data(video_id) + post["video"] = video_data + + # Check if there is a next page + res = supabase_client.table("posts").select("id").order(TIMESTAMP_FIELD, desc=True).offset(offset + POSTS_PER_PAGE).limit(1).execute() + has_next = bool(res.data) + + return jsonify({ + "posts": posts_data, + "has_next": has_next + }) + + except Exception as e: + print(f"Error fetching posts in api_post: {e}") + return jsonify({"error": "Failed to fetch posts", "details": str(e)}), 500 + +@app.route("/api/check_admin", methods=["GET"]) +def api_check_admin(): + if "admin" in session: + return jsonify({"is_admin": True}) + else: + return jsonify({"is_admin": False}) + if __name__ == "__main__": """ diff --git a/static/css/style.css b/static/css/style.css index 283b949..17adc74 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -720,4 +720,23 @@ video { .fileinput { display: none; -} \ No newline at end of file +} + +/* Loader */ +/* HTML:
*/ +.loader { + width: 50px; + padding: 8px; + aspect-ratio: 1; + border-radius: 50%; + background: var(--primary-color); + --_m: + conic-gradient(#0000 10%,#000), + linear-gradient(#000 0 0) content-box; + -webkit-mask: var(--_m); + mask: var(--_m); + -webkit-mask-composite: source-out; + mask-composite: subtract; + animation: l3 1s infinite linear; +} +@keyframes l3 {to{transform: rotate(1turn)}} \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js index 62373b0..3b18916 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -100,4 +100,170 @@ document.addEventListener('DOMContentLoaded', function () { viewer.show(); }); }); +}); + +// Lazy Loading for posts +document.addEventListener('DOMContentLoaded', () => { + const postsContainer = document.getElementById('posts-container'); + const loadingIndicator = document.getElementById('loading-indicator'); + const endOfPostsMessage = document.getElementById('end-of-posts-message'); + + if (!postsContainer) return; + + let page = 2; // Start with page 2, since page 1 is loaded initially + let isLoading = false; + let hasNext = true; + + const loadMorePosts = async () => { + if (isLoading || !hasNext) return; + + isLoading = true; + loadingIndicator.style.display = 'flex'; + loadingIndicator.style.alignItems = 'center'; + loadingIndicator.style.alignContent = 'center'; + loadingIndicator.style.justifyContent = 'center'; + + try { + const response = await fetch(`/api/posts?page=${page}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + + if (data.posts && data.posts.length > 0) { + const isAdmin = await fetch('/api/check_admin') + .then(res => res.json()) + .then(resData => resData.is_admin) + .catch(() => false); + data.posts.forEach(post => { + const postLink = document.createElement('div'); + postLink.className = 'post'; + + let adminLinks = ''; + if (isAdmin) { + adminLinks = ` + Edit + Delete + `; + } + + let imageHTML = ''; + if (post.image) { + imageHTML = ` + +
+ `; + } + + let videoHTML = ''; + if (post.video) { + const video = post.video; + videoHTML = ` +
+
+ Play +
+ +
+
+ +
+ +
+ 00:00 / 00:00 +
+
+
+ + +
+
+ +
    +
  • 0.5x
  • +
  • 0.75x
  • +
  • 1x
  • +
  • 1.25x
  • +
  • 1.5x
  • +
  • 2x
  • +
+
+
+ +
    +
    + + +
    +
    +
    + `; + } + + postLink.innerHTML = ` + +

    ${post.title}

    +

    ${post.content}

    + ${imageHTML} +
    + ${videoHTML} + + Posted on ${post.formatted_timestamp || (post.timestamp ? new Date(post.timestamp).toLocaleDateString() : '')} +
    + ${adminLinks} + + `; + postsContainer.appendChild(postLink); + + if (post.video) { + const newVideoContainer = postLink.querySelector('.video-container'); + if (newVideoContainer && typeof initializeVideoPlayer === 'function') { + initializeVideoPlayer(newVideoContainer); + } + } + }); + page++; + } + + hasNext = data.has_next; + if (!hasNext) { + endOfPostsMessage.style.display = 'block'; + } + + } catch (error) { + console.error("Failed to load more posts:", error); + // Optionally, display an error message to the user + } finally { + isLoading = false; + loadingIndicator.style.display = 'none'; + } + }; + + window.addEventListener('scroll', () => { + // Load more posts when the user is 100px from the bottom + if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 100) { + loadMorePosts(); + } + }); }); \ No newline at end of file diff --git a/static/js/video_player.js b/static/js/video_player.js index 78396d1..ed9bbd4 100644 --- a/static/js/video_player.js +++ b/static/js/video_player.js @@ -1,359 +1,355 @@ document.addEventListener("DOMContentLoaded", () => { const videoContainers = document.querySelectorAll(".video-container"); + videoContainers.forEach(initializeVideoPlayer); +}); - videoContainers.forEach((container) => { - const videoEl = container.querySelector(".video-player"); - const url = videoEl.dataset.url; - const status = videoEl.dataset.status; - - // Get control elements - const playOverlay = container.querySelector(".play-overlay"); - const playPauseBtn = container.querySelector(".play-pause-btn"); - const playPauseIcon = playPauseBtn.querySelector(".play-icon"); - const progressBar = container.querySelector(".progress-bar"); - const currentTimeDisplay = container.querySelector(".current-time-display"); - const durationDisplay = container.querySelector(".duration-display"); - const muteBtn = container.querySelector(".mute-btn"); - const volumeIcon = muteBtn.querySelector(".volume-icon"); - const volumeSlider = container.querySelector(".volume-slider"); - const fullscreenBtn = container.querySelector(".fullscreen-btn"); - const fullscreenIcon = fullscreenBtn.querySelector(".fullscreen-icon"); - const qualityBtn = container.querySelector(".quality-btn"); - const qualityOptionsUl = container.querySelector(".quality-options"); - const speedBtn = container.querySelector(".speed-btn"); - const speedOptionsUl = container.querySelector(".speed-options"); - const pipBtn = container.querySelector(".pip-btn"); - const controlsOverlay = container.querySelector(".controls-overlay"); - - let hlsInstance = null; - let currentQualityLevel = -1; // -1 for auto - let controlsTimeout; - - // --- Helper Functions --- - function formatTime(seconds) { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = Math.floor(seconds % 60); - return `${minutes.toString().padStart(2, "0")}:${remainingSeconds - .toString() - .padStart(2, "0")}`; +function initializeVideoPlayer(container) { + const videoEl = container.querySelector(".video-player"); + if (!videoEl) { + console.error("Video player element not found in container."); + return; + } + const url = videoEl.dataset.url; + const status = videoEl.dataset.status; + + // Get control elements + const playOverlay = container.querySelector(".play-overlay"); + const playPauseBtn = container.querySelector(".play-pause-btn"); + const playPauseIcon = playPauseBtn.querySelector(".play-icon"); + const progressBar = container.querySelector(".progress-bar"); + const currentTimeDisplay = container.querySelector(".current-time-display"); + const durationDisplay = container.querySelector(".duration-display"); + const muteBtn = container.querySelector(".mute-btn"); + const volumeIcon = muteBtn.querySelector(".volume-icon"); + const volumeSlider = container.querySelector(".volume-slider"); + const fullscreenBtn = container.querySelector(".fullscreen-btn"); + const fullscreenIcon = fullscreenBtn.querySelector(".fullscreen-icon"); + const qualityBtn = container.querySelector(".quality-btn"); + const qualityOptionsUl = container.querySelector(".quality-options"); + const speedBtn = container.querySelector(".speed-btn"); + const speedOptionsUl = container.querySelector(".speed-options"); + const pipBtn = container.querySelector(".pip-btn"); + const controlsOverlay = container.querySelector(".controls-overlay"); + + let hlsInstance = null; + let currentQualityLevel = -1; // -1 for auto + let controlsTimeout; + + // --- Helper Functions --- + function formatTime(seconds) { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes.toString().padStart(2, "0")}:${remainingSeconds + .toString() + .padStart(2, "0")}`; + } + + function updateSliderBackground(slider) { + const percentage = + ((slider.value - slider.min) / (slider.max - slider.min)) * 100; + slider.style.background = `linear-gradient(to right, var(--primary-color) 0%, var(--primary-color) ${percentage}%,rgba(255, 255, 255, 0.4) ${percentage}%, rgba(255, 255, 255, 0.4) 100%)`; + } + + function togglePlayPause() { + if (videoEl.paused || videoEl.ended) { + videoEl.play(); + } else { + videoEl.pause(); } - - function updateSliderBackground(slider) { - const percentage = - ((slider.value - slider.min) / (slider.max - slider.min)) * 100; - const primaryColor = getComputedStyle(document.documentElement) - .getPropertyValue("--primary-color") - .trim(); - slider.style.background = `linear-gradient(to right, var(--primary-color) 0%, var(--primary-color) ${percentage}%,rgba(255, 255, 255, 0.4) ${percentage}%, rgba(255, 255, 255, 0.4) 100%)`; + } + + function updatePlayPauseButton() { + if (videoEl.paused || videoEl.ended) { + playPauseIcon.src = "/static/svg/play.svg"; + playOverlay.classList.remove("hidden"); + container.classList.remove("playing"); + container.classList.add("paused"); + showControls(); + } else { + playPauseIcon.src = "/static/svg/pause.svg"; + playOverlay.classList.add("hidden"); + container.classList.add("playing"); + container.classList.remove("paused"); + hideControlsAfterDelay(); } + } - function togglePlayPause() { - if (videoEl.paused || videoEl.ended) { - videoEl.play(); - } else { - videoEl.pause(); - } - } + function updateProgressBar() { + progressBar.value = (videoEl.currentTime / videoEl.duration) * 100; + currentTimeDisplay.textContent = formatTime(videoEl.currentTime); + updateSliderBackground(progressBar); + } - function updatePlayPauseButton() { - if (videoEl.paused || videoEl.ended) { - playPauseIcon.src = "/static/svg/play.svg"; - playOverlay.classList.remove("hidden"); - container.classList.remove("playing"); - container.classList.add("paused"); - showControls(); - } else { - playPauseIcon.src = "/static/svg/pause.svg"; - playOverlay.classList.add("hidden"); - container.classList.add("playing"); - container.classList.remove("paused"); - hideControlsAfterDelay(); - } - } + function seekVideo() { + const seekTime = (progressBar.value / 100) * videoEl.duration; + videoEl.currentTime = seekTime; + } - function updateProgressBar() { - progressBar.value = (videoEl.currentTime / videoEl.duration) * 100; - currentTimeDisplay.textContent = formatTime(videoEl.currentTime); - updateSliderBackground(progressBar); - } - - function seekVideo() { - const seekTime = (progressBar.value / 100) * videoEl.duration; - videoEl.currentTime = seekTime; + function toggleMute() { + videoEl.muted = !videoEl.muted; + updateMuteButton(); + volumeSlider.value = videoEl.muted ? 0 : videoEl.volume * 100; + updateSliderBackground(volumeSlider); + } + + function updateMuteButton() { + if (videoEl.muted || videoEl.volume === 0) { + volumeIcon.src = "/static/svg/volume-mute.svg"; + } else { + volumeIcon.src = "/static/svg/volume-up.svg"; } + } - function toggleMute() { - videoEl.muted = !videoEl.muted; - updateMuteButton(); - volumeSlider.value = videoEl.muted ? 0 : videoEl.volume * 100; - updateSliderBackground(volumeSlider); - } + function changeVolume() { + videoEl.volume = volumeSlider.value / 100; + videoEl.muted = videoEl.volume === 0; + updateMuteButton(); + updateSliderBackground(volumeSlider); + } - function updateMuteButton() { - if (videoEl.muted || videoEl.volume === 0) { - volumeIcon.src = "/static/svg/volume-mute.svg"; - } else { - volumeIcon.src = "/static/svg/volume-up.svg"; - } + function toggleFullscreen() { + if (!document.fullscreenElement) { + container.requestFullscreen().catch((err) => { + console.error( + `Error attempting to enable full-screen mode: ${err.message} (${err.name})` + ); + }); + } else { + document.exitFullscreen(); } - - function changeVolume() { - videoEl.volume = volumeSlider.value / 100; - videoEl.muted = videoEl.volume === 0; - updateMuteButton(); - updateSliderBackground(volumeSlider); + } + + function updateFullscreenButton() { + if (document.fullscreenElement === container) { + fullscreenIcon.src = "/static/svg/fullscreen-exit.svg"; + container.classList.add("fullscreen"); + } else { + fullscreenIcon.src = "/static/svg/fullscreen.svg"; + container.classList.remove("fullscreen"); } + } - function toggleFullscreen() { - if (!document.fullscreenElement) { - container.requestFullscreen().catch((err) => { - console.error( - `Error attempting to enable full-screen mode: ${err.message} (${err.name})` - ); - }); - } else { - document.exitFullscreen(); - } - } + function buildQualityOptions(levels) { + qualityOptionsUl.innerHTML = ""; // Clear existing options - function updateFullscreenButton() { - if (document.fullscreenElement === container) { - fullscreenIcon.src = "/static/svg/fullscreen-exit.svg"; - container.classList.add("fullscreen"); - } else { - fullscreenIcon.src = "/static/svg/fullscreen.svg"; - container.classList.remove("fullscreen"); - } + // Add 'Auto' option + const autoLi = document.createElement("li"); + autoLi.textContent = "Auto"; + autoLi.dataset.level = -1; + if (currentQualityLevel === -1) { + autoLi.classList.add("active"); } - - function buildQualityOptions(levels) { - qualityOptionsUl.innerHTML = ""; // Clear existing options - - // Add 'Auto' option - const autoLi = document.createElement("li"); - autoLi.textContent = "Auto"; - autoLi.dataset.level = -1; - if (currentQualityLevel === -1) { - autoLi.classList.add("active"); - } - autoLi.addEventListener("click", () => selectQualityLevel(-1)); - qualityOptionsUl.appendChild(autoLi); - - levels - .sort((a, b) => b.height - a.height) - .forEach((level, index) => { - const li = document.createElement("li"); - li.textContent = `${level.height}p`; - li.dataset.level = index; // Use index for Hls.js level - if (currentQualityLevel === index) { - li.classList.add("active"); - } - li.addEventListener("click", () => selectQualityLevel(index)); - qualityOptionsUl.appendChild(li); - }); - } - - function selectQualityLevel(levelIndex) { - if (hlsInstance) { - hlsInstance.currentLevel = levelIndex; - currentQualityLevel = levelIndex; - // Update active class - qualityOptionsUl.querySelectorAll("li").forEach((li) => { - li.classList.remove("active"); - if (parseInt(li.dataset.level) === levelIndex) { - li.classList.add("active"); - } - }); - } - qualityOptionsUl.classList.remove("active"); // Hide options after selection - } - - function changePlaybackSpeed(speed) { - videoEl.playbackRate = speed; - speedBtn.textContent = `${speed}x`; - speedOptionsUl.querySelectorAll("li").forEach((li) => { + autoLi.addEventListener("click", () => selectQualityLevel(-1)); + qualityOptionsUl.appendChild(autoLi); + + levels + .sort((a, b) => b.height - a.height) + .forEach((level, index) => { + const li = document.createElement("li"); + li.textContent = `${level.height}p`; + li.dataset.level = index; // Use index for Hls.js level + if (currentQualityLevel === index) { + li.classList.add("active"); + } + li.addEventListener("click", () => selectQualityLevel(index)); + qualityOptionsUl.appendChild(li); + }); + } + + function selectQualityLevel(levelIndex) { + if (hlsInstance) { + hlsInstance.currentLevel = levelIndex; + currentQualityLevel = levelIndex; + // Update active class + qualityOptionsUl.querySelectorAll("li").forEach((li) => { li.classList.remove("active"); - if (parseFloat(li.dataset.speed) === speed) { + if (parseInt(li.dataset.level) === levelIndex) { li.classList.add("active"); } }); - speedOptionsUl.classList.remove("active"); } + qualityOptionsUl.classList.remove("active"); // Hide options after selection + } - function togglePip() { - if (document.pictureInPictureElement) { - document.exitPictureInPicture(); - } else if (videoEl.requestPictureInPicture) { - videoEl.requestPictureInPicture(); + function changePlaybackSpeed(speed) { + videoEl.playbackRate = speed; + speedBtn.textContent = `${speed}x`; + speedOptionsUl.querySelectorAll("li").forEach((li) => { + li.classList.remove("active"); + if (parseFloat(li.dataset.speed) === speed) { + li.classList.add("active"); } + }); + speedOptionsUl.classList.remove("active"); + } + + function togglePip() { + if (document.pictureInPictureElement) { + document.exitPictureInPicture(); + } else if (videoEl.requestPictureInPicture) { + videoEl.requestPictureInPicture(); } - - function hideControls() { - container.classList.remove("show-controls"); - container.classList.add("hide-controls"); + } + + function hideControls() { + container.classList.remove("show-controls"); + container.classList.add("hide-controls"); + } + + function showControls() { + container.classList.remove("hide-controls"); + container.classList.add("show-controls"); + clearTimeout(controlsTimeout); + if (!videoEl.paused) { + hideControlsAfterDelay(); } + } - function showControls() { - container.classList.remove("hide-controls"); - container.classList.add("show-controls"); - clearTimeout(controlsTimeout); - if (!videoEl.paused) { - hideControlsAfterDelay(); + function hideControlsAfterDelay() { + clearTimeout(controlsTimeout); + controlsTimeout = setTimeout(() => { + if (!videoEl.paused && !container.matches(":hover")) { + hideControls(); } - } - - function hideControlsAfterDelay() { - clearTimeout(controlsTimeout); - controlsTimeout = setTimeout(() => { - if (!videoEl.paused && !container.matches(":hover")) { - hideControls(); + }, 3000); // Hide after 3 seconds of inactivity + } + + // --- Initialize HLS or Native Playback --- + if (url && status === "processed") { + if (typeof Hls !== "undefined" && Hls.isSupported()) { + hlsInstance = new Hls(); + hlsInstance.loadSource(url); + hlsInstance.attachMedia(videoEl); + + hlsInstance.on(Hls.Events.MANIFEST_PARSED, function (event, data) { + console.log("HLS Manifest Parsed. Available levels:", data.levels); + if (data.levels && data.levels.length > 1) { + buildQualityOptions(data.levels); + qualityBtn.style.display = ""; + } else { + qualityBtn.style.display = "none"; } - }, 3000); // Hide after 3 seconds of inactivity - } + }); - // --- Initialize HLS or Native Playback --- - if (url && status === "processed") { - if (Hls.isSupported()) { - hlsInstance = new Hls(); - hlsInstance.loadSource(url); - hlsInstance.attachMedia(videoEl); - - hlsInstance.on(Hls.Events.MANIFEST_PARSED, function (event, data) { - console.log("HLS Manifest Parsed. Available levels:", data.levels); - if (data.levels && data.levels.length > 1) { - // Only build if there are multiple quality levels - buildQualityOptions(data.levels); - qualityBtn.style.display = ""; // Show quality button if levels are available - } else { - qualityBtn.style.display = "none"; // Hide if no quality levels + hlsInstance.on(Hls.Events.LEVEL_SWITCHED, function (event, data) { + currentQualityLevel = data.level; + qualityOptionsUl.querySelectorAll("li").forEach((li) => { + li.classList.remove("active"); + if (parseInt(li.dataset.level) === data.level) { + li.classList.add("active"); } }); + }); - hlsInstance.on(Hls.Events.LEVEL_SWITCHED, function (event, data) { - currentQualityLevel = data.level; - // Update UI if needed, e.g., highlight active quality - qualityOptionsUl.querySelectorAll("li").forEach((li) => { - li.classList.remove("active"); - if (parseInt(li.dataset.level) === data.level) { - li.classList.add("active"); - } - }); - }); - - hlsInstance.on(Hls.Events.ERROR, function (event, data) { - if (data.fatal) { - switch (data.type) { - case Hls.ErrorTypes.NETWORK_ERROR: - console.error("HLS fatal network error:", data); - break; - case Hls.ErrorTypes.MEDIA_ERROR: - console.error("HLS fatal media error:", data); - hlsInstance.recoverMediaError(); - break; - default: - console.error("HLS fatal error:", data); - hlsInstance.destroy(); - break; - } - } else { - console.warn("HLS non-fatal error:", data); + hlsInstance.on(Hls.Events.ERROR, function (event, data) { + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + console.error("HLS fatal network error:", data); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + console.error("HLS fatal media error:", data); + hlsInstance.recoverMediaError(); + break; + default: + console.error("HLS fatal error:", data); + hlsInstance.destroy(); + break; } - }); - } else if (videoEl.canPlayType("application/vnd.apple.mpegurl")) { - // Native HLS support (Safari) - videoEl.src = url; - qualityBtn.style.display = "none"; // Hide quality button for native HLS - } else { - console.error( - "This browser does not support HLS natively or via hls.js" - ); - } - } else if (url) { - // Fallback for non-processed videos (e.g., direct MP4 link) + } else { + console.warn("HLS non-fatal error:", data); + } + }); + } else if (videoEl.canPlayType("application/vnd.apple.mpegurl")) { videoEl.src = url; - qualityBtn.style.display = "none"; // Hide quality button for non-HLS + qualityBtn.style.display = "none"; + } else { + console.error( + "This browser does not support HLS natively or via hls.js" + ); } + } else if (url) { + videoEl.src = url; + qualityBtn.style.display = "none"; + } + + // --- Event Listeners --- + playOverlay.addEventListener("click", togglePlayPause); + playPauseBtn.addEventListener("click", togglePlayPause); + videoEl.addEventListener("click", togglePlayPause); + videoEl.addEventListener("play", updatePlayPauseButton); + videoEl.addEventListener("pause", updatePlayPauseButton); + videoEl.addEventListener("ended", updatePlayPauseButton); + + videoEl.addEventListener("timeupdate", updateProgressBar); + videoEl.addEventListener("loadedmetadata", () => { + durationDisplay.textContent = formatTime(videoEl.duration); + progressBar.max = 100; + progressBar.value = 0; + currentTimeDisplay.textContent = formatTime(0); + updatePlayPauseButton(); + updateMuteButton(); + changePlaybackSpeed(1); + updateSliderBackground(progressBar); + updateSliderBackground(volumeSlider); + }); + progressBar.addEventListener("input", seekVideo); + + muteBtn.addEventListener("click", toggleMute); + volumeSlider.addEventListener("input", changeVolume); + videoEl.addEventListener("volumechange", updateMuteButton); + + fullscreenBtn.addEventListener("click", toggleFullscreen); + document.addEventListener("fullscreenchange", updateFullscreenButton); + document.addEventListener("webkitfullscreenchange", updateFullscreenButton); + document.addEventListener("mozfullscreenchange", updateFullscreenButton); + document.addEventListener("msfullscreenchange", updateFullscreenButton); + + qualityBtn.addEventListener("click", (e) => { + e.stopPropagation(); + qualityOptionsUl.classList.toggle("active"); + speedOptionsUl.classList.remove("active"); + }); - // --- Event Listeners --- - playOverlay.addEventListener("click", togglePlayPause); - playPauseBtn.addEventListener("click", togglePlayPause); - videoEl.addEventListener("click", togglePlayPause); - videoEl.addEventListener("play", updatePlayPauseButton); - videoEl.addEventListener("pause", updatePlayPauseButton); - videoEl.addEventListener("ended", updatePlayPauseButton); - - videoEl.addEventListener("timeupdate", updateProgressBar); - videoEl.addEventListener("loadedmetadata", () => { - durationDisplay.textContent = formatTime(videoEl.duration); - progressBar.max = 100; // Reset max to 100 for percentage - progressBar.value = 0; - currentTimeDisplay.textContent = formatTime(0); - updatePlayPauseButton(); // Initial button state - updateMuteButton(); // Initial mute button state - changePlaybackSpeed(1); // Initial speed - updateSliderBackground(progressBar); // Initial progress bar background - updateSliderBackground(volumeSlider); // Initial volume slider background - }); - progressBar.addEventListener("input", seekVideo); - - muteBtn.addEventListener("click", toggleMute); - volumeSlider.addEventListener("input", changeVolume); - videoEl.addEventListener("volumechange", updateMuteButton); - - fullscreenBtn.addEventListener("click", toggleFullscreen); - document.addEventListener("fullscreenchange", updateFullscreenButton); - document.addEventListener("webkitfullscreenchange", updateFullscreenButton); - document.addEventListener("mozfullscreenchange", updateFullscreenButton); - document.addEventListener("msfullscreenchange", updateFullscreenButton); - - qualityBtn.addEventListener("click", (e) => { - e.stopPropagation(); // Prevent document click from immediately closing - qualityOptionsUl.classList.toggle("active"); - speedOptionsUl.classList.remove("active"); // Close speed options if open - }); - - speedBtn.addEventListener("click", (e) => { - e.stopPropagation(); - speedOptionsUl.classList.toggle("active"); - qualityOptionsUl.classList.remove("active"); // Close quality options if open - }); + speedBtn.addEventListener("click", (e) => { + e.stopPropagation(); + speedOptionsUl.classList.toggle("active"); + qualityOptionsUl.classList.remove("active"); + }); - speedOptionsUl.querySelectorAll("li").forEach((li) => { - li.addEventListener("click", () => - changePlaybackSpeed(parseFloat(li.dataset.speed)) - ); - }); + speedOptionsUl.querySelectorAll("li").forEach((li) => { + li.addEventListener("click", () => + changePlaybackSpeed(parseFloat(li.dataset.speed)) + ); + }); - if (pipBtn && document.pictureInPictureEnabled && !videoEl.disablePictureInPicture) { - pipBtn.addEventListener("click", togglePip); - videoEl.addEventListener("enterpictureinpicture", () => - pipBtn.classList.add("active") - ); - videoEl.addEventListener("leavepictureinpicture", () => - pipBtn.classList.remove("active") - ); - } else if (pipBtn) { - pipBtn.style.display = "none"; + if (pipBtn && document.pictureInPictureEnabled && !videoEl.disablePictureInPicture) { + pipBtn.addEventListener("click", togglePip); + videoEl.addEventListener("enterpictureinpicture", () => + pipBtn.classList.add("active") + ); + videoEl.addEventListener("leavepictureinpicture", () => + pipBtn.classList.remove("active") + ); + } else if (pipBtn) { + pipBtn.style.display = "none"; + } + + document.addEventListener("click", (e) => { + if (!qualityOptionsUl.contains(e.target) && e.target !== qualityBtn) { + qualityOptionsUl.classList.remove("active"); } + if (!speedOptionsUl.contains(e.target) && e.target !== speedBtn) { + speedOptionsUl.classList.remove("active"); + } + }); - // Hide options when clicking outside - document.addEventListener("click", (e) => { - if (!qualityOptionsUl.contains(e.target) && e.target !== qualityBtn) { - qualityOptionsUl.classList.remove("active"); - } - if (!speedOptionsUl.contains(e.target) && e.target !== speedBtn) { - speedOptionsUl.classList.remove("active"); - } - }); + container.addEventListener("mouseenter", showControls); + container.addEventListener("mousemove", showControls); + container.addEventListener("mouseleave", hideControlsAfterDelay); - // Hide controls on mouseout and show on mouseenter - container.addEventListener("mouseenter", showControls); - container.addEventListener("mousemove", showControls); - container.addEventListener("mouseleave", hideControlsAfterDelay); + updatePlayPauseButton(); + updateMuteButton(); + volumeSlider.value = videoEl.volume * 100; +} - // Initial state setup - updatePlayPauseButton(); - updateMuteButton(); - volumeSlider.value = videoEl.volume * 100; - }); -}); diff --git a/templates/index.html b/templates/index.html index 6be494f..38a0715 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,7 +2,7 @@ {% block content %} -
    +

    My Blog

    {% if 'admin' in session %} New Post @@ -75,4 +75,13 @@

    {{ post[1] }}

    {% endfor %}
    + + + + + {% endblock %} \ No newline at end of file