소스 검색

Added new post editor and import utilities

Toby Chui 15 시간 전
부모
커밋
f15520489b

+ 0 - 88
sd_card/www/admin/blog.html

@@ -1,88 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <meta charset="utf-8">
-    <title>PostEngine Pro</title>
-    <meta name="viewport" content="width=device-width, initial-scale=1" >
-    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
-    <style>
-        body{
-            background-color: rgb(243, 243, 243);
-            border-top: 1px solid #70aeff;
-        }
-
-        .ui.vertical.menu{
-            width: calc(15em - 3px) !important;
-        }
-
-        .postengine_context{
-            padding-left: calc(15em + 1px) !important;
-        }
-    </style>
-</head>
-<body>
-    <div class="ui container">
-
-    </div>
-    <div id="postengine_side_menu" class="ui left fixed inverted vertical menu">
-        <div class="item" style="pointer-events: none;">
-          <img class="ui fluid image" src="./img/post-engine.svg">
-        </div>
-        <div class="item">
-          <div class="header">Posts</div>
-          <div class="menu">
-            <a class="item active" xtab="postengine_all_post">All Posts</a>
-            <a class="item" xtab="postengine_new_post">Add New</a>
-            <a class="item" xtab="postengine_list_catergories">Categories</a>
-          </div>
-        </div>
-        <div class="item">
-          <div class="header">Media</div>
-          <div class="menu">
-            <a class="item" xtab="postengine_media_library">Library</a>
-            <a class="item" xtab="postengine_media_add_new">Add New</a>
-          </div>
-        </div>
-        <div class="item">
-          <div class="header">Pages</div>
-          <div class="menu">
-            <a class="item" xtab="postengine_pages_all">All Pages</a>
-            <a class="item" xtab="postengine_pages_add_new">Add New</a>
-          </div>
-        </div>
-        <div class="item">
-          <div class="header">Settings</div>
-          <div class="menu">
-            <a class="item" xtab="postengine_settings_general">General</a>
-            <a class="item" xtab="postengine_settings_permalinks">Permalinks</a>
-          </div>
-        </div>
-    </div>
-    <!-- Function Tabs -->
-    <div id="postengine_all_post" class="postengine_context"></div>
-    <div id="postengine_new_post" class="postengine_context" style="display:none;"></div>
-    <div id="postengine_list_catergories" class="postengine_context" style="display:none;"></div>
-    <div id="postengine_media_library" class="postengine_context" style="display:none;"></div>
-    <div id="postengine_media_add_new" class="postengine_context" style="display:none;"></div>
-    <div id="postengine_pages_all" class="postengine_context" style="display:none;"></div>
-    <div id="postengine_pages_add_new" class="postengine_context" style="display:none;"></div>
-    <div id="postengine_settings_general" class="postengine_context" style="display:none;"></div>
-    <div id="postengine_settings_permalinks" class="postengine_context" style="display:none;"></div>
-
-    <script>
-        // Add event listener to menu items with target attribute
-        $("#postengine_side_menu .item[xtab]").on("click", function() {
-            var targetId = $(this).attr("xtab");
-            $(".postengine_context").hide();
-            $("#postengine_side_menu .item.active").removeClass("active");
-            $(this).addClass("active");
-            $("#" + targetId).show();
-        });
-
-        //Load all the elements
-        $("#postengine_all_post").load("posteng/all.html");
-    </script>
-</body>
-</html>

+ 21 - 7
sd_card/www/admin/fs.html

@@ -36,16 +36,16 @@
     <body class="whiteTheme">
         <div id="navibar" class="navibar">
             <!-- File Opr Group-->
-            <button class="fileOprBtn" title="Open" onclick="openViaButton(event);"><img class="opricon" src="img/opr/open.svg"><p class="oprtxt" locale="fileopr/Open">Open</p></button>
-            <button id="copyButton" class="fileOprBtn" title="Copy" onclick="copy();"><img class="opricon" src="img/opr/copy.svg"><p class="oprtxt" locale="fileopr/Copy">Copy</p></button>
-            <button id="pasteButton" class="fileOprBtn" title="Paste" onclick="paste();"><img class="opricon" src="img/opr/paste.svg"><p class="oprtxt" locale="fileopr/Paste">Paste</p></button>
+            <button class="fileOprBtn" title="Open" onclick="openViaButton(event);"><img class="opricon" xsrc="img/opr/open.svg"><p class="oprtxt" locale="fileopr/Open">Open</p></button>
+            <button id="copyButton" class="fileOprBtn" title="Copy" onclick="copy();"><img class="opricon" xsrc="img/opr/copy.svg"><p class="oprtxt" locale="fileopr/Copy">Copy</p></button>
+            <button id="pasteButton" class="fileOprBtn" title="Paste" onclick="paste();"><img class="opricon" xsrc="img/opr/paste.svg"><p class="oprtxt" locale="fileopr/Paste">Paste</p></button>
             <div class="fileoprGroupDivider" style="display: inline-block; vertical-align: top;">
                 <button class="fileoprSmallBtn" title="Refresh" onclick="refresh();"><i class="green refresh icon"></i> <span locale="fileopr/Refresh">Refresh</span></button><br>
                 <button class="fileoprSmallBtn" title="Cut" onclick="cut();"><i class="blue cut icon"></i> <span locale="fileopr/Cut">Cut</span></button><br>
                 <button class="fileoprSmallBtn" title="Rename" onclick="rename();"><i class="teal i cursor icon"></i> <span locale="fileopr/Rename">Rename</span></button>
             </div>
