A brand new welcome page (installer)

This commit is contained in:
2025-11-25 15:16:57 +08:00
parent 3c6f8714d2
commit 630d1d2f07
18 changed files with 332 additions and 20 deletions

View File

@@ -20,7 +20,8 @@ func CheckInstall(config *service.Config) {
router.Handle("/font-awesome/", staticRouter)
router.Handle("/js/", staticRouter)
router.Handle("/img/", staticRouter)
server.Handler = router
router.Handle("/.well-known/", RouteWebDevTools())
server.Handler = InjectWebServerHeaders(config, router)
service.ListenWebService(config, server)
return
}
@@ -32,6 +33,15 @@ func InstallWebHandler(w http.ResponseWriter, r *http.Request) {
ErrorHandler(w, r, http.StatusNotFound)
return
}
data := installPageData{
StatusCode: 200,
}
loadInstallTemplate()
w.WriteHeader(200)
SetHeaders(w, "text/html; charset=utf-8")
if err := installTemplate.Execute(w, data); err != nil {
log.Printf("[Error] Failed to render install page: %v", err)
}
}
func GetInstallLock(config *service.Config) bool {

View File

@@ -1 +1,22 @@
package handler
import (
"fmt"
"net/http"
"github.com/OmniX-Space/MeowBox-Core/internal/service"
)
func InjectWebServerHeaders(config *service.Config, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.Server.ShowServerVersion {
version := service.GetVersion()
serverHeader := fmt.Sprintf("MeowBox/%s", version)
w.Header().Set("Server", serverHeader)
next.ServeHTTP(w, r)
return
}
w.Header().Set("Server", "MeowBox")
next.ServeHTTP(w, r)
})
}

View File

@@ -2,6 +2,14 @@ package handler
import "net/http"
func RouteWebDevTools() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/.well-known/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
return mux
}
func RouteStaticFiles() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/favicon.ico", StaticFileHandler)

View File

