Эх сурвалжийг харах

Refactor and add INI Reader

xcbosa-itx 2 жил өмнө
parent
commit
74761e5cb5

+ 2 - 1
CMakeLists.txt

@@ -25,13 +25,14 @@ execute_process(
 
 aux_source_directory(. DIR_SRCS)
 aux_source_directory(controller DIR_CONTROLLERS)
+aux_source_directory(controller/auto-generated DIR_CONTROLLERS_AUTOGENERATED)
 aux_source_directory(view DIR_VIEW)
 
 add_subdirectory(utils)
 add_subdirectory(httpserver)
 add_subdirectory(processor)
 
-add_executable(FRPCWebUI ${DIR_SRCS} ${DIR_CONTROLLERS} ${DIR_VIEW})
+add_executable(FRPCWebUI ${DIR_SRCS} ${DIR_CONTROLLERS} ${DIR_CONTROLLERS_AUTOGENERATED} ${DIR_VIEW})
 
 find_package(Threads)
 target_link_libraries(FRPCWebUI PRIVATE HttpServerUtils HttpServer Processor ${CMAKE_THREAD_LIBS_INIT})

+ 36 - 0
controller/EntryController.cpp

@@ -0,0 +1,36 @@
+//
+// Created by xcbosa on 2023/1/30.
+//
+
+#include "../processor/processor.h"
+#include "../webuiconf.h"
+#include "../processor/templates/framework7/Framework7Document.hpp"
+
+using namespace std;
+using namespace xc::processor;
+using namespace xc::processor::templates;
+using namespace xc::processor::templates::framework7;
+using namespace configor;
+
+namespace xc::controller {
+
+    ResponseData *EntryController(RequestData request) {
+        bool isLogin = false;
+        return new TemplateResponseData({
+            If(isLogin, {
+                Framework7Document({
+                    p("登陆成功")
+                }, {
+                    a("2023 © Frp-WebUI by XCBOSA")
+                        .classAdd("link")
+                        .onclick("window.open('https://github.com/XCBOSA/frp-webui-500k.git')")
+                })
+            }, {
+                ContentGeneratorReference("LoginController", request)
+            })
+        });
+    }
+
+    ContentGeneratorDefineS(request.getURL() == "/" || request.getURL().length() == 0, EntryController(request))
+
+}

+ 41 - 0
controller/LoginController.cpp

@@ -0,0 +1,41 @@
+//
+// Created by xcbosa on 2023/1/30.
+//
+
+#include "../processor/processor.h"
+#include "../webuiconf.h"
+#include "../processor/templates/framework7/Framework7Document.hpp"
+
+using namespace std;
+using namespace xc::processor;
+using namespace xc::processor::templates;
+using namespace xc::processor::templates::framework7;
+using namespace configor;
+
+namespace xc::controller {
+
+    ResponseData *LoginController(RequestData request) {
+        return new TemplateResponseData({
+            Framework7Document({
+                FormView({
+                    FormInputView("userName", "用户名", "text", "输入您的用户名"),
+                    FormInputView("password", "密码", "password", "输入您的密码")
+                }),
+                BlockView({
+                    BlockColumnView({
+                        ButtonView("登陆"),
+                        VerticalSpacer(10),
+                        p("如果您需要注册,请联系管理员")
+                    })
+                })
+                }, {
+                a("2023 © Frp-WebUI by XCBOSA")
+                    .classAdd("link")
+                    .onclick("window.open('https://github.com/XCBOSA/frp-webui-500k.git')")
+            })
+        });
+    }
+
+    ContentGeneratorDefineWithNameS("LoginController", false, LoginController(request))
+
+}

+ 59 - 74
controller/StaticWebPageController.cpp

@@ -12,85 +12,70 @@
 using namespace std;
 using namespace xc::processor;
 using namespace xc::processor::templates;
-
+using namespace xc::processor::templates::framework7;
 using namespace configor;
 
 namespace xc::controller {
 
     ContentGeneratorDefine({
-                               if (!conf::enableStaticAssetsController) { return false; }
-                               if (request.getMethod() != "GET") { return false; }
-                               if (request.getURL().length() == 0) { return false; }
-                               struct stat buffer;
-                               string filePath = "html";
-                               if (request.getURL()[0] == '/') {
-                                   filePath += request.getURL();
-                               } else {
-                                   filePath += "/" + request.getURL();
-                               }
-                               if (stat(filePath.c_str(), &buffer) == 0) {
-                                   if (S_ISREG(buffer.st_mode)) {
-                                       return true;
-                                   } else {
-                                       for (auto file: conf::defaultFiles) {
-                                           string newFilePath = filePath;
-                                           if (filePath[filePath.length() - 1] == '/') {
-                                               newFilePath += file;
-                                           } else {
-                                               newFilePath += "/" + file;
-                                           }
-                                           if (stat(newFilePath.c_str(), &buffer) == 0) {
-                                               return S_ISREG(buffer.st_mode);
-                                           }
-                                       }
-                                   }
-                               }
-                               return false;
-                           }, {
-                               struct stat buffer;
-                               string filePath = "html";
-                               if (request.getURL()[0] == '/') {
-                                   filePath += request.getURL();
-                               } else {
-                                   filePath += "/" + request.getURL();
-                               }
-                               if (stat(filePath.c_str(), &buffer) == 0) {
-                                   if (S_ISREG(buffer.st_mode)) {
-                                       return (ResponseData *) new BinaryResponseData(200, filePath,
-                                                                                      mimeTypeOfFile(filePath));
-                                   } else {
-                                       for (auto file: conf::defaultFiles) {
-                                           string newFilePath = filePath;
-                                           if (filePath[filePath.length() - 1] == '/') {
-                                               newFilePath += file;
-                                           } else {
-                                               newFilePath += "/" + file;
-                                           }
-                                           if (stat(newFilePath.c_str(), &buffer) == 0) {
-                                               if (S_ISREG(buffer.st_mode)) {
-                                                   return (ResponseData *) new BinaryResponseData(200, newFilePath,
-                                                                                                  mimeTypeOfFile(
-                                                                                                          newFilePath));
-                                               }
-                                           }
-                                       }
-                                   }
-                               }
-
-                               return (ResponseData *) new FileResponseData(conf::errorPage404);
-                           })
-
-    ResponseData *test1(RequestData request) {
-        return new TemplateResponseData({
-            framework7::Framework7Document()
-        });
-    }
-
-    ContentGeneratorDefineS(request.getURL() == "/test1",
-                            test1(request))
-
-    ContentGeneratorDefineS(request.getURL() == "/test2",
-                            new TextResponseData(200, "test2 controller response"))
+        if (!conf::enableStaticAssetsController) { return false; }
+        if (request.getMethod() != "GET") { return false; }
+        if (request.getURL().length() == 0) { return false; }
+        struct stat buffer;
+        string filePath = "html";
+        if (request.getURL()[0] == '/') {
+            filePath += request.getURL();
+        } else {
+            filePath += "/" + request.getURL();
+        }
+        if (stat(filePath.c_str(), &buffer) == 0) {
+            if (S_ISREG(buffer.st_mode)) {
+                return true;
+            } else {
+                for (auto file: conf::defaultFiles) {
+                    string newFilePath = filePath;
+                    if (filePath[filePath.length() - 1] == '/') {
+                        newFilePath += file;
+                    } else {
+                        newFilePath += "/" + file;
+                    }
+                    if (stat(newFilePath.c_str(), &buffer) == 0) {
+                        return S_ISREG(buffer.st_mode);
+                    }
+                }
+            }
+        }
+        return false;
+        }, {
+        struct stat buffer;
+        string filePath = "html";
+        if (request.getURL()[0] == '/') {
+            filePath += request.getURL();
+        } else {
+            filePath += "/" + request.getURL();
+        }
+        if (stat(filePath.c_str(), &buffer) == 0) {
+            if (S_ISREG(buffer.st_mode)) {
+                return (ResponseData *) new BinaryResponseData(200, filePath,
+                                                               mimeTypeOfFile(filePath));
+            } else {
+                for (auto file: conf::defaultFiles) {
+                    string newFilePath = filePath;
+                    if (filePath[filePath.length() - 1] == '/') {
+                        newFilePath += file;
+                    } else {
+                        newFilePath += "/" + file;
+                    }
+                    if (stat(newFilePath.c_str(), &buffer) == 0) {
+                        if (S_ISREG(buffer.st_mode)) {
+                            return (ResponseData *) new BinaryResponseData(200, newFilePath, mimeTypeOfFile(newFilePath));
+                        }
+                    }
+                }
+            }
+        }
+        return (ResponseData *) new FileResponseData(conf::errorPage404);
+    })
 
 }
 

