Browse Source

Added post renderer

Toby Chui 16 hours ago
parent
commit
47049dec73

+ 4 - 4
sd_card/www/admin/post.html

@@ -53,7 +53,7 @@
           <div class="item">
             <div class="header">Media</div>
             <div class="menu">
-              <!-- <a class="item" name="library" xtab="posteng/convt.html">Convert</a> -->
+              <a class="item" name="library" xtab="posteng/library.html">Image Library</a>
               <a class="item" name="paste" xtab="posteng/paste.html">Image Paste</a>
             </div>
           </div>
@@ -104,8 +104,8 @@
                 case "newpost":
                     $("#postengine_tab").load("posteng/new.html");
                     break;
-                case "convert":
-                    $("#postengine_tab").load("posteng/convt.html");
+                case "library":
+                    $("#postengine_tab").load("posteng/library.html");
                     break;
                 case "paste":
                     $("#postengine_tab").load("posteng/paste.html");
@@ -133,7 +133,7 @@
         function msgbox(message, duration = 3000) {
           const snackbar = document.getElementById("msgbox_snackbar");
           const text = document.getElementById("msgbox_text");
-          text.textContent = message;
+          $(text).html(message);
           $(snackbar).fadeIn("fast").delay(duration).fadeOut("fast");
         }
 

+ 112 - 0
sd_card/www/admin/posteng/edit.html

@@ -151,6 +151,8 @@
         ]
     });
 
+    bindOnPasteEvent(); // Bind paste event to the editor
+
     // Allow vertical resize for the editor
     document.getElementById("editor").style.resize = "vertical";
 
@@ -458,4 +460,114 @@
         // Initialize with root directory
         fetchDirectoryContents(currentDir);
     });
+
+    /*
+        Handle image paste into the mde editor
+    */
+    function compressImageToJpeg(imageData, filename) {
+        return new Promise((resolve, reject) => {
+            const canvas = document.createElement('canvas');
+            const ctx = canvas.getContext('2d');
+            const img = new Image();
+
+            img.onload = () => {
+                const maxWidth = 1024; // Set maximum width for compression
+                const maxHeight = 1024; // Set maximum height for compression
+                let width = img.width;
+                let height = img.height;
+
+                // Maintain aspect ratio
+                if (width > maxWidth || height > maxHeight) {
+                    if (width / height > maxWidth / maxHeight) {
+                        height = Math.round((height * maxWidth) / width);
+                        width = maxWidth;
+                    } else {
+                        width = Math.round((width * maxHeight) / height);
+                        height = maxHeight;
+                    }
+                }
+
+                canvas.width = width;
+                canvas.height = height;
+                ctx.drawImage(img, 0, 0, width, height);
+
+                canvas.toBlob(
+                    (blob) => {
+                        if (blob) {
+                            const compressedFile = new File([blob], filename, {
+                                type: 'image/jpeg',
+                            });
+                            resolve(compressedFile);
+                        } else {
+                            reject(new Error('Compression failed.'));
+                        }
+                    },
+                    'image/jpeg',
+                    0.8 // Compression quality (0.0 to 1.0)
+                );
+            };
+
+            img.onerror = () => reject(new Error('Failed to load image.'));
+            img.src = imageData; // Set the image source to the data URL
+        });
+    }
+
+    function uploadImage(comperssedFile, callback=undefined) {
+        const formData = new FormData();
+        if (comperssedFile) {
+            formData.append("file1", comperssedFile);
+            var ajax = new XMLHttpRequest();
+            ajax.addEventListener("load", function(event) {
+                let responseText = event.target.responseText;
+                if (responseText.includes("ok")){
+                    msgbox(`<i class="ui green check icon"></i> Image uploaded`, 3000);
+                    if (callback != undefined){
+                        callback();
+                    }
+                }
+            }, false);
+
+            ajax.addEventListener("error", function(event) {
+                alert("Failed to upload image: " + event.target.responseText);
+            }, false);
+
+            ajax.open("POST", "/upload?dir=/site/img");
+            ajax.send(formData);
+        }else{
+            console.error("No image to upload!");
+        }
+    }
+
+    function bindOnPasteEvent(){
+        $(".CodeMirror textarea").on("paste", function(event) {
+            console.log(event);
+            const items = (event.clipboardData || event.originalEvent.clipboardData).items;
+            for (let i = 0; i < items.length; i++) {
+                if (items[i].type.indexOf("image") !== -1) {
+                    const file = items[i].getAsFile();
+                    const reader = new FileReader();
+                    reader.onload = function(e) {
+                        const imageData = e.target.result;
+                        const fileName = Date.now() + ".jpg";
+
+                        compressImageToJpeg(imageData, fileName).then((compressedFile) => {
+                            console.log("Pasting compressed image:", fileName);
+                            const imgTag = `![Image](/site/img/${fileName})`;
+                            uploadImage(compressedFile, function(){
+                                console.log("Image uploaded:", fileName);
+                                simplemde.codemirror.replaceSelection(imgTag);
+                            });
+
+                        }).catch((error) => {
+                            msgbox(`<i class="ui red times icon"></i> Image compression failed`, 3000);
+                            console.error("Image compression failed:", error);
+                        });
+                    };
+                    reader.readAsDataURL(file);
+                    event.preventDefault();
+                    return;
+                }
+            }
+        });
+    }
 </script>