-            <button class="fileOprBtn" title="Upload" onclick="upload(); "><img class="opricon" src="img/opr/upload.svg"><p class="oprtxt wideScreenOnly" locale="fileopr/Upload">Upload</p></button>
-            <button class="fileOprBtn" title="Download" onclick="downloadFile(); "><img class="opricon" src="img/opr/download.svg"><p class="oprtxt wideScreenOnly" locale="fileopr/Download">Download</p></button>
+            <button class="fileOprBtn" title="Upload" onclick="upload(); "><img class="opricon" xsrc="img/opr/upload.svg"><p class="oprtxt wideScreenOnly" locale="fileopr/Upload">Upload</p></button>
+            <button class="fileOprBtn" title="Download" onclick="downloadFile(); "><img class="opricon" xsrc="img/opr/download.svg"><p class="oprtxt wideScreenOnly" locale="fileopr/Download">Download</p></button>
             <div class="fileoprGroupDivider" style="display: inline-block; vertical-align: top;"></div>
           
             <div class="fileoprGroupDivider" style="display: inline-block; vertical-align: top;">
@@ -53,7 +53,7 @@
                 <button class="fileoprSmallBtn" title="New Folder" onclick="newFolder();"><i style="color: #ffe79e !important;" class="yellow folder icon"></i> <span locale="fileopr/New Folder">New Folder</span></button><br>
                 <button class="fileoprSmallBtn" title="Delete" onclick="deleteFile();"><i class="red times icon"></i> <span locale="fileopr/Delete">Delete</span></button><br>
             </div>
-            <button class="fileOprBtn" title="Download" onclick="shareFile(); "><img class="opricon" src="img/opr/share.svg"><p class="oprtxt wideScreenOnly" locale="fileopr/Share">Share</p></button>
+            <button class="fileOprBtn" title="Download" onclick="shareFile(); "><img class="opricon" xsrc="img/opr/share.svg"><p class="oprtxt wideScreenOnly" locale="fileopr/Share">Share</p></button>
             <br>
             <!-- Directoy navigations -->
             <div class="addressBar">
@@ -1214,7 +1214,21 @@
             }
 
 
-
+            /* 
+                Delay loading 
+                ESP8266 cannot handle so many concurrent conncetions
+                So we delay the loading of images to prevent the ESP8266 from overloading
+                This is a hack to make the page load faster and not freeze the browser
+            */ 
+            setTimeout(() => {
+                $("img[xsrc]").each(function () {
+                    const imgElement = $(this);
+                    const randomDelay = Math.floor(Math.random() * (1500 - 1000 + 1)) + 500; // Random delay between 500ms and 1500ms
+                    setTimeout(() => {
+                        imgElement.attr("src", imgElement.attr("xsrc"));
+                    }, randomDelay);
+                });
+            }, 0);
         </script>
     </body>
 </html>

+ 28 - 12
sd_card/www/admin/index.html

@@ -44,23 +44,23 @@
         </div>
     </div>
     <div class="windows">
-        <div id="fs" class="frameWrapper">
-            <iframe src="fs.html"></iframe>
+        <div id="fs" class="frameWrapper" xload="fs.html">
+            <iframe src="about:blank"></iframe>
         </div>
-        <div id="blog" class="frameWrapper">
-            <iframe src="blog.html"></iframe>
+        <div id="blog" class="frameWrapper" xload="post.html" style="display:none;">
+            <iframe src="about:blank"></iframe>
         </div>
-        <div id="search" class="frameWrapper" style="display:none;">
-            <iframe src="search.html"></iframe>
+        <div id="search" class="frameWrapper" xload="search.html" style="display:none;">
+            <iframe src="about:blank"></iframe>
         </div>
-        <div id="shares" class="frameWrapper" style="display:none;">
-            <iframe src="shares.html"></iframe>
+        <div id="shares" class="frameWrapper" xload="shares.html" style="display:none;">
+            <iframe src="about:blank"></iframe>
         </div>
-        <div id="users" class="frameWrapper" style="display:none;">
-            <iframe src="users.html"></iframe>
+        <div id="users" class="frameWrapper" xload="users.html" style="display:none;">
+            <iframe src="about:blank"></iframe>
         </div>