+ 3 - 3
controller/framework7-framework7-bundle.min.css.cpp → controller/auto-generated/framework7-framework7-bundle.min.css.cpp

@@ -2,9 +2,9 @@
 // Created by xcbosa on 2023-01-30
 //
 
-#include "../processor/processor.h"
-#include "../utils/utils.h"
-#include "../webuiconf.h"
+#include "../../processor/processor.h"
+#include "../../utils/utils.h"
+#include "../../webuiconf.h"
 
 using namespace std;
 using namespace xc::processor;

+ 3 - 3
controller/framework7-framework7-bundle.min.js.cpp → controller/auto-generated/framework7-framework7-bundle.min.js.cpp

@@ -2,9 +2,9 @@
 // Created by xcbosa on 2023-01-30
 //
 
-#include "../processor/processor.h"
-#include "../utils/utils.h"
-#include "../webuiconf.h"
+#include "../../processor/processor.h"
+#include "../../utils/utils.h"
+#include "../../webuiconf.h"
 
 using namespace std;
 using namespace xc::processor;

+ 3 - 3
controller/index.css.cpp → controller/auto-generated/index.css.cpp

@@ -2,9 +2,9 @@
 // Created by xcbosa on 2023-01-30
 //
 
-#include "../processor/processor.h"
-#include "../utils/utils.h"
-#include "../webuiconf.h"
+#include "../../processor/processor.h"
+#include "../../utils/utils.h"
+#include "../../webuiconf.h"
 
 using namespace std;
 using namespace xc::processor;