+ 108 - 0
sd_card/www/admin/posteng/library.html

@@ -0,0 +1,108 @@
+<br>
+<div class="ui container" style="margin-top: 20px;">
+    <h2 class="ui header">Image Library</h2>
+    <p>The table below shows the screenshots pasted into the editor</p>
+    <table class="ui celled table">
+        <thead>
+            <tr>
+                <th>Image Name</th>
+                <th>Upload Time</th>
+                <th>Preview</th>
+                <th>Delete</th>
+            </tr>
+        </thead>
+        <tbody id="libraryTableBody">
+            <tr>
+                <td colspan="4" style="text-align: center;">
+                    <div class="ui active inline loader"></div>
+                    <span>Loading...</span>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+    <button class="ui green basic button" onclick="initImageLibrary();">
+        <i class="refresh icon"></i> Refresh
+    </button>
+    <div class="ui divider"></div>
+    <!-- Image Preview Section -->
+    <h3>Image Preview</h3>
+    <p>Click on the preview button to see the image</p>
+    <div class="ui segment">
+        <img id="library_image_preview_element" class="ui centered big image" src="">
+    </div>
+    <br><br>
+</div>
+
+<script>
+    function initImageLibrary(){
+        // Fetch the list of posts from the server
+        $("#libraryTableBody").html(`<tr><td colspan="4" style="text-align: center;"><div class="ui active inline loader"></div><span>Loading...</span></td></tr>`);
+        $.get("/api/fs/list?dir=/site/img/", function(data){
+            console.log(data);
+            let imageList = data.filter(file => (file.Filename.endsWith(".jpg") || file.Filename.endsWith(".png")) && !file.IsDir);
+            imageList.sort((a, b) => {
+                const getTimestamp = filename => parseInt(filename.split('_')[0]);
+                return getTimestamp(b.Filename) - getTimestamp(a.Filename); // Sort by timestamp in filename
+            });
+
+            //Render the images in the table
+            let tableBody = $("#libraryTableBody");
+            tableBody.empty(); // Clear existing rows
+
+            imageList.forEach(image => {
+                const imageName = image.Filename.split('/').pop(); // Extract the image name
+                const imagePath = "/site/img/" + image.Filename; 
+                const uploadTimestamp = parseInt(imageName.split('.')[0]); // Extract the timestamp from the filename
+                const uploadDate = new Date(uploadTimestamp).toLocaleString(); // Convert timestamp to local time
+                tableBody.append(`
+                    <tr>
+                        <td>${imageName.replace(/^\d+_/, '')}</td>
+                        <td>${uploadDate}</td>
+                        <td><button class="ui basic button" onclick="previewImage('${imagePath}');">Preview</button></td>
+                        <td><button class="ui basic red button" onclick="deleteImage('${imagePath}');">Delete</button></td>
+                    </tr>
+                `);
+            });
+
+            if (imageList.length == 0) {
+                tableBody.append(`
+                    <tr id="noimage">
+                        <td colspan="4" style="text-align: center;"><i class="ui green circle check icon"></i> No images / screenshots</td>
+                    </tr>
+                `);
+            }
+            
+
+        });
+    }
+
+    function previewImage(imagePath) {
+        // Set the image source to the selected image path
+        $("#library_image_preview_element").attr("src", imagePath);
+        $("#library_image_preview_element").show(); // Show the image preview element
+    }
+
+    function deleteImage(imagePath) {
+        // Confirm deletion
+        if (confirm("Are you sure you want to delete this image?")) {
+            $.ajax({
+                url: "/api/fs/del?target=" + imagePath,
+                method: "POST",
+                success: function(data) {
+                    if (data.error !== undefined) {
+                        msgbox(`<i class="ui red times icon"></i> Error deleting image: ` + data.error);
+                    } else {
+                        msgbox(`<i class="ui green check icon"></i> Image deleted`);
+                        initImageLibrary(); // Refresh the image library
+                        $("#library_image_preview_element").attr("src", "").hide(); // Clear and hide the image preview
+                    }
+                },
+                error: function() {
+                    alert("Error deleting image.");
+                }
+            });
+        }
+    }
+
+    initImageLibrary();
+</script>