-        <div id="info" class="frameWrapper" style="display:none;">
-            <iframe src="info.html"></iframe>
+        <div id="info" class="frameWrapper" xload="info.html" style="display:none;">
+            <iframe src="about:blank"></iframe>
         </div>
     </div>
     <script>
@@ -79,6 +79,7 @@
             event.preventDefault();
             let targetFrameID = $(object).attr("xframe");
             $(".frameWrapper").hide();
+            initiateIframeForID(targetFrameID);
             $("#" + targetFrameID).show();
             window.location.hash = targetFrameID;
        }
@@ -98,11 +99,26 @@
             let targetFrameID = window.location.hash.substring(1);
             if ($("#" + targetFrameID).length) {
                 $(".frameWrapper").hide();
+                initiateIframeForID(targetFrameID);
                 $("#" + targetFrameID).show();
                 $(".mainmenu .item.active").removeClass("active");
                 $(".mainmenu .item[xframe='" + targetFrameID + "']").addClass("active");
             }
+        }else{
+            //Load the default frame
+            initiateIframeForID("fs");
         }
+
+        //Dynamically load the iframe source when the frame is shown
+        function initiateIframeForID(eleId){
+            let targetFrameID = eleId;
+            if ($("#" + targetFrameID).length && $("#" + targetFrameID).attr("xload")) {
+                $("#" + targetFrameID + " iframe").attr("src", $("#" + targetFrameID).attr("xload"));
+                $("#" + targetFrameID).removeAttr("xload");
+            }
+        }
+
+        
     </script>
 </body>
 </html>

+ 9 - 3
sd_card/www/admin/info.html

@@ -119,7 +119,7 @@
                 }
             });
         }
-        initWiFiInfo();
+        
 
         function initSDCardInfo(){
             $("#spaceUsedBar").find(".progress").text("Loading...");
@@ -135,7 +135,7 @@
                 $("#volText").text(`Used: ${humanFileSize(data.usedSpace)} / Total: ${humanFileSize(data.diskSpace)}`);
             });
         }
-        initSDCardInfo();
+        
 
         //Send wake on lan package
         function sendWakeOnLan(){
@@ -199,7 +199,13 @@
                 }
             });
        }
-       initLoginCheck();
+
+        document.addEventListener("DOMContentLoaded", function() {
+            initWiFiInfo();
+            initSDCardInfo();
+            initLoginCheck();
+        });
+        
     </script>
 </body>
 </html>

+ 160 - 0
sd_card/www/admin/post.html

@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>PostEngine Pro</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1" >
+    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
+    <style>
+        body{
+            background-color: rgb(243, 243, 243);
+            border-top: 1px solid #70aeff;
+        }
+
+        .ui.vertical.menu{
+            width: calc(15em - 3px) !important;
+        }
+
+        .postengine_context{
+            padding-left: calc(15em + 1px) !important;
+        }
+
+        #msgbox_snackbar {
+          min-width: 240px;
+          position: fixed;
+          bottom: 20px;
+          right: 20px;
+          background-color: #323232;
+          color: white;
+          padding: 16px;
+          border-radius: 4px;
+          display: none;
+          z-index: 1000;
+        }
+    </style>
+</head>
+  <body>
+      <div class="ui container">
+
+      </div>
+      <div id="postengine_side_menu" class="ui left fixed inverted vertical menu">
+          <div class="item" style="pointer-events: none;">
+            <img class="ui fluid image" src="./img/post-engine.svg">
+          </div>
+          <div class="item">
+            <div class="header">Posts</div>
+            <div class="menu">
+              <a class="item active" xtab="posteng/all.html">All Posts</a>
+              <a class="item" xtab="posteng/new.html">Add New</a>
+            </div>
+          </div>
+          <div class="item">
+            <div class="header">Media</div>
+            <div class="menu">
+              <a class="item" xtab="">Library</a>
+              <a class="item" xtab="">Add New</a>
+            </div>
+          </div>
+          <div class="item">
+            <div class="header">Pages</div>
+            <div class="menu">
+              <a class="item" xtab="">All Pages</a>
+              <a class="item" xtab="">Add New</a>
+            </div>
+          </div>
+          <div class="item">
+            <div class="header">Settings</div>
+            <div class="menu">
+              <a class="item" xtab="">General</a>
+              <a class="item" xtab="">Permalinks</a>
+            </div>
+          </div>
+      </div>
+      <!-- Function Tabs -->
+      <div id="postengine_tab" class="postengine_context"></div>
+      
+      <!-- msgbox snackbar -->
+      <div id="msgbox_snackbar">
+        <span id="msgbox_text">This is a message</span>
+      </div>
+      <script>
+        let editingPost = ""; //The name of the post being edited, if empty = new post
+        $("#postengine_tab").load("posteng/all.html");
+
+        // Add event listener to menu items with target attribute
+        $("#postengine_side_menu .item[xtab]").on("click", function() {
+            var targetId = $(this).attr("xtab");
+            $("#postengine_side_menu .item.active").removeClass("active");
+            $(this).addClass("active");
+            //Load the xtab url into the postengine_tab div
+            $("#postengine_tab").load(targetId, function() {
+                // Callback function after loading the content
+                // You can add any additional logic here if needed
+                console.log("Loaded: " + targetId);
+            });
+        });
+
+
+        function msgbox(message, duration = 3000) {
+          const snackbar = document.getElementById("msgbox_snackbar");
+          const text = document.getElementById("msgbox_text");
+          text.textContent = message;
+          snackbar.style.display = "block";
+          setTimeout(() => {
+            snackbar.style.display = "none";
+          }, duration);
+        }
+
+        /*
+        
+          Common post management features
+          
+        */
+
+        //Storage and loader utils to load from pref database
+        //the perf database is a key-value store that only
+        //writable when logged in but readable by everyone
+        
+        function setValue(key, value, callback){
+            $.get("/api/pref/set?key=" + key + "&value=" + value, function(data){
+                callback(data);
+            });
+        }
+
+        function loadValue(key, callback){
+            $.get("/api/pref/get?key=" + key, function(data){
+                callback(data);
+            });
+        }
+       
+        //Update the post index in the server side
+        function updatePostIndex(callback=undefined){
+          let postList = [];
+          $.ajax({
+              url: "/api/fs/list?dir=/site/posts",
+              success: function(data){
+                  data.forEach(file => {
+                      let filename = file.Filename;
+                      let ext = filename.split(".").pop();
+                      if (ext == "md" && file.IsDir == false){
+                          //Markdown file. Render it
+                          postList.push(filename);
+                      }
+                  });
+
+                  //Set the
+                  setValue("site-posts", btoa(encodeURIComponent(JSON.stringify(postList))), function(data){
+                      console.log(data);
+                      if (callback != undefined){
+                          callback();
+                      }
+                  });
+              }
+          });
+      }
+
+      </script>
+  </body>
+</html>