+ 3 - 3
controller/index.js.cpp → controller/auto-generated/index.js.cpp

@@ -2,9 +2,9 @@
 // Created by xcbosa on 2023-01-30
 //
 
-#include "../processor/processor.h"
-#include "../utils/utils.h"
-#include "../webuiconf.h"
+#include "../../processor/processor.h"
+#include "../../utils/utils.h"
+#include "../../webuiconf.h"
 
 using namespace std;
 using namespace xc::processor;

+ 6 - 4
data/Generate-Assets.py

@@ -7,6 +7,8 @@ import sys
 import datetime
 
 if __name__ == "__main__":
+    if not os.path.exists("../controller/auto-generated"):
+        os.mkdir("../controller/auto-generated")
     if len(sys.argv) != 2:
         print("usage: Generate-Assets <AssetFilePath>")
         exit(-1)
@@ -21,16 +23,16 @@ if __name__ == "__main__":
         fileText = " + \n".join(fileTexts)
         if len(fileTexts) == 0:
             fileText = "\"\""
-        writeToFilePath = "../controller/" + assetPath.replace("/", "-") + ".cpp"
+        writeToFilePath = "../controller/auto-generated/" + assetPath.replace("/", "-") + ".cpp"
         print("Writing to " + writeToFilePath)
         with open(writeToFilePath, "w+", encoding="utf-8") as w:
             w.write("//\n")
             w.write("// Created by xcbosa on " + str(datetime.date.today()) + "\n")
             w.write("//\n")
             w.write("\n")
-            w.write("#include \"../processor/processor.h\"\n")
-            w.write("#include \"../utils/utils.h\"\n")
-            w.write("#include \"../webuiconf.h\"\n")
+            w.write("#include \"../../processor/processor.h\"\n")
+            w.write("#include \"../../utils/utils.h\"\n")
+            w.write("#include \"../../webuiconf.h\"\n")
             w.write("\n")
             w.write("using namespace std;\n")
             w.write("using namespace xc::processor;\n")

+ 12 - 3
processor/ContentGenerator.cpp

@@ -23,16 +23,25 @@ namespace xc {
             cout << "[ContentGenerator] Registered " << name << endl;
         }
 
-        bool ContentGenerator::matchRequest(RequestData request) {
+        bool ContentGenerator::matchRequest(RequestData request) const {
             return this->checker(request);
         }
 
-        ResponseData *ContentGenerator::generateResponse(RequestData request) {
+        ResponseData *ContentGenerator::generateResponse(RequestData request) const {
             return this->generator(request);
         }
 
-        string ContentGenerator::getName() {
+        string ContentGenerator::getName() const {
             return this->name;
         }
+
+        const ContentGenerator *findContentGenerator(string name) {
+            for (int i = 0; i < generatorsCnt; i++) {
+                if (generators[i]->getName() == name) {
+                    return generators[i];
+                }
+            }
+            return nullptr;
+        }
     }
 }

+ 8 - 4
processor/ContentGenerator.h

@@ -13,7 +13,9 @@
 #define __string_fic(a) #a
 #define __string_fic_r(a) __string_fic(a)
 
-#define ContentGeneratorDefine(cond, eval) const static ContentGenerator __uniqueVarName(AutoContentGenerator_Line_)(__string_fic_r(__uniqueVarName(AutoRegistered_Line_) __FILE_NAME__), ([] (auto request) { cond; }), ([] (auto request) { eval; }));
+#define ContentGeneratorDefineWithName(name, cond, eval) const static ContentGenerator __uniqueVarName(AutoContentGenerator_Line_)(name, ([] (auto request) { cond; }), ([] (auto request) { eval; }));
+#define ContentGeneratorDefineWithNameS(name, cond, eval) ContentGeneratorDefineWithName(name, return cond, return eval)
+#define ContentGeneratorDefine(cond, eval) ContentGeneratorDefineWithName(__string_fic_r(__uniqueVarName(AutoRegistered_Line_) __FILE_NAME__), cond, eval)
 #define ContentGeneratorDefineS(cond, eval) ContentGeneratorDefine(return cond, return eval)
 
 using namespace std;
@@ -31,14 +33,16 @@ namespace xc {
         class ContentGenerator {
         public:
             ContentGenerator(string name, RequestCheckBlock checker, ContentGenerateBlock generator);
-            bool matchRequest(RequestData request);
-            ResponseData *generateResponse(RequestData request);
-            string getName();
+            bool matchRequest(RequestData request) const;
+            ResponseData *generateResponse(RequestData request) const;
+            string getName() const;
         private:
             string name;
             RequestCheckBlock checker;
             ContentGenerateBlock generator;
         };
 
+        const ContentGenerator *findContentGenerator(string name);
+
     }
 }