+ 113 - 0
sd_card/www/admin/posteng/new.html

@@ -142,6 +142,8 @@
         ]
     });
 
+    bindOnPasteEvent(); // Bind paste event to the editor
+
     // Allow vertical resize for the editor
     document.getElementById("editor").style.resize = "vertical";
 
@@ -512,4 +514,115 @@
         // Initialize with root directory
         fetchDirectoryContents(currentDir);
     });
+
+    /*
+        Handle image paste into the mde editor
+    */
+    function compressImageToJpeg(imageData, filename) {
+        return new Promise((resolve, reject) => {
+            const canvas = document.createElement('canvas');
+            const ctx = canvas.getContext('2d');
+            const img = new Image();
+
+            img.onload = () => {
+                const maxWidth = 1024; // Set maximum width for compression
+                const maxHeight = 1024; // Set maximum height for compression
+                let width = img.width;
+                let height = img.height;
+
+                // Maintain aspect ratio
+                if (width > maxWidth || height > maxHeight) {
+                    if (width / height > maxWidth / maxHeight) {
+                        height = Math.round((height * maxWidth) / width);
+                        width = maxWidth;
+                    } else {
+                        width = Math.round((width * maxHeight) / height);
+                        height = maxHeight;
+                    }
+                }
+
+                canvas.width = width;
+                canvas.height = height;
+                ctx.drawImage(img, 0, 0, width, height);
+
+                canvas.toBlob(
+                    (blob) => {
+                        if (blob) {
+                            const compressedFile = new File([blob], filename, {
+                                type: 'image/jpeg',
+                            });
+                            resolve(compressedFile);
+                        } else {
+                            reject(new Error('Compression failed.'));
+                        }
+                    },
+                    'image/jpeg',
+                    0.8 // Compression quality (0.0 to 1.0)
+                );
+            };
+
+            img.onerror = () => reject(new Error('Failed to load image.'));
+            img.src = imageData; // Set the image source to the data URL
+        });
+    }
+
+    function uploadImage(comperssedFile, callback=undefined) {
+        const formData = new FormData();
+        if (comperssedFile) {
+            formData.append("file1", comperssedFile);
+            var ajax = new XMLHttpRequest();
+            ajax.addEventListener("load", function(event) {
+                let responseText = event.target.responseText;
+                if (responseText.includes("ok")){
+                    msgbox(`<i class="ui green check icon"></i> Image uploaded`, 3000);
+                    if (callback != undefined){
+                        callback();
+                    }
+                }
+            }, false);
+
+            ajax.addEventListener("error", function(event) {
+                alert("Failed to upload image: " + event.target.responseText);
+            }, false);
+
+            ajax.open("POST", "/upload?dir=/site/img");
+            ajax.send(formData);
+        }else{
+            console.error("No image to upload!");
+        }
+    }
+
+    function bindOnPasteEvent(){
+        $(".CodeMirror textarea").on("paste", function(event) {
+            console.log(event);
+            const items = (event.clipboardData || event.originalEvent.clipboardData).items;
+            for (let i = 0; i < items.length; i++) {
+                if (items[i].type.indexOf("image") !== -1) {
+                    const file = items[i].getAsFile();
+                    const reader = new FileReader();
+                    reader.onload = function(e) {
+                        const imageData = e.target.result;
+                        const fileName = Date.now() + ".jpg";
+
+                        compressImageToJpeg(imageData, fileName).then((compressedFile) => {
+                            console.log("Pasting compressed image:", fileName);
+                            const imgTag = `![Image](/site/img/${fileName})`;
+                            uploadImage(compressedFile, function(){
+                                console.log("Image uploaded:", fileName);
+                                simplemde.codemirror.replaceSelection(imgTag);
+                            });
+
+                        }).catch((error) => {
+                            msgbox(`<i class="ui red times icon"></i> Image compression failed`, 3000);
+                            console.error("Image compression failed:", error);
+                        });
+                    };
+                    reader.readAsDataURL(file);
+                    event.preventDefault();
+                    return;
+                }
+            }
+        });
+    }
+    
 </script>