+ 128 - 43
sd_card/www/admin/posteng/all.html

@@ -1,58 +1,143 @@
 <br>
 <div class="ui container">
-    <h2 class="ui header">All Blog Posts</h2>
-    <table class="ui celled table">
-        <thead>
-            <tr>
-                <th>Post Name</th>
-                <th>Edit</th>
-                <th>Delete</th>
-            </tr>
-        </thead>
-        <tbody id="postTableBody">
-            <!-- Rows will be dynamically loaded here -->
-        </tbody>
-    </table>
+    <div class="ui basic segment">
+        <h2 class="ui header">All Posts</h2>
+        <table class="ui single line table">
+            <thead>
+                <tr>
+                    <th>Post Name</th>
+                    <th>Creation Time</th>
+                    <th>Edit</th>
+                    <th>Delete</th>
+                </tr>
+            </thead>
+            <tbody id="postTableBody">
+                <tr id="loadingRow">
+                    <td colspan="4" style="text-align: center;">
+                        <div class="ui active inline loader"></div>
+                    </td>
+                </tr>
+                <!-- Rows will be dynamically loaded here -->
+            </tbody>
+        </table>
+        <button class="ui green basic button" id="refreshButton">
+            <i class="ui refresh icon"></i> Refresh
+        </button>
+    </div>
+
+    <div class="ui basic small modal" id="deleteConfirmModal">
+        <div class="ui icon header">
+            <i class="trash alternate outline icon"></i>
+            Confirm Delete
+        </div>
+        <div class="content" align="center">
+            <p>Are you sure you want to delete the post <span id="deletePostName"></span>?</p>
+        </div>
+        <div class="actions">
+            <div class="ui red ok inverted button" onclick="confirmDelete()" deleteingPostID="">
+                <i class="trash icon"></i>
+                Yes
+            </div>
+            <div class="ui basic cancel inverted button">
+                <i class="remove icon"></i>
+                Cancel
+            </div>
+        </div>
+    </div>
 </div>
 
 <script>