@@ -14,6 +14,9 @@ import (
//go:embed web
var webFiles embed.FS
//go:embed web/install.html
var installTemplateContent string
//go:embed web/index.html
var pageTemplateContent string
@@ -21,12 +24,25 @@ var pageTemplateContent string
var errorTemplateContent string
var (
pageTemplate *template.Template
errorTemplate *template.Template
errorOnce sync.Once
pageOnce sync.Once
installTemplate *template.Template
pageTemplate *template.Template
errorTemplate *template.Template
installOnce sync.Once
pageOnce sync.Once
errorOnce sync.Once
)
// loadInstallTemplate Initialize template (executed only once)
func loadInstallTemplate() {
installOnce.Do(func() {
var err error
installTemplate, err = template.New("install").Parse(installTemplateContent)
if err != nil {
log.Fatalf("[Error] Failed to parse install template: %v", err)
}
})
}
// loadPageTemplate Initialize template (executed only once)
func loadPageTemplate() {
pageOnce.Do(func() {
@@ -78,5 +94,4 @@ func StaticFileHandler(w http.ResponseWriter, r *http.Request) {
func SetHeaders(w http.ResponseWriter, contentType string) {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Server", "CloudCat-Project")
}

View File

@@ -14,6 +14,11 @@ type errorPageData struct {
Message string
}
// InstallPageData Data model for install page template
type installPageData struct {
StatusCode int
}
// IndexPageData Data model for index page template
type indexPageData struct {
StatusCode int

View File

@@ -0,0 +1,63 @@
.welcome {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
flex-direction: column-reverse;
}
.welcome-button {
position: absolute;
bottom: 64px;
width: 150px;
backdrop-filter: blur(3px);
border: 0;
border-radius: 20px;
background-color: rgba(255, 255, 255, 0.2);
color: white;
padding: 8px;
font-size: 24px;
cursor: pointer;
}
.welcome h1 {
font-weight: bold;
font-size: 64px;
color: white;
font-family: 'Comic Sans MS', cursive, sans-serif;
margin: 0;
opacity: 0;
}
@keyframes fadeInOut {
0% {
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.welcome-animation {
animation: fadeInOut 3s forwards;
}
.container {
width: 80%;
height: calc(100% - 100px);
border-radius: 20px;
backdrop-filter: blur(3px);
background-color: rgba(255, 255, 255, 0.2);
margin: 50px auto;
}

View File

@@ -7,8 +7,9 @@ body {
}
/* Main styles */
.container {
#page {
height: 100vh;
background: url('/img/background.webp') no-repeat center center fixed;
background-size: cover;
overflow: hidden;
}

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body>
<script src="/js/httperr.js"></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body>
<script src="/js/index.js"></script>
</body>
</html>

View File

@@ -5,8 +5,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/font-awesome/css/all.min.css">
</head>
<body>

View File

@@ -0,0 +1,19 @@
/*
* i18n.js
* MeowBox-Core
* Created by MoeCinnamo on 2025/11/24.
*
* This file contains the i18n strings for the web interface.
*/
const en = {
"hello": "Hello",
};
const zhCN = {
"hello": "你好",
};
const jp = {
"hello": "こんにちは",
};
const languages = [en, zhCN, jp];

View File

@@ -1,9 +1,75 @@
function install() {
document.title = "安装程序 - MeowBox";
const container = document.createElement('div');
container.classList.add('container');
document.body.appendChild(container);
function welcome() {
document.title = "Welcome - MeowBox";
const page = document.getElementById("page");
let currentIndex = 0;
const welcomeDiv = document.createElement("div");
welcomeDiv.classList.add("welcome");
const welcomeButton = document.createElement("button");
welcomeButton.classList.add("welcome-button");
welcomeButton.innerHTML = 'Next <i class="fa-solid fa-circle-arrow-right"></i>';
welcomeDiv.appendChild(welcomeButton);
function displayNextLanguage() {
if (currentIndex >= languages.length) {
currentIndex = 0;
}
const welcomeTitle = document.createElement("h1");
welcomeTitle.classList.add("welcome-animation")
welcomeTitle.textContent = languages[currentIndex]["hello"];
welcomeDiv.appendChild(welcomeTitle);
setTimeout(() => {
welcomeDiv.removeChild(welcomeTitle);
currentIndex++;
displayNextLanguage();
}, 3000);
}
displayNextLanguage();
page.appendChild(welcomeDiv);
}
function install() {
}
// Load external script
function loadScript(scriptUrl) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = scriptUrl;
script.async = true;
script.onload = () => {
resolve();
script.remove();
};
script.onerror = () => {
const errorMsg = `Failed to load script: ${scriptUrl}`;
console.error(`[ScriptLoader] ${errorMsg}`);
reject(new Error(errorMsg));
};
document.body.appendChild(script);
});
}
// Initialize app
async function initializeApp() {
try {
await Promise.all([
loadScript("/js/layout.js")
]);
createPage();
await Promise.all([
loadScript("/js/i18n.js"),
loadCSSAsPromise("/css/layout.css"),
loadCSSAsPromise("/css/install.css"),
loadCSSAsPromise("/font-awesome/css/all.min.css")
])
welcome();
} catch (error) {
console.error('App initialization error:', error);
}
}
document.addEventListener('DOMContentLoaded', () => {
install();
initializeApp();
});

View File

@@ -0,0 +1,75 @@
function loadCSS(url, callback, errorCallback, attributes = {}) {
const existingLinks = document.querySelectorAll('link[rel="stylesheet"]');
for (let i = 0; i < existingLinks.length; i++) {
if (existingLinks[i].href === url || existingLinks[i].href.endsWith(url)) {
if (typeof callback === 'function') {
callback();
}
return existingLinks[i];
}
}
const link = document.createElement('link');
link.rel = attributes.rel || 'stylesheet';
link.type = attributes.type || 'text/css';
link.href = url;
if (attributes.media) link.media = attributes.media;
if (attributes.id) link.id = attributes.id;
if (attributes.crossOrigin) link.crossOrigin = attributes.crossOrigin;
link.onload = function () {
if (typeof callback === 'function') {
callback();
}
};
link.onerror = function (err) {
if (typeof errorCallback === 'function') {
errorCallback(err);
} else {
console.error(`Failed to load CSS: ${url}`, err);
}
};
const head = document.head || document.getElementsByTagName('head')[0];
head.appendChild(link);
return link;
}
function loadCSSAsPromise(url, attributes = {}) {
return new Promise((resolve, reject) => {
loadCSS(
url,
resolve,
reject,
attributes
);
});
}
function loadMD5Script() {
return new Promise((resolve, reject) => {
if (typeof md5 !== 'undefined') {
resolve(); // MD5 library already loaded
return;
}
const script = document.createElement('script');
script.src = '/js/md5.min.js';
script.async = true;
script.onload = () => resolve();
script.onerror = () => {
console.error('Failed to load MD5 library');
reject(new Error('MD5 library load failed'));
};
document.head.appendChild(script);
});
}
function createPage() {
const main = document.createElement('div');
main.id = "page";
document.body.appendChild(main);
}

1
internal/handler/web/js/md5.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function (n) { "use strict"; function d(n, t) { var r = (65535 & n) + (65535 & t); return (n >> 16) + (t >> 16) + (r >> 16) << 16 | 65535 & r } function f(n, t, r, e, o, u) { return d((u = d(d(t, n), d(e, u))) << o | u >>> 32 - o, r) } function l(n, t, r, e, o, u, c) { return f(t & r | ~t & e, n, t, o, u, c) } function g(n, t, r, e, o, u, c) { return f(t & e | r & ~e, n, t, o, u, c) } function v(n, t, r, e, o, u, c) { return f(t ^ r ^ e, n, t, o, u, c) } function m(n, t, r, e, o, u, c) { return f(r ^ (t | ~e), n, t, o, u, c) } function c(n, t) { var r, e, o, u; n[t >> 5] |= 128 << t % 32, n[14 + (t + 64 >>> 9 << 4)] = t; for (var c = 1732584193, f = -271733879, i = -1732584194, a = 271733878, h = 0; h < n.length; h += 16)c = l(r = c, e = f, o = i, u = a, n[h], 7, -680876936), a = l(a, c, f, i, n[h + 1], 12, -389564586), i = l(i, a, c, f, n[h + 2], 17, 606105819), f = l(f, i, a, c, n[h + 3], 22, -1044525330), c = l(c, f, i, a, n[h + 4], 7, -176418897), a = l(a, c, f, i, n[h + 5], 12, 1200080426), i = l(i, a, c, f, n[h + 6], 17, -1473231341), f = l(f, i, a, c, n[h + 7], 22, -45705983), c = l(c, f, i, a, n[h + 8], 7, 1770035416), a = l(a, c, f, i, n[h + 9], 12, -1958414417), i = l(i, a, c, f, n[h + 10], 17, -42063), f = l(f, i, a, c, n[h + 11], 22, -1990404162), c = l(c, f, i, a, n[h + 12], 7, 1804603682), a = l(a, c, f, i, n[h + 13], 12, -40341101), i = l(i, a, c, f, n[h + 14], 17, -1502002290), c = g(c, f = l(f, i, a, c, n[h + 15], 22, 1236535329), i, a, n[h + 1], 5, -165796510), a = g(a, c, f, i, n[h + 6], 9, -1069501632), i = g(i, a, c, f, n[h + 11], 14, 643717713), f = g(f, i, a, c, n[h], 20, -373897302), c = g(c, f, i, a, n[h + 5], 5, -701558691), a = g(a, c, f, i, n[h + 10], 9, 38016083), i = g(i, a, c, f, n[h + 15], 14, -660478335), f = g(f, i, a, c, n[h + 4], 20, -405537848), c = g(c, f, i, a, n[h + 9], 5, 568446438), a = g(a, c, f, i, n[h + 14], 9, -1019803690), i = g(i, a, c, f, n[h + 3], 14, -187363961), f = g(f, i, a, c, n[h + 8], 20, 1163531501), c = g(c, f, i, a, n[h + 13], 5, -1444681467), a = g(a, c, f, i, n[h + 2], 9, -51403784), i = g(i, a, c, f, n[h + 7], 14, 1735328473), c = v(c, f = g(f, i, a, c, n[h + 12], 20, -1926607734), i, a, n[h + 5], 4, -378558), a = v(a, c, f, i, n[h + 8], 11, -2022574463), i = v(i, a, c, f, n[h + 11], 16, 1839030562), f = v(f, i, a, c, n[h + 14], 23, -35309556), c = v(c, f, i, a, n[h + 1], 4, -1530992060), a = v(a, c, f, i, n[h + 4], 11, 1272893353), i = v(i, a, c, f, n[h + 7], 16, -155497632), f = v(f, i, a, c, n[h + 10], 23, -1094730640), c = v(c, f, i, a, n[h + 13], 4, 681279174), a = v(a, c, f, i, n[h], 11, -358537222), i = v(i, a, c, f, n[h + 3], 16, -722521979), f = v(f, i, a, c, n[h + 6], 23, 76029189), c = v(c, f, i, a, n[h + 9], 4, -640364487), a = v(a, c, f, i, n[h + 12], 11, -421815835), i = v(i, a, c, f, n[h + 15], 16, 530742520), c = m(c, f = v(f, i, a, c, n[h + 2], 23, -995338651), i, a, n[h], 6, -198630844), a = m(a, c, f, i, n[h + 7], 10, 1126891415), i = m(i, a, c, f, n[h + 14], 15, -1416354905), f = m(f, i, a, c, n[h + 5], 21, -57434055), c = m(c, f, i, a, n[h + 12], 6, 1700485571), a = m(a, c, f, i, n[h + 3], 10, -1894986606), i = m(i, a, c, f, n[h + 10], 15, -1051523), f = m(f, i, a, c, n[h + 1], 21, -2054922799), c = m(c, f, i, a, n[h + 8], 6, 1873313359), a = m(a, c, f, i, n[h + 15], 10, -30611744), i = m(i, a, c, f, n[h + 6], 15, -1560198380), f = m(f, i, a, c, n[h + 13], 21, 1309151649), c = m(c, f, i, a, n[h + 4], 6, -145523070), a = m(a, c, f, i, n[h + 11], 10, -1120210379), i = m(i, a, c, f, n[h + 2], 15, 718787259), f = m(f, i, a, c, n[h + 9], 21, -343485551), c = d(c, r), f = d(f, e), i = d(i, o), a = d(a, u); return [c, f, i, a] } function i(n) { for (var t = "", r = 32 * n.length, e = 0; e < r; e += 8)t += String.fromCharCode(n[e >> 5] >>> e % 32 & 255); return t } function a(n) { var t = []; for (t[(n.length >> 2) - 1] = void 0, e = 0; e < t.length; e += 1)t[e] = 0; for (var r = 8 * n.length, e = 0; e < r; e += 8)t[e >> 5] |= (255 & n.charCodeAt(e / 8)) << e % 32; return t } function e(n) { for (var t, r = "0123456789abcdef", e = "", o = 0; o < n.length; o += 1)t = n.charCodeAt(o), e += r.charAt(t >>> 4 & 15) + r.charAt(15 & t); return e } function r(n) { return unescape(encodeURIComponent(n)) } function o(n) { return i(c(a(n = r(n)), 8 * n.length)) } function u(n, t) { return function (n, t) { var r, e = a(n), o = [], u = []; for (o[15] = u[15] = void 0, 16 < e.length && (e = c(e, 8 * n.length)), r = 0; r < 16; r += 1)o[r] = 909522486 ^ e[r], u[r] = 1549556828 ^ e[r]; return t = c(o.concat(a(t)), 512 + 8 * t.length), i(c(u.concat(t), 640)) }(r(n), r(t)) } function t(n, t, r) { return t ? r ? u(t, n) : e(u(t, n)) : r ? o(n) : e(o(n)) } "function" == typeof define && define.amd ? define(function () { return t }) : "object" == typeof module && module.exports ? module.exports = t : n.md5 = t }(this);

View File

@@ -3,9 +3,10 @@ package service
// Config represents the full configuration structure
type Config struct {
Server struct {
Host string `json:"host"`
Port int `json:"port"`
Tls struct {
Host string `json:"host"`
Port int `json:"port"`
ShowServerVersion bool `json:"show_server_version"`
Tls struct {
Enabled bool `json:"enabled"`
Cert string `json:"cert_file"`
Key string `json:"key_file"`

View File

@@ -1,5 +1,5 @@
package service
func GetVersion() string {
return "0.1.0 dev 2025-11-22"
return "v0.1.0 dev 2025-11-24"
}