+ 125 - 123
sd_card/www/posts.html

@@ -1,87 +1,27 @@
-<div class="ts-container is-very-narrow">
-    <p>Work in progress</p>
-</div>
-<script>
-     let loggedIn = false;
-
-    //Check the user has logged in
-    //Post editing function still require session check
-    //this just here to hide the edit buttons
-    $.get("/api/auth/chk", function(data){
-        if (data == false){
-            //User cannot use admin function. Hide the buttons.
-            $(".adminOnly").remove();
-            loggedIn = false;
-        }else{
-            loggedIn = true;
-        }
-        loadValue("blog-posts", function(){
-            initPosts();
-        });
-    });
-
-    //Initialize blog info
-    function initBlogInfo(){
-        loadValue("blog-title", function(title){
-            if (title.error != undefined || title == ""){
-                title = "WebStick";
-            }
-            document.title = decodeURIComponent(title);
-            $("#pageTitle").text(decodeURIComponent(title));
-        });
-
-        loadValue("blog-subtitle", function(title){
-            if (title.error != undefined || title == ""){
-                title = "A personal web server hosted on an ESP8266 using a micro SD card";
-            }
-            $("#pageDesc").text(decodeURIComponent(title));
-        });
-    }
-
-    $(document).ready(function(){
-        initBlogInfo();
-    });
-   
-
-
-    //Edit blog title and subtitles
-    function editBlogSubtitle(){
-        let newtitle = prompt("New Blog Subtitle", "");
-        if (newtitle != null) {
-            setValue("blog-subtitle", encodeURIComponent(newtitle), function(){
-                initBlogInfo();
-            })
-        } 
-    }
-
-    function editBlogTitle(){
-        let newtitle = prompt("New Blog Title", "");
-        if (newtitle != null) {
-            setValue("blog-title", encodeURIComponent(newtitle), function(){
-                initBlogInfo();
-            })
-        } 
-    }   
-
-
-
-
-
-
-/*
-    Post Edit functions
-*/
-
-function editPost(btn){
-    let postFilename = $(btn).attr("filename");
-    let hash = encodeURIComponent(JSON.stringify({
-        "filename": postFilename,
-        "filepath": "/blog/posts/" + postFilename
-    }))
-    window.open("/admin/mde/index.html#" + hash);
-}
 