+ 33 - 2
processor/templates/ViewTemplatePrototypes.cpp

@@ -3,6 +3,7 @@
 //
 
 #include "ViewTemplatePrototypes.h"
+#include "../ContentGenerator.h"
 
 namespace xc {
     namespace processor {
@@ -20,7 +21,6 @@ namespace xc {
 
             string fixStringFormat(string strToFix) {
                 replace_all(strToFix, "\"", "\\\"");
-                replace_all(strToFix, "\'", "\\\'");
                 replace_all(strToFix, "\r", "\\\r");
                 replace_all(strToFix, "\n", "\\\n");
                 return strToFix;
@@ -42,7 +42,7 @@ namespace xc {
 
             ViewTemplatePrototype::ViewTemplatePrototype(string directHTML): directHTML(directHTML), useDirectHTML(true), keyName(), properties(), styles() { }
 
-            ViewTemplatePrototype::ViewTemplatePrototype(const char *directHTML): directHTML(directHTML), useDirectHTML(true), keyName(), properties(), styles() { }
+            ViewTemplatePrototype::ViewTemplatePrototype(const char *directHTML): directHTML(directHTML == nullptr ? "" : directHTML), useDirectHTML(true), keyName(), properties(), styles() { }
 
             ViewTemplatePrototype::ViewTemplatePrototype(string keyName, string innerText): directHTML(), useDirectHTML(false), keyName(keyName), innerHTML(innerText), properties(), styles() { }
 
@@ -141,18 +141,49 @@ namespace xc {
             ViewTemplatePrototype& ViewTemplatePrototype::dir(string value) { return this->prop("dir", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::draggable(bool value) { return this->prop("draggable", value ? "true" : "false"); }
             ViewTemplatePrototype& ViewTemplatePrototype::hidden(bool value) { return this->prop("hidden", value ? "true" : "false"); }
+            ViewTemplatePrototype& ViewTemplatePrototype::disabled(bool value) { return this->prop("disabled", value ? "true" : "false"); }
             ViewTemplatePrototype& ViewTemplatePrototype::id(string value) { return this->prop("id", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::lang(string value) { return this->prop("lang", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::spellcheck(bool value) { return this->prop("spellcheck", value ? "true" : "false"); }
             ViewTemplatePrototype& ViewTemplatePrototype::title(string value) { return this->prop("title", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::href(string value) { return this->prop("href", value); }
+            ViewTemplatePrototype& ViewTemplatePrototype::onclick(string value) { return this->prop("onclick", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::name(string value) { return this->prop("name", value); }
+            ViewTemplatePrototype& ViewTemplatePrototype::placeholder(string value) { return this->prop("placeholder", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::content(string value) { return this->prop("content", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::rel(string value) { return this->prop("rel", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::type(string value) { return this->prop("type", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::src(string value) { return this->prop("src", value); }
             ViewTemplatePrototype& ViewTemplatePrototype::charset(string value) { return this->prop("charset", value); }
 
+            View ContentGeneratorReference(string name, RequestData request) {
+                const ContentGenerator *generator = processor::findContentGenerator(name);
+                if (generator == nullptr) {
+                    return View("");
+                }
+                ResponseData *resp = generator->generateResponse(request);
+                if (resp == nullptr) {
+                    return View("");
+                }
+                ostringstream oss;
+                resp->writeResponseBodyTo(oss);
+                delete resp;
+                return View(oss.str());
+            }
+
+            If::If(bool conditions, ViewCollection then): View("") {
+                if (conditions) {
+                    this->inner(then);
+                }
+            }
+
+            If::If(bool conditions, ViewCollection then, ViewCollection else_): View("") {
+                if (conditions) {
+                    this->inner(then);
+                } else {
+                    this->inner(else_);
+                }
+            }
 
             Foreach::Foreach(vector<JsonModel> model, function<ViewTemplatePrototype(JsonModel)> generateBlock): ViewTemplatePrototype("") {
                 ViewCollection col;

+ 13 - 1
processor/templates/ViewTemplatePrototypes.h

@@ -31,12 +31,15 @@ namespace xc {
                 ViewTemplatePrototype& dir(string value);
                 ViewTemplatePrototype& draggable(bool value);
                 ViewTemplatePrototype& hidden(bool value);
+                ViewTemplatePrototype& disabled(bool value);
                 ViewTemplatePrototype& id(string value);
                 ViewTemplatePrototype& lang(string value);
                 ViewTemplatePrototype& spellcheck(bool value);
                 ViewTemplatePrototype& title(string value);
                 ViewTemplatePrototype& href(string value);
+                ViewTemplatePrototype& onclick(string value);
                 ViewTemplatePrototype& name(string value);
+                ViewTemplatePrototype& placeholder(string value);
                 ViewTemplatePrototype& content(string value);
                 ViewTemplatePrototype& rel(string value);
                 ViewTemplatePrototype& type(string value);
@@ -55,14 +58,23 @@ namespace xc {
                 void makeSureNotUseDirectHTML();
             };
 
+            typedef ViewTemplatePrototype View;
             typedef vector<ViewTemplatePrototype> ViewCollection;
             typedef json::value JsonModel;
             typedef JsonModel JsonArrayObject;
 
+            View ContentGeneratorReference(string name, RequestData request);
+
             typedef json::object JsonDictDef;
             typedef json::array JsonArrDef;
 
-            class Foreach: public ViewTemplatePrototype {
+            class If: public View {
+            public:
+                If(bool conditions, ViewCollection then);
+                If(bool conditions, ViewCollection then, ViewCollection else_);
+            };
+
+            class Foreach: public View {
             public:
                 Foreach(vector<JsonModel> model, function<ViewTemplatePrototype (JsonModel)> generateBlock);
                 Foreach(vector<JsonModel> model, function<ViewCollection (JsonModel)> generateBlock);

+ 90 - 13
processor/templates/framework7/Framework7Document.hpp

@@ -8,16 +8,16 @@
 #include "../../processor.h"
 
 namespace xc::processor::templates::framework7 {
-    class Framework7Document: public ViewTemplateComponent {
+    class Framework7Document: public View {
     public:
-        Framework7Document(): ViewTemplateComponent({ }) {
+        Framework7Document(ViewCollection pageContent, ViewCollection tabBarContent): View({ }) {
             html html({
                 head({
                     meta().charset("utf-8"),
                     meta().name("viewport").content("width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover"),
                     meta().name("apple-mobile-web-app-capable").content("yes"),
                     meta().name("theme-color").content("#2196f3"),
-                    templates::title("My App"),
+                    templates::title(conf::title),
                     link().rel("stylesheet").href("framework7/framework7-bundle.min.css"),
                     link().rel("stylesheet").href("index.css")
                 }),
@@ -27,20 +27,13 @@ namespace xc::processor::templates::framework7 {
                             div({
                                 div().classAdd("navbar-bg"),
                                 div({
-                                    div("App").classAdd("title")
+                                    div(conf::title).classAdd("title")
                                 }).classAdd("navbar-inner")
                             }).classAdd("navbar"),
                             div({
-                                div({
-                                    a("Link 1").href("#").classAdd("link"),
-                                    a("Link 2").href("#").classAdd("link")
-                                }).classAdd("toolbar-inner")
+                                div(tabBarContent).classAdd("toolbar-inner")
                             }).classAdd("toolbar toolbar-bottom"),
-                            div({
-                                p("Page content goes here"),
-                                a("About app").href("/about/")
-                            }).classAdd("page-content")
-                            
+                            div(pageContent).classAdd("page-content")
                         }).prop("data-name", "home").classAdd("page")
                     }).classAdd("view view-main")
                 }).id("app"),
@@ -50,6 +43,90 @@ namespace xc::processor::templates::framework7 {
             this->inner(html);
         }
     };
+
+    class FormView: public form {
+    public:
+        FormView(ViewCollection items): form() {
+            this->inner(items);
+            this->classAdd("list");
+        }
+    };
+
+    class FormItemView: public ul {
+    public:
+        FormItemView(ViewCollection content): ul(li(content)) { }
+    };
+
+    class FormInputView: public FormItemView {
+    public:
+        FormInputView(string keyName, string displayName, string contentType, string placeholder): FormItemView({
+            div({
+                div({
+                    div(displayName).classAdd("item-title item-label"),
+                    div({
+                        input()
+                            .type(contentType)
+                            .name(keyName)
+                            .placeholder(placeholder)
+                    }).classAdd("item-input-wrap")
+                }).classAdd("item-inner")
+            }).classAdd("item-content item-input")
+        }) { }
+    };
+
+    class BlockView: public div {
+    public:
+        BlockView(ViewCollection content): div(content) {
+            this->classAdd("block block-strong row");
+        }
+    };
+
+    class BlockColumnView: public div {
+    public:
+        BlockColumnView(ViewCollection content): div(content) {
+            this->classAdd("col");
+        }
+    };
+
+    const string ClassButtonViewTypeFill("button-fill");
+    const string ClassButtonViewTypeRound("button-round");
+    const string ClassButtonViewTypeSmall("button-small");
+    const string ClassButtonViewTypeLarge("button-large");
+    const string ClassButtonViewTypeRaised("button-raised");
+    const string ClassButtonViewTypeOutline("button-outline");
+    const string ClassButtonViewTypeActive("button-active");
+    const string ClassButtonViewTypePreloader("button-preloader");
+    const string ClassButtonViewTypeLoading("button-loading");
+    const string ClassColorRed("color-red");
+    const string ClassColorGreen("color-green");
+    const string ClassColorBlue("color-blue");
+    const string ClassColorPink("color-pink");
+    const string ClassColorYellow("color-yellow");
+    const string ClassColorOrange("color-orange");
+    const string ClassColorGray("color-gray");
+    const string ClassColorBlack("color-black");
+    const string ClassColorWhite("color-white");
+
+    class ButtonView: public a {
+    public:
+        ButtonView(string text): a(text) {
+            this->href("#");
+            this->classAdd("button button-fill");
+        }
+    };
+
+    class VerticalSpacer: public div {
+    public:
+        VerticalSpacer(float height): div() {
+            ostringstream oss;
+            oss << height;
+            oss << "px";
+            this->style("height", oss.str());
+        }
+    };
+
+
+
 } // framework7
 
 #endif //FRPCWEBUI_FRAMEWORK7DOCUMENT_H

+ 24 - 0
utils/BinaryResponseData.cpp

@@ -75,6 +75,30 @@ namespace xc {
             ::fflush(fp);
         }
 
+        void BinaryResponseData::writeResponseBodyTo(ostream &fp) const {
+            int mtu = conf::mtu;
+            if (this->isWriteFromMemory()) {
+                for (int i = 0; i < this->bodySize; i++) {
+                    fp.put(this->body[i]);
+                }
+            }
+            if (this->isWriteFromFile()) {
+                string filePath = this->filePath;
+                ::FILE *inputFile = ::fopen(filePath.c_str(), "rb");
+                if (inputFile) {
+                    ::uint8_t buff[mtu];
+                    long readPerPack;
+                    while ((readPerPack = ::fread(buff, 1, mtu, inputFile)) > 0) {
+                        for (int i = 0; i < readPerPack; i++) {
+                            fp.put(buff[i]);
+                        }
+                    }
+                } else {
+                    cerr << "[FileIOError]: " << ::strerror(errno) << endl;
+                }
+            }
+        }
+
         bool BinaryResponseData::isWriteFromFile() const {
             return !this->isWriteFromMemory();
         }

+ 1 - 0
utils/BinaryResponseData.h

@@ -15,6 +15,7 @@ namespace xc {
             BinaryResponseData(int statusCode, uint8_t *body, int bodySize, string contentType);
             BinaryResponseData(int statusCode, string filePath, string contentType);
             void writeTo(::FILE *fp) const;
+            void writeResponseBodyTo(ostream &fp) const;
             void setHeader(string headerName, string value);
             bool isWriteFromFile() const;
             bool isWriteFromMemory() const;

+ 12 - 1
utils/FileReader.cpp

@@ -11,7 +11,7 @@ namespace xc::utils {
     string contentsOfTextFile(string filePath) {
         ifstream fin(filePath);
         if (fin.fail()) {
-            cerr << "[FileIOError]: " << ::strerror(errno) << endl;
+            cerr << "[FileIOError]: Read Error " << ::strerror(errno) << endl;
             return "404";
         }
         stringstream buffer;
@@ -21,6 +21,17 @@ namespace xc::utils {
         return str;
     }
 
+    void saveTextFile(string filePath, string content) {
+        ofstream ofs(filePath);
+        if (ofs.fail()) {
+            cerr << "[FileIOError]: Write Error " << ::strerror(errno) << endl;
+            return;
+        }
+        ofs << content;
+        ofs.flush();
+        ofs.close();
+    }
+
     string mimeTypeOfFile(string filePath) {
         std::filesystem::path p(filePath);
         string ext = p.extension();

+ 181 - 0
utils/INI.cpp

@@ -0,0 +1,181 @@
+//
+// Created by xcbosa on 2023/1/30.
+//
+
+#include "INI.h"
+#include "utils.h"
+
+namespace xc {
+    namespace utils {
+
+        vector<string> split(const string& str, const string& delim) {
+            vector<string> res;
+            if ("" == str) return res;
+            char * strs = new char[str.length() + 1];
+            strcpy(strs, str.c_str());
+            char * d = new char[delim.length() + 1];
+            strcpy(d, delim.c_str());
+            char *p = strtok(strs, d);
+            while(p) {
+                string s = p;
+                res.push_back(s);
+                p = strtok(NULL, d);
+            }
+            return res;
+        }
+
+        string& replace_all(string& src, const string& old_value, const string& new_value) {
+            for (string::size_type pos(0); pos != string::npos; pos += new_value.length()) {
+                if ((pos = src.find(old_value, pos)) != string::npos) {
+                    src.replace(pos, old_value.length(), new_value);
+                }
+                else break;
+            }
+            return src;
+        }
+
+        std::string& trim(std::string &s) {
+            if (s.empty()) { return s; }
+            s.erase(0,s.find_first_not_of(" "));
+            s.erase(s.find_last_not_of(" ") + 1);
+            return s;
+        }
+
+        INISection::INISection(): title(), data() { }
+
+        INISection::INISection(string title, map<string, string> data): title(title), data(data) { }
+
+        string INISection::get(string key) {
+            if (this->has(key)) {
+                return this->data[key];
+            } else {
+                return string();
+            }
+        }
+
+        bool INISection::has(string key) {
+            return this->data.count(key) != 0;
+        }
+
+        void INISection::set(string key, string value) {
+            this->data[key] = value;
+        }
+
+        string INISection::getTitle() {
+            return this->title;
+        }
+
+        void INISection::setTitle(string title) {
+            this->title = title;
+        }
+
+        INI::INI(): data() { }
+
+        INI::INI(string str) {
+            vector<string> lines = split(str, "\n");
+            string sectionName = "";
+            map<string, string> currentSection;
+            for (string it : lines) {
+                string line = it;
+                replace_all(line, "\r", "");
+                if (line.length() == 0) { continue; }
+                if (line[0] == '[') {
+                    if (sectionName.length() > 0 || !currentSection.empty()) {
+                        this->data.push_back(INISection(sectionName, currentSection));
+                        sectionName.clear();
+                        currentSection.clear();
+                    }
+                    ostringstream oss;
+                    for (string::iterator it = line.begin() + 1; it != line.end() && *it != ']'; it++) {
+                        oss << *it;
+                    }
+                    sectionName = oss.str();
+                    continue;
+                }
+                ostringstream left, right;
+                bool isRight = false;
+                for (int i = 0; i < line.length(); i++) {
+                    if (isRight) {
+                        right << line[i];
+                        continue;
+                    }
+                    if (line[i] == '=') {
+                        isRight = true;
+                        continue;
+                    }
+                    left << line[i];
+                }
+                string leftStr = left.str();
+                string rightStr = right.str();
+                trim(leftStr);
+                trim(rightStr);
+                currentSection[leftStr] = rightStr;
+            }
+            if (sectionName.length() > 0 || !currentSection.empty()) {
+                this->data.push_back(INISection(sectionName, currentSection));
+                sectionName.clear();
+                currentSection.clear();
+            }
+        }
+
+        INI::INI(vector<INISection> data): data(data) { }
+
+        INI readFromFile(string filePath) {
+            return INI(contentsOfTextFile(filePath));
+        }
+
+        bool INI::has(string header) {
+            return this->get(header) != nullptr;
+        }
+
+        INISection* INI::get(string header) {
+            for (auto it = this->data.begin(); it != this->data.end(); it++) {
+                if (it->title == header) {
+                    return &*it;
+                }
+            }
+            return nullptr;
+        }
+
+        INISection *INI::getMust(string header) {
+            INISection *section = this->get(header);
+            if (section == nullptr) {
+                this->data.push_back(INISection(header, map<string, string>()));
+                return this->getMust(header);
+            }
+            return section;
+        }
+
+        void INI::remove(string header) {
+            std::remove_if(this->data.begin(), this->data.end(), [&header](auto it) {
+                return it.title == header;
+            });
+        }
+
+        string INI::getINIString() {
+            ostringstream oss;
+            for (INISection section : this->data) {
+                oss << '[';
+                oss << section.title;
+                oss << "]\n";
+                for (auto it : section.data) {
+                    oss << it.first;
+                    oss << " = ";
+                    oss << it.second;
+                    oss << '\n';
+                }
+                oss << '\n';
+            }
+            return oss.str();
+        }
+
+        INIFile::INIFile(string filePath): INI(contentsOfTextFile(filePath)) {
+            this->filePath = filePath;
+        }
+
+        void INIFile::save() {
+            saveTextFile(this->filePath, this->getINIString());
+        }
+
+    } // xc
+} // utils

+ 56 - 0
utils/INI.h

@@ -0,0 +1,56 @@
+//
+// Created by xcbosa on 2023/1/30.
+//
+
+#pragma once
+
+#include "utils-private.h"
+
+namespace xc {
+    namespace utils {
+
+        class INI;
+
+        class INISection {
+            friend class INI;
+        public:
+            INISection();
+            INISection(string title, map<string, string> data);
+
+            bool has(string key);
+            string get(string key);
+            void set(string key, string value);
+            string getTitle();
+            void setTitle(string title);
+        private:
+            string title;
+            map<string, string> data;
+        };
+
+        class INI {
+        public:
+            INI();
+            INI(string string);
+            INI(vector<INISection> data);
+            friend INI readFromFile(string filePath);
+
+            bool has(string header);
+            INISection* get(string header);
+            INISection* getMust(string header);
+            void remove(string header);
+
+            string getINIString();
+        private:
+            vector<INISection> data;
+        };
+
+        class INIFile: public INI {
+        public:
+            INIFile(string filePath);
+            void save();
+        private:
+            string filePath;
+        };
+
+    } // xc
+} // utils

+ 1 - 0
utils/ResponseData.h

@@ -12,6 +12,7 @@ namespace xc {
         class ResponseData {
         public:
             virtual void writeTo(::FILE *fp) const = 0;
+            virtual void writeResponseBodyTo(ostream &fp) const = 0;
         };
 
     } // xc

+ 4 - 0
utils/TextResponseData.cpp

@@ -81,5 +81,9 @@ namespace xc {
             ::fprintf(fp, "0\r\n\r\n");
             ::fflush(fp);
         }
+
+        void TextResponseData::writeResponseBodyTo(ostream &fp) const {
+            fp << this->body;
+        }
     } // xc
 } // utils

+ 1 - 0
utils/TextResponseData.h

@@ -25,6 +25,7 @@ namespace xc {
             void setBody(string body);
             string getBody();
             void writeTo(::FILE *fp) const;
+            void writeResponseBodyTo(ostream &fp) const;
         private:
             int statusCode;
             map<string, string> headers;

+ 2 - 0
utils/utils.h

@@ -9,10 +9,12 @@
 #include "TextResponseData.h"
 #include "FileResponseData.h"
 #include "BinaryResponseData.h"
+#include "INI.h"
 
 using namespace std;
 
 namespace xc::utils {
     string contentsOfTextFile(string filePath);
+    void saveTextFile(string filePath, string content);
     string mimeTypeOfFile(string filePath);
 }

+ 17 - 1
webuiconf.h

@@ -6,6 +6,8 @@
 
 #include <vector>
 #include <map>
+#include <sys/stat.h>
+#include "webui.h"
 #include "utils/utils.h"
 
 using namespace std;
@@ -15,9 +17,23 @@ namespace xc::conf {
     const int clientSocketTimeoutSeconds = 3;
     const int taskProcessTimeoutSeconds = 1;
     const int mtu = 1536;
-
     const bool enableStaticAssetsController = false;
 
+    const string title("Frp-WebUI-XCBOSA");
+
+    const string userPasswordSalt("eDGJ&v,.W0U(66.lVQFsKfWb*bm*M+Lj");
+    const string userJWTSecret("r)xB=P-6A4dpqXLk%03=f+*8TlXDM@%r");
+    const string userDataDir = "data/users";
+
+    inline string getUserDataDir() {
+        struct stat buffer;
+        if (stat(userDataDir.c_str(), &buffer) == 0) {
+            assertx(!S_ISREG(buffer.st_mode), "xc::conf::userDataDir must be configured to a folder, but a file located there.");
+        }
+        mkdir(userDataDir.c_str(), S_IRWXU);
+        return userDataDir;
+    }
+
     const map<string, string> fileExtensionToMimeTypes = {
             { ".html", "text/html" },
             { ".htm", "text/html" },