瀏覽代碼

Added post editor

Toby Chui 9 小時之前
父節點
當前提交
61c9344ee2

+ 0 - 24
sd_card/www/admin/mde/index.html

@@ -147,30 +147,6 @@
                         callback();
                     }
                 });
-                /*
-                ao_module_agirun("./MDEditor/filesaver.js", {
-                    filepath: filepath, 
-                    content: newcontent
-                }, function(data){
-                    console.log(data);
-                    if (data.error !== undefined){
-                        alert(data.error);
-                    }else{
-                        //Save succeed. Update last saved content
-                        lastSaveContent = newcontent;
-
-                        //Update the title as well
-                        document.title = (originalTitle);
-
-                        if (callback != undefined){
-                            callback();
-                        }
-                    }
-                }, function(){
-                    alert("Save File Failed!")
-                });
-                */
-
             }
 
 

+ 43 - 13
sd_card/www/admin/post.html

@@ -46,29 +46,29 @@
           <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>
+              <a class="item active" name="allposts" xtab="posteng/all.html">All Posts</a>
+              <a class="item" name="newpost" 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>
+              <a class="item" name="library" xtab="">Convert</a>
+              <a class="item" name="paste" xtab="">Paste</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>
+              <a class="item" name="allpages" xtab="">All Pages</a>
+              <a class="item" name="newpage" 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>
+              <a class="item" name="settings" xtab="">General</a>
+              <a class="item" name="permlinks" xtab="">Permalinks</a>
             </div>
           </div>
       </div>
@@ -80,7 +80,7 @@
         <span id="msgbox_text">This is a message</span>
       </div>
       <script>
-        let editingPost = ""; //The name of the post being edited, if empty = new post
+        var 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
@@ -96,15 +96,45 @@
             });
         });
 
+        function switchToTab(tabName) {
+           switch(tabName) {
+                case "allposts":
+                    $("#postengine_tab").load("posteng/all.html");
+                    break;
+                case "newpost":
+                    $("#postengine_tab").load("posteng/new.html");
+                    break;
+                case "library":
+                    $("#postengine_tab").load("posteng/library.html");
+                    break;
+                case "paste":
+                    $("#postengine_tab").load("posteng/paste.html");
+                    break;
+                case "allpages":
+                    $("#postengine_tab").load("posteng/pages.html");
+                    break;
+                case "newpage":
+                    $("#postengine_tab").load("posteng/newpage.html");
+                    break;
+                case "settings":
+                    $("#postengine_tab").load("posteng/settings.html");
+                    break;
+                case "permlinks":
+                    $("#postengine_tab").load("posteng/permalinks.html");
+                    break;
+            }
+            // Update the active class on the menu items
+            $("#postengine_side_menu .item.active").removeClass("active");
+            $("#postengine_side_menu .item[name='" + tabName + "']").addClass("active");
+
+        }
+
 
         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);