+<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+<div class="ts-grid mobile:is-stacked">
+    <div class="column is-4-wide">
+        <!-- Post List -->
+        <div class="ts-box">
+            <div class="ts-content">
+                <div class="ts-header is-heavy">Recent Posts</div>
+                <div id="posts_list" class="ts-list has-top-spaced-small">
+                    
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="column is-12-wide">
+        <div id="posts_contents">
+            <!-- Post Contents -->
+        </div>
+    </div>
+</div>
 
+<script>
+var postList = [];
+var currentPost = 0;
 /*
     Rendering for Posts
 */
@@ -90,43 +30,114 @@ function loadMarkdownToHTML(markdownURL, targetElement){
     fetch(markdownURL).then( r => r.text() ).then( text =>{
         var converter = new showdown.Converter();
         let targetHTML = converter.makeHtml(text);
-        console.log(targetHTML);
-        $(targetElement).html(targetHTML);
+        let processedEle = $(targetHTML);
+        //Wrap images in a div with class ts-image
+        processedEle.find("img").each(function() {
+            let imgSrc = $(this).attr("src");
+            console.log(imgSrc);
+            $(this).wrap(`<div class="ts-image is-rounded"></div>`);
+            $(this).attr("src", imgSrc);
+        });
+
+        processedEle.find("h1, h2, h3, h4, h5, h6, p").each(function() {
+            $(this).addClass("ts-text");
+        });
+
+        // Add preload="none" to all video and audio elements
+        processedEle.find("video, audio").each(function() {
+            $(this).attr("preload", "none");
+        });
+
+        $(targetElement).html(processedEle);
     });
 }
 
 function initPosts(){
-    $("#posttable").html("<div class='ui basic segment'><p><i class='ui loading spinner icon'></i> Loading Blog Posts</p></div>");
-    loadValue("blog-posts", function(data){
-        $("#posttable").html("");
+    $("#posts_list").html(`<a href="#!"><span class="ts-icon is-spinning is-circle-notch-icon"></span> Loading</a>`);
+    $("#posts_contents").html(`
+        <div class="ui active dimmer" id="loadingDimmer">
+            <div class="ui text loader">Loading post contents</div>
+        </div>
+    `);
+    loadValue("site-posts", function(data){
+        $("#posts_list").html("");
+        $("#posts_contents").html(``);
         try{
-            let postList = JSON.parse(decodeURIComponent(atob(data)));
+            postList = JSON.parse(decodeURIComponent(atob(data)));
+            console.log(postList);
+            
+            // Sort posts by timestamp from latest to oldest
+            postList.sort((a, b) => {
+                let timestampA = parseInt(a.split("_")[0]);
+                let timestampB = parseInt(b.split("_")[0]);
+                return timestampB - timestampA;
+            });
+
+            let lastPostMonth = "";
+            for (let i = 0; i < postList.length; i++) {
+                let post = postList[i];
+                let thisPostMonth = new Date(parseInt(post.split("_")[0]) * 1000).toLocaleString(undefined, { month: 'long' });
+                let thisPostYear = new Date(parseInt(post.split("_")[0]) * 1000).getFullYear();
+                console.log(post, thisPostMonth, thisPostYear);
+                if (thisPostMonth != lastPostMonth) {
+                    appendMonthDividerToIndex(thisPostMonth + " " + thisPostYear);
+                    lastPostMonth = thisPostMonth;
+                }
+
+                renderIndex(post, i);
+            }
 
-            //From latest to oldest
-            postList.reverse();
-            console.log("Post listed loaded: ", postList);
             if (postList.length == 0){
-                $("#nopost").show();
+                $("#posts_list").html(`No posts found`);
+                $("#posts_contents").html(`No posts found`);
             }else{
-                $("#nopost").hide();
-                postList.forEach(postFilename => {
-                    renderPost(postFilename);
-                })
+                 //Render the first post
+                 renderPost(postList[0]);
             }
         }catch(ex){
-            $("#nopost").show();
+            console.log(ex);
+            $("#posts_list").html(`No posts found`);
+            $("#posts_contents").html(`No posts found`);
         }
         
     })
 }
 
-function forceUpdatePostIndex(){
-    updatePostIndex(function(){
-        window.location.reload();
-    });
+function loadPost(element){
+    let postID = $(element).attr("postid");
+    let filename = postList[postID];
+    console.log(filename);
+    $("#posts_contents").html(`
+        <div class="ts-text is-center-aligned">Loading Post</div>
+    `);
+    renderPost(filename);
+    //Scroll to the top of the page
+    $("html, body").animate({ scrollTop: 0 }, "slow");
 }
 
+//Render the side menu index
+function renderIndex(filename, index){
+    //Remove the timestamp
+    let postTitle = filename.split("_");
+    let timeStamp = postTitle.shift();
+    postTitle = postTitle.join("_");
+
+    //Pop the file extension
+    postTitle = postTitle.split(".");
+    postTitle.pop();
+    postTitle = postTitle.join(".");
 
+    //Create a wrapper element
+    $("#posts_list").append(`
+        <a href="#!" postid="${index}" onclick="loadPost(this);" class="ts-text is-link is-undecorated item">${postTitle}</a>
+    `);
+}
+
+function appendMonthDividerToIndex(month){
+    $("#posts_list").append(`
+        <div class="ts-text is-header has-top-spaced-small">${month}</div>
+    `);
+}
 
 //Render post
 function renderPost(filename){
@@ -141,32 +152,23 @@ function renderPost(filename){
     postTitle = postTitle.join(".");
 
     var postTime = new Date(parseInt(timeStamp) * 1000).toLocaleDateString("en-US")
-    let postEditFeature = `<div class="adminOnly" style="position: absolute; top: 3em; right: 0.4em;">
-                <a class="ui basic mini icon button" onclick="editPost(this);" filename="${filename}" title="Edit Post"><i class="edit icon"></i></a>
-                <button class="ui basic mini icon button" onclick="deletePost(this);" ptitle="${postTitle}" filename="${filename}" title="Remove Post"><i class="red trash icon"></i></button>
-            </div>`;
-
-    if (!loggedIn){
-        postEditFeature = "";
-    }
+
     //Create a wrapper element
-    $("#posttable").append(`
-        <div class="ui basic segment postObject" id="${timeStamp}">
-            <div class="ui divider"></div>
-            <h4 class="ui header">
-                <i class="blue paperclip icon"></i>
-                <div class="content">
-                   ${postTitle}
+    $("#posts_contents").html(`
+        <div class="ts-box site-post" post_id="${timeStamp}">
+            <div class="ts-content is-padded">
+                <div class="ts-header is-heavy">${postTitle}</div>
+                <div class="ts-text is-description has-top-spaced-small">
+                    ${postTime}
                 </div>
-            </h4>
-            ${postEditFeature}
-            <div class="postContent">
-
+                <div class="ts-divider has-top-spaced-small has-bottom-spaced-small"></div>
+                <div class="ts-text postContent"></div>
             </div>
-            <small><i class="calendar alternate outline icon"></i> ${postTime}</small>
         </div>
     `);
-    let targetElement =  $("#" + timeStamp).find(".postContent");
-    loadMarkdownToHTML("/blog/posts/" + filename,targetElement);
+    let targetElement =  $(`.site-post[post_id='${timeStamp}']`).find(".postContent");
+    loadMarkdownToHTML("/site/posts/" + filename,targetElement);
 }
+
+initPosts();
 </script>