-    $(document).ready(function() {
-        // Dummy function to fetch posts from an API
-        function fetchPosts() {
-            // Simulated API response
-            const posts = [
-                { id: 1, name: "First Blog Post" },
-                { id: 2, name: "Second Blog Post" },
-                { id: 3, name: "Third Blog Post" }
-            ];
-
-            // Clear the table body
-            $('#postTableBody').empty();
-
-            // Populate the table with posts
-            posts.forEach(post => {
-                $('#postTableBody').append(`
+    /*
+        Post List
+
+        This script will fetch the list of posts from /www/site/posts/*.md
+        and display them in a table format.
+    */
+
+    function getPosts(){
+        // Show loading animation
+        $("#postTableBody").html(`<tr id="loadingRow">
+            <td colspan="4" style="text-align: center;">
+                <div class="ui active inline loader"></div>
+            </td>
+        </tr>`);
+        // Fetch the list of posts from the server
+        $.get("/api/fs/list?dir=/site/posts/", function(data){
+            console.log(data);
+            let postList = data.filter(file => file.Filename.endsWith(".md") && !file.IsDir);
+            postList.sort((a, b) => {
+                const getTimestamp = filename => parseInt(filename.split('_')[0]);
+                return getTimestamp(b.Filename) - getTimestamp(a.Filename); // Sort by timestamp in filename
+            });
+
+            //Render the posts in the table
+            console.log("Post list loaded: ", postList);
+            $("#postTableBody").empty(); // Clear the table body
+
+            postList.forEach(post => {
+                const postName = post.Filename.split('/').pop().replace('.md', ''); // Extract the post name
+                const postTime = new Date(post.Filename.split('_')[0] * 1000).toLocaleString(); // Convert timestamp to readable format
+                $("#postTableBody").append(`
                     <tr>
-                        <td>${post.name}</td>
-                        <td><button class="ui blue button edit-button" data-id="${post.id}">Edit</button></td>
-                        <td><button class="ui red button delete-button" data-id="${post.id}">Delete</button></td>
+                        <td>${postName.replace(/^\d+_/, '')}</td>
+                        <td>${postTime}</td>
+                        <td><button class="ui basic button edit-button" onclick="editPost('${post.Filename}');"><i class="ui blue edit icon"></i> Edit</button></td>
+                        <td><button class="ui red basic button delete-button" onclick="deletePost('${post.Filename}');"><i class="ui trash icon"></i> Delete</button></td>
                     </tr>
                 `);
             });
-        }
-
-        // Call the fetchPosts function to load data
-        fetchPosts();
 
-        // Event listeners for Edit and Delete buttons
-        $(document).on('click', '.edit-button', function() {
-            const postId = $(this).data('id');
-            alert(`Edit post with ID: ${postId}`);
+            if (postList.length == 0) {
+                $("#postTableBody").append(`
+                    <tr id="nopost">
+                        <td colspan="4" style="text-align: center;">No posts available</td>
+                    </tr>
+                `);
+            }
         });
+    }
+
+    //Open post editor
+    function editPost(postID){
+        //TODO: Implement the post editor functionality
+    }
+
+    // Delete post
+    function deletePost(postID) {
+        const postName = postID.replace(/^\d+_/, ''); // Trim the timestamp prefix
+        $('#deletePostName').text(postName); // Populate the post name in the modal
+        $('#deleteConfirmModal .ok.button').attr('deleteingPostID', postID); // Store the postID in the confirm button
+        $('#deleteConfirmModal').modal('show'); // Show the confirmation modal
+    }
 
-        $(document).on('click', '.delete-button', function() {
-            const postId = $(this).data('id');
-            alert(`Delete post with ID: ${postId}`);
+    function confirmDelete() {
+        const postID = $('#deleteConfirmModal .ok.button').attr('deleteingPostID'); // Retrieve the postID
+        if (!postID) return;
+
+        // Send a delete request to the server
+        $.ajax({
+            url: `/api/fs/del?target=/site/posts/${postID}`,
+            type: 'POSt',
+            success: function(response) {
+                console.log('Post deleted:', response);
+                $('#deleteConfirmModal').modal('hide'); // Hide the modal
+                getPosts(); // Refresh the post list
+                msgbox(`Post "${postID.replace(/^\d+_/, '')}" deleted successfully`, 2000); // Show success message
+            },
+            error: function(error) {
+                console.error('Error deleting post:', error);
+                msgbox('Failed to delete the post', 2000); // Show error message
+            }
         });
+    }
+
+    // Initial call to load posts
+    getPosts();
+
+    // Event listener for the Refresh button
+    $('#refreshButton').on('click', function() {
+        getPosts(); // Reload the table content
+        msgbox("Post list refreshed", 2000); // Show a message box
     });
+ 
 </script>

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

@@ -0,0 +1,9 @@
+<br>
+<link rel="stylesheet" href="mde/script/mde.css">
+<script src="mde/script/mde.js"></script>
+<div class="ui container">
+   
+</div>  
+<script>
+    
+</script>

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

@@ -0,0 +1,398 @@
+
+<link rel="stylesheet" href="mde/script/mde.css">
+<script src="mde/script/mde.js"></script>
+<style>
+    .selectable{
+        cursor: pointer;
+    }
+
+    .selectable:hover{
+        background-color: rgba(0, 0, 0, 0.1);
+    }
+
+    #mediaContent{
+        max-height: 50vh;
+        overflow-y: auto;
+    }
+
+    .editor-preview img {
+        max-width: 100%;
+        height: auto;
+    }
+
+    .CodeMirror {
+        resize: vertical; /* Allows vertical resizing */
+        overflow: auto;  /* Ensures scrollbars appear if content overflows */
+        min-height: 200px; /* Optional: Set a minimum height */
+    }
+
+    .file-item.selected {
+        background-color: rgba(0, 0, 0, 0.1) !important;
+    }
+
+    .folder-item.selected {
+        background-color: rgba(0, 0, 0, 0.1) !important;
+    }
+</style>
+<br>
+<div class="ui container" style="margin-top: 20px;">
+    <h2 class="ui header">New Post</h2>
+    <!-- Load draft message -->
+    <div id="loadDraftMessage" class="ui hidden message">
+        <div class="header">Saved Draft Found</div>
+        <p>A saved draft was found. Would you like to load it?</p>
+        <button class="ui green basic button" id="loadDraftButton">Load Draft</button>
+        <button class="ui red basic button" id="discardDraftButton">Discard</button>
+    </div>
+    <!-- Post Editor -->
+    <div class="ui form">
+        <div class="field">
+            <label for="title">Title</label>
+            <input type="text" id="title" placeholder="Enter title here">
+        </div>
+        
+        <div class="field">
+            <label for="editor">Post Content</label>
+            <textarea id="editor"></textarea>
+        </div>
+        
+        <button class="ui basic button" id="saveButton"><i class="ui green upload icon"></i>Publish</button>
+        <button class="ui blue basic button" id="saveDraftButton"><i class="ui save icon"></i>Save as Draft</button>
+    </div>
+</div>
+
+<!-- Media Selector -->
+<div id="postengine_media_selector" class="ui modal">
+    <i class="close icon"></i>
+    <div class="content">
+        <div class="ui fluid input">
+            <button class="ui basic circular icon button" id="backButton">
+                <i class="arrow left icon"></i>
+            </button>
+            <button class="ui basic circular icon button" id="parentButton" style="margin-left: 0.4em;">
+                <i class="arrow up icon"></i>
+            </button>
+            <input type="text" placeholder="/" id="locationBar" style="margin-left: 0.4em;" value="/">
+        </div>
+        <div id="mediaContent" class="ui segments" style="margin-top: 1em;">
+            <div class="ui basic segment" style="pointer-events: none; user-select: none; opacity: 0.5;">
+                <i class="ui green circle check icon"></i> Loading media files...
+            </div>
+        </div>
+        <div id="limited_access_warning" class="ui hidden red message" style="display:none;">
+            This folder is not accessible by guest visitors but only logged-in users.
+        </div>
+    </div>
+    <div class="actions">
+        <div class="ui black deny button">
+            Cancel
+        </div>
+        <button class="ui basic button" id="importButton">
+            <i class="green download icon"></i> Import Selected
+        </button>
+    </div>
+</div>
+
+<script>
+    var currentDir = "/img"; // Initialize current path
+    var pathHistory = [currentDir]; // Initialize path history
+
+    $(document).ready(function() {
+        // Initialize SimpleMDE
+        var simplemde = new SimpleMDE({ 
+            element: document.getElementById("editor"),
+            toolbar: [
+                "bold", 
+                "italic", 
+                "heading", 
+                "|", 
+                "quote", 
+                "unordered-list", 
+                "ordered-list", 
+                "|", 
+                "link", 
+                "image",
+                {
+                    name: "mediaSelect",
+                    action: function customFunction(editor) {
+                        openMediaSelector(editor);
+                    },
+                    className: "fa fa-folder", // Font Awesome icon
+                    title: "Select Media"
+                },
+                "|", 
+                "preview",
+            ]
+        });
+
+        /*
+            Media Selector
+
+            This function will open the media selector modal when the media button is clicked.
+        */
+
+        function fetchDirectoryContents(path) {
+            $.ajax({
+                url: `/api/fs/list?dir=${encodeURIComponent(path)}`,
+                method: 'GET',
+                success: function(data) {
+                    if (data.error == undefined){
+                        pathHistory.push(currentDir);
+                        if (path.endsWith("/")) {
+                            currentDir = path.slice(0, -1); // Remove trailing slash if present
+                        } else {
+                            currentDir = path; // Update current directory
+                        }
+                        $("#locationBar").val(currentDir); // Update location bar
+                        renderDirectoryContents(data);
+                    } else {
+                        alert("Failed to fetch directory contents: " + data.error);
+                    }
+                },
+                error: function() {
+                    alert("Error fetching directory contents.");
+                }
+            });
+        }
+
+        function renderDirectoryContents(contents) {
+            const container = $("#mediaContent");
+            container.empty();
+            let folderElements = ``;
+            let fileElements = ``;
+            let selectableFileCounter = 0;
+            contents.forEach(item => {
+                if (item.IsDir) {
+                    folderElements += (`
+                    <div class="ui segment folder-item selectable" data-path="${currentDir + "/" + item.Filename}">
+                        <i class="yellow folder icon"></i> ${item.Filename}
+                    </div>
+                    `);
+                    selectableFileCounter++;
+                } else if (!item.IsDir && isWebSafeFile(item.Filename)) {
+                    let fileIcon = getFileTypeIcons(item.Filename);
+                    fileElements += (`
+                    <div class="ui segment file-item selectable" data-path="${currentDir + "/" + item.Filename}" data-type="${getFileType(item.Filename)}">
+                        ${fileIcon} ${item.Filename}
+                    </div>
+                    `);
+                    selectableFileCounter++;
+                }
+            });
+
+            container.append(folderElements);
+            container.append(fileElements);
+
+            if (selectableFileCounter == 0) {
+                container.append("<div class='ui basic segment' style='pointer-events: none; user-select: none; opacity: 0.5;'><i class='ui green circle check icon'></i> No usable files / folders found</div>");
+            }
+
+            // Add click handlers for folders and files
+            $(".folder-item").on('dblclick', function() {
+                const path = $(this).data('path');
+                fetchDirectoryContents(path);
+            });
+
+            $(".folder-item").on("click", function(event) {
+                const path = $(this).data('path');
+                if (!event.ctrlKey) {
+                    // Highlight the selected folder and remove previous selection if ctrl is not held
+                    $(".folder-item.selected").removeClass("selected");
+                }
+                $(this).toggleClass("selected"); // Toggle selection for the clicked item
+            });
+
+            $(".file-item").on('dblclick', function() {
+                $("#postengine_media_selector").modal("hide");
+                const path = $(this).data('path');
+                const type = $(this).data('type');
+                addMediaFileToEditor(path, type);
+            });
+
+            $(".file-item").on("click", function(event) {
+                const path = $(this).data('path');
+                const type = $(this).data('type');
+                if (!event.ctrlKey) {
+                    // Highlight the selected file and remove previous selection if ctrl is not held
+                    $(".file-item.selected").removeClass("selected");
+                }
+                $(this).toggleClass("selected"); // Toggle selection for the clicked item
+            });
+
+            //Show warning if the directory starts with /admin or /store
+            if (currentDir.startsWith("/admin") || currentDir.startsWith("/store")) {
+                $("#limited_access_warning").show();
+            } else {
+                $("#limited_access_warning").hide();
+            }
+        }
+
+        // Bind event to the back button
+        $('#backButton').on('click', function() {
+            if (pathHistory.length > 1) {
+                pathHistory.pop(); // Remove the current directory
+                const previousPath = pathHistory.pop();
+                fetchDirectoryContents(previousPath);
+            }
+        });
+
+        // Bind event to the parent button
+        $('#parentButton').on('click', function() {
+            if (currentDir == "/") { 
+                msgbox("Already at the root directory", 3000);
+                return;
+            }
+            let parentPath = currentDir.split("/");
+            parentPath.pop(); // Remove the last element (current directory)
+            parentPath = parentPath.join("/"); // Join the remaining elements to form the parent path
+            if (parentPath == ""){
+                parentPath = "/"; // Set to root if empty
+            }
+            fetchDirectoryContents(parentPath);
+        });
+
+        // Bind event to the import button
+        $('#importButton').on('click', function() {
+            const selectedFolders = $(".folder-item.selected");
+            const selectedFiles = $(".file-item.selected");
+            if (selectedFiles.length === 0 && selectedFolders.length === 0) {
+                alert("No files selected for import.");
+                return;
+            }
+
+            selectedFolders.each(function() {
+                const path = $(this).data('path');
+                const type = "link"; 
+                addMediaFileToEditor(path, type);
+            });
+
+            selectedFiles.each(function() {
+                const path = $(this).data('path');
+                const type = $(this).data('type');
+                addMediaFileToEditor(path, type);
+            });
+
+            $("#postengine_media_selector").modal("hide");
+        });
+
+        function isWebSafeFile(fileName) {
+            const webSafeExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'webm', 'mp3', 'ogg'];
+            const extension = fileName.split('.').pop().toLowerCase();
+            return webSafeExtensions.includes(extension);
+        }
+
+        function getFileType(fileName) {
+            const extension = fileName.split('.').pop().toLowerCase();
+            if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
+                return 'image';
+            } else if (extension === 'webm') {
+                return 'video';
+            } else if (['mp3', 'ogg'].includes(extension)) {
+                return 'audio';
+            } else {
+                return 'unknown';
+            }
+        }
+
+
+        function openMediaSelector(editor){
+            // Reset history
+            pathHistory = []; // Reset path history
+            fetchDirectoryContents(currentDir);
+            $("#postengine_media_selector").modal("show");
+        }
+
+        function addMediaFileToEditor(mediaLink, mediaType){
+            if (mediaType === 'image') {
+                simplemde.codemirror.replaceSelection(`![Image](${mediaLink})`);
+             } else if (mediaType === 'video') {
+                let mimeType = mediaLink.split('.').pop().toLowerCase() === 'webm' ? 'video/webm' : 'video/mp4';
+                simplemde.codemirror.replaceSelection(`<video style="background:black;" width="720" height="480" controls><source src="${mediaLink}" type="${mimeType}"></video>`);
+            } else if (mediaType === 'audio') {
+                simplemde.codemirror.replaceSelection(`<audio style="min-width: 512px;" controls><source src="${mediaLink}" type="audio/mpeg">Your browser does not support the audio element.</audio>`);
+            } else if (mediaType === 'link') {
+                let folderName = mediaLink.split("/").pop(); // Extract folder name from path
+                simplemde.codemirror.replaceSelection(`[${folderName}](${mediaLink})`);
+            } else {
+                alert("Unsupported media type!");
+                return;
+            }
+        }
+
+        function getFileTypeIcons(fileName) {
+            const extension = fileName.split('.').pop().toLowerCase();
+            if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
+                return '<i class="blue file image icon"></i>';
+            } else if (extension === 'webm') {
+                return '<i class="violet file video icon"></i>';
+            } else if (['mp3', 'ogg'].includes(extension)) {
+                return '<i class="green file audio icon"></i>';
+            } else {
+                return '<i class="file icon"></i>';
+            }
+        }
+
+
+        // Initialize with root directory
+        fetchDirectoryContents(currentDir);
+
+        // Save button functionality
+        $('#saveButton').on('click', function() {
+            const title = $('#title').val();
+            const tags = $('#tags').val();
+            const content = simplemde.value();
+
+            console.log("Title:", title);
+            console.log("Tags:", tags);
+            console.log("Content:", content);
+
+            // Add your save logic here
+            alert("Content saved successfully!");
+        });
+
+        // Save Draft button functionality
+        $('#saveDraftButton').on('click', function() {
+            const title = $('#title').val();
+            const content = simplemde.value();
+
+            if (title.trim() === "" && content.trim() === "") {
+                alert("Cannot save an empty draft.");
+                return;
+            }
+
+            localStorage.setItem('draftTitle', title);
+            localStorage.setItem('draftContent', content);
+
+            msgbox("Draft saved successfully!");
+        });
+
+        // Discard Draft button functionality
+        $('#discardDraftButton').on('click', function() {
+            if (confirm("Are you sure you want to discard the draft? This action cannot be undone.")) {
+                localStorage.removeItem('draftTitle');
+                localStorage.removeItem('draftContent');
+
+                $('#title').val('');
+                simplemde.value('');
+
+                msgbox("Draft discarded successfully!");
+
+                $('#loadDraftMessage').addClass('hidden');
+            }
+        });
+
+        // Load draft from localStorage on page load
+        const savedTitle = localStorage.getItem('draftTitle');
+        const savedContent = localStorage.getItem('draftContent');
+
+        if (savedTitle || savedContent) {
+            $('#loadDraftMessage').removeClass('hidden');
+
+            $('#loadDraftButton').on('click', function() {
+                $('#title').val(savedTitle || '');
+                simplemde.value(savedContent || '');
+                $('#loadDraftMessage').addClass('hidden');
+            });
+        }
+    });
+</script>

+ 1 - 36
sd_card/www/posts.html

@@ -63,18 +63,7 @@
         } 
     }   
 
-    //Storage and loader utils
-    function setValue(key, value, callback){
-        $.get("/api/pref/set?key=" + key + "&value=" + value, function(data){
-            callback(data);
-        });
-    }
-
-    function loadValue(key, callback){
-        $.get("/api/pref/get?key=" + key, function(data){
-            callback(data);
-        });
-    }
+    
 
 /*
     New Post
@@ -231,31 +220,7 @@ function forceUpdatePostIndex(){
     });
 }
 
-function updatePostIndex(callback=undefined){
-    let postList = [];
-    $.ajax({
-        url: "/api/fs/list?dir=/blog/posts",
-        success: function(data){
-            data.forEach(file => {
-                let filename = file.Filename;
-                let ext = filename.split(".").pop();
-                if (ext == "md" && file.IsDir == false){
-                    //Markdown file. Render it
-                    postList.push(filename);
-                }
-            });
 
-            setValue("blog-posts", btoa(encodeURIComponent(JSON.stringify(postList))), function(data){
-                console.log(data);
-                if (callback != undefined){
-                    callback();
-                }
-            });
-        }
-    });
-
-   
-}
 
 //Render post
 function renderPost(filename){

+ 0 - 0
sd_card/www/blog/index.html → sd_card/www/site/index.html


+ 0 - 0
sd_card/www/blog/posts/1710676940_Hello World!.md → sd_card/www/site/posts/1710676940_Hello World!.md


+ 0 - 0
sd_card/www/blog/posts/index.html → sd_card/www/site/posts/index.html