+          $(snackbar).fadeIn("fast").delay(duration).fadeOut("fast");
         }
 
         /*

+ 8 - 1
sd_card/www/admin/posteng/all.html

@@ -44,6 +44,10 @@
             </div>
         </div>
     </div>
+
+    <div class="ui active dimmer" style="display:none;" id="loadingDimmer">
+        <div class="ui text loader">Loading post contents</div>
+    </div>
 </div>
 
 <script>
@@ -99,7 +103,9 @@
 
     //Open post editor
     function editPost(postID){
-        //TODO: Implement the post editor functionality
+        $("#loadingDimmer").show(); // Show loading dimmer
+        editingPost = postID; // Store the postID for editing
+        $("#postengine_tab").load("posteng/edit.html");
     }
 
     // Delete post
@@ -119,6 +125,7 @@
             url: `/api/fs/del?target=/site/posts/${postID}`,
             type: 'POSt',
             success: function(response) {
+                updatePostIndex(); // Update the post index
                 console.log('Post deleted:', response);
                 $('#deleteConfirmModal').modal('hide'); // Hide the modal
                 getPosts(); // Refresh the post list

+ 452 - 5
sd_card/www/admin/posteng/edit.html

@@ -1,9 +1,456 @@
-<br>
+
 <link rel="stylesheet" href="mde/script/mde.css">
 <script src="mde/script/mde.js"></script>
-<div class="ui container">
-   
-</div>  
+<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,
+    .CodeMirror-scroll {
+        height: 520px !important;
+        min-height: 520px !important;
+        max-height: 520px !important;
+    }
+
+    .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;">
+    <div class="ui header">
+        Edit Post
+    </div>
+    <!-- Post Editor -->
+    <div class="ui form">
+        <div class="disabled field">
+            <label for="title">Title</label>
+            <input type="text" id="title" placeholder="Enter title here" readonly="readonly">
+        </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 save icon"></i> Update</button>
+        <button class="ui basic button" id="cancelButton" onclick="handleBackToPostList();">
+            <i class="ui red cancel icon"></i> Discard Changes
+        </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>
+<br><br><br>
 <script>
+    var currentDir = "/"; // Initialize current path
+    var pathHistory = []; // Initialize path history
+    var unsafeCharacters = /[<>:"/\\|?*\x00-\x1F]/; // Regex for unsafe characters
+    if (typeof(autosaveInterval) != "undefined"){
+        clearInterval(autosaveInterval); // Clear any existing autosave interval
+    }
+
+    //Load the post content if editingPost is not empty
+    var editingPostFilename = editingPost;
+    if (loadPostContent == ""){
+        alert("No post selected for editing.");
+        switchToTab("allposts");
+    }else{
+        loadPostContent(editingPost); // Load the content of the post into the editor
+    }
+
+    //Generate the title from the filename
+    var editingPostTitle = editingPost.split("_").slice(1).join("_").split(".")[0]; // Extract the post title from the filename
+    if (editingPostTitle.endsWith(".md")) {
+        editingPostTitle = editingPostTitle.slice(0, -3); // Remove the ".md" suffix
+    }
+    $("#title").val(editingPostTitle); 
+
+    function handleBackToPostList(){
+        if (confirm("Are you sure you want to discard the editing content?")) {
+            switchToTab("allposts"); // Switch to the all posts tab
+        }
+    }
+    // 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",
+        ]
+    });
+
+    // Allow vertical resize for the editor
+    document.getElementById("editor").style.resize = "vertical";
+
+    /*
+        Post Loader
+    */
+
+    function loadPostContent(filename) {
+        let filepath = "/site/posts/" + filename;
+        $.get("/api/fs/download?file=" + filepath, function(data) {
+            if (data.error == undefined){
+                simplemde.value(data); // Load the content into the editor
+            } else {
+                alert("Failed to load post content: " + data.error);
+            }
+        }).fail(function() {
+            alert("Error loading post content.");
+        });
+    }
+
+    /*
+        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>';
+        }
+    }
     
-</script>
+
+    // Save button functionality
+    $('#saveButton').on('click', function() {
+        const title = $('#title').val();
+        const content = simplemde.value();
+
+        console.log("Title:", title);
+        console.log("Content:", content);
+
+        createNewPost(title, content);
+
+    });
+
+
+    /*
+        Post Create Function
+    */
+
+    // Function to create a new post
+    function createNewPost(title, content="# Hello World\n"){
+        if (title.trim() === "" && content.trim() === "") {
+            alert("Cannot create an empty post.");
+            return;
+        }
+        $("#saveButton").addClass("loading disabled");
+        //Create the markdown file at the /blog/posts folder
+        const blob = new Blob([content], { type: 'text/plain' });
+        let storeFilename = editingPostFilename; //Overwrite on top of the original one
+        const file = new File([blob], storeFilename);
+        handleFile(file, "/site/posts", function(){
+            //Update the post index
+            updatePostIndex(function(){
+                $("#confirmNewPostBtn").removeClass("loading disabled");
+                msgbox("Post created successfully!", 3000);
+
+                switchToTab("allposts"); // Switch to the all posts tab
+            });
+
+            //Clear the draft 
+            localStorage.removeItem('draftTitle');
+            localStorage.removeItem('draftContent');
+        });
+    }
+
+    // Error handler for AJAX requests
+    function errorHandler(event) {
+        msgbox("Failed to create post: " + event.target.responseText, 3000);
+        $("#saveButton").removeClass("loading disabled");
+    }
+
+    // Function to handle file upload
+    function handleFile(file, dir=currentPath, callback=undefined) {
+        // Perform actions with the selected file
+        var formdata = new FormData();
+        formdata.append("file1", file);
+        var ajax = new XMLHttpRequest();
+        ajax.addEventListener("load", function(event){
+            let responseText = event.target.responseText;
+            try{
+                responseText = JSON.parse(responseText);
+                if (responseText.error != undefined){
+                    alert(responseText.error);
+                }
+            }catch(ex){
+
+            }
+            if (callback != undefined){
+                callback();
+            }
+        }, false); // doesnt appear to ever get called even upon success
+        ajax.addEventListener("error", errorHandler, false);
+        //ajax.addEventListener("abort", abortHandler, false);
+        ajax.open("POST", "/upload?dir=" + dir);
+        ajax.send(formdata);
+    }
+
+    $(document).ready(function() {
+        // Initialize with root directory
+        fetchDirectoryContents(currentDir);
+    });
+</script>

+ 362 - 256
sd_card/www/admin/posteng/new.html

@@ -56,9 +56,8 @@
             <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>
+        <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>
 
@@ -93,310 +92,417 @@
         </button>
     </div>
 </div>
-
+<br><br><br>
 <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"
+    var currentDir = "/"; // Initialize current path
+    var pathHistory = []; // Initialize path history
+    var unsafeCharacters = /[<>:"/\\|?*\x00-\x1F]/; // Regex for unsafe characters
+    if (typeof(autosaveInterval) != "undefined"){
+        clearInterval(autosaveInterval); // Clear any existing autosave interval
+    }
+    var autosaveInterval;
+    // Load draft from localStorage on page load
+    var savedTitle = localStorage.getItem('draftTitle');
+    var savedContent = localStorage.getItem('draftContent');
+
+    // Initialize SimpleMDE
+    var simplemde = new SimpleMDE({ 
+        element: document.getElementById("editor"),
+        toolbar: [
+            "bold", 
+            "italic", 
+            "heading", 
+            "|", 
+            "code",
+            "quote", 
+            "unordered-list", 
+            "ordered-list", 
+            "|", 
+            "link", 
+            "table",
+            "horizontal-rule",
+            "image",
+            {
+                name: "mediaSelect",
+                action: function customFunction(editor) {
+                    openMediaSelector(editor);
                 },
-                "|", 
-                "preview",
-            ]
-        });
+                className: "fa fa-folder", // Font Awesome icon
+                title: "Select Media"
+            },
+            "|", 
+            "preview",
+        ]
+    });
 
-        // Allow vertical resize for the editor
-        document.getElementById("editor").style.resize = "vertical";
-
-        /*
-            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.");
-                }
-            });
-        }
+    // Allow vertical resize for the editor
+    document.getElementById("editor").style.resize = "vertical";
 
-        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++;
-                }
-            });
+    /*
+        Media Selector
 
-            container.append(folderElements);
-            container.append(fileElements);
+        This function will open the media selector modal when the media button is clicked.
+    */
 
-            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>");
+    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.");
             }
+        });
+    }
 
-            // Add click handlers for folders and files
-            $(".folder-item").on('dblclick', function() {
-                const path = $(this).data('path');
-                fetchDirectoryContents(path);
-            });
+    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++;
+            }
+        });
 
-            $(".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
-            });
+        container.append(folderElements);
+        container.append(fileElements);
 
-            $(".file-item").on('dblclick', function() {
-                $("#postengine_media_selector").modal("hide");
-                const path = $(this).data('path');
-                const type = $(this).data('type');
-                addMediaFileToEditor(path, type);
-            });
+        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>");
+        }
 
-            $(".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
-            });
+        // Add click handlers for folders and files
+        $(".folder-item").on('dblclick', function() {
+            const path = $(this).data('path');
+            fetchDirectoryContents(path);
+        });
 
-            //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();
+        $(".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
+        });
 
-        // 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);
-            }
+        $(".file-item").on('dblclick', function() {
+            $("#postengine_media_selector").modal("hide");
+            const path = $(this).data('path');
+            const type = $(this).data('type');
+            addMediaFileToEditor(path, type);
         });
 
-        // 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
+        $(".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");
             }
-            fetchDirectoryContents(parentPath);
+            $(this).toggleClass("selected"); // Toggle selection for the clicked item
         });
 
-        // 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;
-            }
+        //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();
+        }
+    }
 
-            selectedFolders.each(function() {
-                const path = $(this).data('path');
-                const type = "link"; 
-                addMediaFileToEditor(path, type);
-            });
+    // 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);
+        }
+    });
 
-            selectedFiles.each(function() {
-                const path = $(this).data('path');
-                const type = $(this).data('type');
-                addMediaFileToEditor(path, type);
-            });
+    // 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);
+    });
 
-            $("#postengine_media_selector").modal("hide");
+    // 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);
         });
 
-        function isWebSafeFile(fileName) {
-            const webSafeExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'webm', 'mp3', 'ogg'];
-            const extension = fileName.split('.').pop().toLowerCase();
-            return webSafeExtensions.includes(extension);
+        $("#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 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>';
+        }
+    }
 
-        function openMediaSelector(editor){
-            // Reset history
-            pathHistory = []; // Reset path history
-            fetchDirectoryContents(currentDir);
-            $("#postengine_media_selector").modal("show");
+    /*
+        Title Validation
+
+        This function will validate the title input to ensure it does not contain unsafe characters.
+        If the title contains unsafe characters, the save button will be disabled and an alert will be shown.
+    */
+   
+    function validateTitle() {
+        const title = $('#title').val();
+        if (unsafeCharacters.test(title) || title.length > 19) {
+            msgbox("Title contains unsafe characters or is too long (max 19 characters).", 3000);
+            return false;
         }
+        return true;
+    }
+    
 
-        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;
-            }
+    // Save button functionality
+    $('#saveButton').on('click', function() {
+        const title = $('#title').val();
+        const content = simplemde.value();
+
+        console.log("Title:", title);
+        console.log("Content:", content);
+
+        if (!validateTitle()){
+            return; // Exit if title is invalid
         }
 
-        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>';
-            }
+        createNewPost(title, content);
+
+    });
+
+    // 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);
 
-        // Initialize with root directory
-        fetchDirectoryContents(currentDir);
+        msgbox("Draft saved");
+    });
 
-        // Save button functionality
-        $('#saveButton').on('click', function() {
-            const title = $('#title').val();
-            const tags = $('#tags').val();
-            const content = simplemde.value();
+    // 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');
 
-            console.log("Title:", title);
-            console.log("Tags:", tags);
-            console.log("Content:", content);
+            $('#title').val('');
+            simplemde.value('');
 
-            // Add your save logic here
-            alert("Content saved successfully!");
-        });
+            msgbox("Draft discarded");
 
-        // Save Draft button functionality
-        $('#saveDraftButton').on('click', function() {
-            const title = $('#title').val();
-            const content = simplemde.value();
+            $('#loadDraftMessage').addClass('hidden');
+        }
+    });
 
-            if (title.trim() === "" && content.trim() === "") {
-                alert("Cannot save an empty draft.");
-                return;
-            }
+   
 
-            localStorage.setItem('draftTitle', title);
-            localStorage.setItem('draftContent', content);
+    if (savedTitle || savedContent) {
+        $('#loadDraftMessage').removeClass('hidden');
 
-            msgbox("Draft saved successfully!");
+        $('#loadDraftButton').on('click', function() {
+            $('#title').val(savedTitle || '');
+            simplemde.value(savedContent || '');
+            $('#loadDraftMessage').addClass('hidden');
         });
+    }
 
-        // 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');
+    // Autosave functionality
+    autosaveInterval = setInterval(function() {
+        if (!document.getElementById("editor")) {
+            clearInterval(autosaveInterval);
+            return;
+        }
+        const title = $('#title').val();
+        const content = simplemde.value();
 
-                $('#title').val('');
-                simplemde.value('');
+        if (title.trim() === "" && content.trim() === "") {
+            return; // Don't save empty drafts
+        }
 
-                msgbox("Draft discarded successfully!");
+        localStorage.setItem('draftTitle', title);
+        localStorage.setItem('draftContent', content);
+    }, 5000); // Autosave every 5 seconds
 
-                $('#loadDraftMessage').addClass('hidden');
-            }
+
+    /*
+        Post Create Function
+    */
+
+    // Function to create a new post
+    function createNewPost(title, content="# Hello World\n"){
+        if (title.trim() === "" && content.trim() === "") {
+            alert("Cannot create an empty post.");
+            return;
+        }
+        $("#saveButton").addClass("loading disabled");
+        //Create the markdown file at the /blog/posts folder
+        const blob = new Blob([content], { type: 'text/plain' });
+        let storeFilename = parseInt(Date.now()/1000) + "_" + title+'.md';
+        const file = new File([blob], storeFilename);
+        handleFile(file, "/site/posts", function(){
+            //Update the post index
+            updatePostIndex(function(){
+                $("#confirmNewPostBtn").removeClass("loading disabled");
+                msgbox("Post created successfully!", 3000);
+
+                switchToTab("allposts"); // Switch to the all posts tab
+            });
+
+            //Clear the draft 
+            localStorage.removeItem('draftTitle');
+            localStorage.removeItem('draftContent');
         });
+    }
 
-        // Load draft from localStorage on page load
-        const savedTitle = localStorage.getItem('draftTitle');
-        const savedContent = localStorage.getItem('draftContent');
+    // Error handler for AJAX requests
+    function errorHandler(event) {
+        msgbox("Failed to create post: " + event.target.responseText, 3000);
+        $("#saveButton").removeClass("loading disabled");
+    }
 
-        if (savedTitle || savedContent) {
-            $('#loadDraftMessage').removeClass('hidden');
+    // Function to handle file upload
+    function handleFile(file, dir=currentPath, callback=undefined) {
+        // Perform actions with the selected file
+        var formdata = new FormData();
+        formdata.append("file1", file);
+        var ajax = new XMLHttpRequest();
+        ajax.addEventListener("load", function(event){
+            let responseText = event.target.responseText;
+            try{
+                responseText = JSON.parse(responseText);
+                if (responseText.error != undefined){
+                    alert(responseText.error);
+                }
+            }catch(ex){
 
-            $('#loadDraftButton').on('click', function() {
-                $('#title').val(savedTitle || '');
-                simplemde.value(savedContent || '');
-                $('#loadDraftMessage').addClass('hidden');
-            });
-        }
+            }
+            if (callback != undefined){
+                callback();
+            }
+        }, false); // doesnt appear to ever get called even upon success
+        ajax.addEventListener("error", errorHandler, false);
+        //ajax.addEventListener("abort", abortHandler, false);
+        ajax.open("POST", "/upload?dir=" + dir);
+        ajax.send(formdata);
+    }
+
+    $(document).ready(function() {
+        // Initialize with root directory
+        fetchDirectoryContents(currentDir);
     });
 </script>

+ 0 - 94
sd_card/www/posts.html

@@ -63,85 +63,10 @@
         } 
     }   
 
-    
 
-/*
-    New Post
-
-    New post is created via creating a markdown file in the server
-    side and open it with the markdown editor
-*/
-
-$("#createNewPostBtn").on("click", function(){
-    $("#newPostModal").toggle("fast");
-});
-
-function createNewPost(){
-    let filename = $("#newPostTitle").val().trim();
-    if (filename == ""){
-        alert("Post title cannot be empty.");
-        return;
-    }
-
-    if (filename.indexOf("/") >= 0){
-        //Contains /. Reject
-        alert("File name cannot contain path seperator");
-        return;
-    }
-
-    $("#confirmNewPostBtn").addClass("loading").addClass("disabled");
-    //Create the markdown file at the /blog/posts folder
-    const blob = new Blob(["# Hello World\n"], { type: 'text/plain' });
-    let storeFilename = parseInt(Date.now()/1000) + "_" + filename+'.md';
-    const file = new File([blob], storeFilename);
-    handleFile(file, "/blog/posts", function(){
-        //Update the post index
-        updatePostIndex();
 
-        $("#confirmNewPostBtn").removeClass("loading").removeClass("disabled");
 
-        //Open the markdown file in new tab
-        let hash = encodeURIComponent(JSON.stringify({
-            "filename": storeFilename,
-            "filepath": "/blog/posts/" + storeFilename
-        }))
-        window.open("/admin/mde/index.html#" + hash);
 
-        $("#newPostModal").hide();
-    });
-
-    
-}
-
-function handleFile(file, dir=currentPath, callback=undefined) {
-    // Perform actions with the selected file
-    var formdata = new FormData();
-    formdata.append("file1", file);
-    var ajax = new XMLHttpRequest();
-    ajax.addEventListener("load", function(event){
-        let responseText = event.target.responseText;
-        try{
-            responseText = JSON.parse(responseText);
-            if (responseText.error != undefined){
-                alert(responseText.error);
-            }
-        }catch(ex){
-
-        }
-        if (callback != undefined){
-            callback();
-        }
-    }, false); // doesnt appear to ever get called even upon success
-    ajax.addEventListener("error", errorHandler, false);
-    //ajax.addEventListener("abort", abortHandler, false);
-    ajax.open("POST", "/upload?dir=" + dir);
-    ajax.send(formdata);
-}
-
-function errorHandler(event) {
-    aelrt("New Post creation failed");
-    $("#pasteButton").removeClass("disabled");
-}
 
 /*
     Post Edit functions
@@ -156,25 +81,6 @@ function editPost(btn){
     window.open("/admin/mde/index.html#" + hash);
 }
 
-function deletePost(btn){
-    let postFilename = $(btn).attr("filename");
-    let postTitle = $(btn).attr("ptitle");
-    if (confirm("Confirm remove post titled: " + postTitle + "?")){
-        $.ajax({
-            url: "/api/fs/del?target=/blog/posts/" + postFilename,
-            method: "POST",
-            success: function(data){
-                if (data.error != undefined){
-                    alert("Post delete failed. See console for more info.");
-                    console.log(data.error);
-                }else{
-                   //Deleted
-                   initPosts();
-                }
-            }
-        });
-    }
-}
 
 /*
     Rendering for Posts