mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 13:44:24 +00:00
refactor: resolve template assets through manifest helper
This commit is contained in:
parent
e6752e04db
commit
cfb169d2fb
10 changed files with 216 additions and 55 deletions
66
sub/sub.go
66
sub/sub.go
|
|
@ -5,12 +5,15 @@ package sub
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -26,6 +29,8 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type subscriptionAssetManifest map[string]string
|
||||||
|
|
||||||
// setEmbeddedTemplates parses and sets embedded templates on the engine
|
// setEmbeddedTemplates parses and sets embedded templates on the engine
|
||||||
func setEmbeddedTemplates(engine *gin.Engine) error {
|
func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||||
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
|
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
|
||||||
|
|
@ -41,6 +46,50 @@ func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func subscriptionTemplateFuncMap(basePath string, manifest subscriptionAssetManifest) template.FuncMap {
|
||||||
|
i18nWebFunc := func(key string, params ...string) string {
|
||||||
|
return locale.I18n(locale.Web, key, params...)
|
||||||
|
}
|
||||||
|
assetFunc := func(logical string) string {
|
||||||
|
target := logical
|
||||||
|
if manifest != nil {
|
||||||
|
if hashed, ok := manifest[logical]; ok {
|
||||||
|
target = hashed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path.Join(basePath, "assets", target)
|
||||||
|
}
|
||||||
|
return template.FuncMap{
|
||||||
|
"i18n": i18nWebFunc,
|
||||||
|
"asset": assetFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSubscriptionAssetManifest() (subscriptionAssetManifest, error) {
|
||||||
|
if raw, err := os.ReadFile("web/public/assets-manifest.json"); err == nil {
|
||||||
|
return decodeSubscriptionAssetManifest(raw)
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeSubscriptionAssetManifest(webpkg.EmbeddedAssetsManifest())
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeSubscriptionAssetManifest(raw []byte) (subscriptionAssetManifest, error) {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil, fmt.Errorf("subscription asset manifest is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest subscriptionAssetManifest
|
||||||
|
if err := json.Unmarshal(raw, &manifest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(manifest) == 0 {
|
||||||
|
return nil, fmt.Errorf("subscription asset manifest has no entries")
|
||||||
|
}
|
||||||
|
return manifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Server represents the subscription server that serves subscription links and JSON configurations.
|
// Server represents the subscription server that serves subscription links and JSON configurations.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
|
|
@ -181,11 +230,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
// set per-request localizer from headers/cookies
|
// set per-request localizer from headers/cookies
|
||||||
engine.Use(locale.LocalizerMiddleware())
|
engine.Use(locale.LocalizerMiddleware())
|
||||||
|
|
||||||
// register i18n function similar to web server
|
assetManifest, err := loadSubscriptionAssetManifest()
|
||||||
i18nWebFunc := func(key string, params ...string) string {
|
if err != nil {
|
||||||
return locale.I18n(locale.Web, key, params...)
|
return nil, err
|
||||||
}
|
}
|
||||||
engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
|
|
||||||
|
engine.SetFuncMap(subscriptionTemplateFuncMap(basePath, assetManifest))
|
||||||
|
|
||||||
// Templates: prefer embedded; fallback to disk if necessary
|
// Templates: prefer embedded; fallback to disk if necessary
|
||||||
if err := setEmbeddedTemplates(engine); err != nil {
|
if err := setEmbeddedTemplates(engine); err != nil {
|
||||||
|
|
@ -212,10 +262,10 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
|
|
||||||
// Mount assets in multiple paths to handle different URL patterns
|
// Mount assets in multiple paths to handle different URL patterns
|
||||||
var assetsFS http.FileSystem
|
var assetsFS http.FileSystem
|
||||||
if _, err := os.Stat("web/assets"); err == nil {
|
if _, err := os.Stat("web/public/assets"); err == nil {
|
||||||
assetsFS = http.FS(os.DirFS("web/assets"))
|
assetsFS = http.FS(os.DirFS("web/public/assets"))
|
||||||
} else {
|
} else {
|
||||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
if subFS, err := fs.Sub(webpkg.EmbeddedPublicAssets(), "public/assets"); err == nil {
|
||||||
assetsFS = http.FS(subFS)
|
assetsFS = http.FS(subFS)
|
||||||
} else {
|
} else {
|
||||||
logger.Error("sub: failed to mount embedded assets:", err)
|
logger.Error("sub: failed to mount embedded assets:", err)
|
||||||
|
|
@ -277,7 +327,7 @@ func (s *Server) getHtmlFiles() ([]string, error) {
|
||||||
files = append(files, theme)
|
files = append(files, theme)
|
||||||
}
|
}
|
||||||
// page itself
|
// page itself
|
||||||
page := filepath.Join(dir, "web", "html", "subpage.html")
|
page := filepath.Join(dir, "web", "html", "settings", "panel", "subscription", "subpage.html")
|
||||||
if _, err := os.Stat(page); err == nil {
|
if _, err := os.Stat(page); err == nil {
|
||||||
files = append(files, page)
|
files = append(files, page)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
89
sub/sub_test.go
Normal file
89
sub/sub_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubscriptionTemplatesUseAssetHelper(t *testing.T) {
|
||||||
|
engine := gin.New()
|
||||||
|
engine.SetFuncMap(subscriptionTemplateFuncMap("/sub/", subscriptionAssetManifest{
|
||||||
|
"moment/moment.min.js": "moment/moment.min.12345678.js",
|
||||||
|
}))
|
||||||
|
|
||||||
|
if err := setEmbeddedTemplates(engine); err != nil {
|
||||||
|
t.Fatalf("setEmbeddedTemplates() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
rendered := engine.HTMLRender.Instance("subpage.html", gin.H{
|
||||||
|
"title": "subscription.title",
|
||||||
|
"host": "example.com",
|
||||||
|
"base_path": "/sub/test-subid/",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := rendered.Render(recorder); err != nil {
|
||||||
|
t.Fatalf("rendered.Render() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := recorder.Body.String()
|
||||||
|
if !strings.Contains(body, `/sub/assets/moment/moment.min.12345678.js`) {
|
||||||
|
t.Fatalf("rendered body missing subscription asset path: %s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHtmlFilesReturnsCurrentSubscriptionTemplatePath(t *testing.T) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("os.Getwd() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
writeTestFile(t, filepath.Join(tempDir, "web", "html", "common", "page.html"))
|
||||||
|
writeTestFile(t, filepath.Join(tempDir, "web", "html", "component", "aThemeSwitch.html"))
|
||||||
|
currentTemplatePath := filepath.Join(tempDir, "web", "html", "settings", "panel", "subscription", "subpage.html")
|
||||||
|
writeTestFile(t, currentTemplatePath)
|
||||||
|
|
||||||
|
if err := os.Chdir(tempDir); err != nil {
|
||||||
|
t.Fatalf("os.Chdir() error = %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := os.Chdir(wd); err != nil {
|
||||||
|
t.Fatalf("restore working directory: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server := NewServer()
|
||||||
|
files, err := server.getHtmlFiles()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getHtmlFiles() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !containsPath(files, currentTemplatePath) {
|
||||||
|
t.Fatalf("getHtmlFiles() missing current subscription template path %q in %v", currentTemplatePath, files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTestFile(t *testing.T, path string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatalf("os.MkdirAll(%q) error = %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
|
||||||
|
t.Fatalf("os.WriteFile(%q) error = %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsPath(paths []string, want string) bool {
|
||||||
|
for _, path := range paths {
|
||||||
|
if path == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,18 @@ func TestAssetResolverReturnsLogicalPathInDebug(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAssetResolverPreservesBasePathWithoutDoubleSlash(t *testing.T) {
|
||||||
|
resolver := newAssetResolver("/xui/", false, assetManifest{
|
||||||
|
"css/custom.min.css": "css/custom.min.11111111.css",
|
||||||
|
})
|
||||||
|
|
||||||
|
got := resolver.URL("css/custom.min.css")
|
||||||
|
want := "/xui/assets/css/custom.min.11111111.css"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("expected %q, got %q", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAssetResolverPanicsOnMissingProductionAsset(t *testing.T) {
|
func TestAssetResolverPanicsOnMissingProductionAsset(t *testing.T) {
|
||||||
resolver := newAssetResolver("/", false, assetManifest{})
|
resolver := newAssetResolver("/", false, assetManifest{})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
<meta name="renderer" content="webkit">
|
<meta name="renderer" content="webkit">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="robots" content="noindex,nofollow">
|
<meta name="robots" content="noindex,nofollow">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue/antd.min.css">
|
<link rel="stylesheet" href="{{ asset "ant-design-vue/antd.min.css" }}">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}">
|
<link rel="stylesheet" href="{{ asset "css/custom.min.css" }}">
|
||||||
<style>
|
<style>
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
font-family: 'Vazirmatn';
|
font-family: 'Vazirmatn';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
|
src: url('{{ asset "Vazirmatn-UI-NL-Regular.woff2" }}') format('woff2');
|
||||||
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
|
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
|
|
@ -72,18 +72,18 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ define "page/body_scripts" }}
|
{{ define "page/body_scripts" }}
|
||||||
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "vue/vue.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
|
<script src="{{ asset "moment/moment.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
<script src="{{ asset "ant-design-vue/antd.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "axios/axios.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
|
<script src="{{ asset "qs/qs.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "js/axios-init.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "js/util/index.js" }}"></script>
|
||||||
<script>
|
<script>
|
||||||
const basePath = '{{ .base_path }}';
|
const basePath = '{{ .base_path }}';
|
||||||
axios.defaults.baseURL = basePath;
|
axios.defaults.baseURL = basePath;
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "js/websocket.js" }}"></script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ define "page/body_end" }}
|
{{ define "page/body_end" }}
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "component/aPersianDatepicker"}}
|
{{define "component/aPersianDatepicker"}}
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.css?{{ .cur_ver }}" />
|
<link rel="stylesheet" href="{{ asset "persian-datepicker/persian-datepicker.min.css" }}" />
|
||||||
<script src="{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "moment/moment-jalali.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "persian-datepicker/persian-datepicker.min.js" }}"></script>
|
||||||
<script>
|
<script>
|
||||||
const persianDatepicker = {};
|
const persianDatepicker = {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -601,11 +601,11 @@
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
{{template "page/body_scripts" .}}
|
{{template "page/body_scripts" .}}
|
||||||
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "qrcode/qrious2.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "uri/URI.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "js/model/reality_targets.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "js/model/inbound.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "js/model/dbinbound.js" }}"></script>
|
||||||
{{template "component/aSidebar" .}}
|
{{template "component/aSidebar" .}}
|
||||||
{{template "component/aThemeSwitch" .}}
|
{{template "component/aThemeSwitch" .}}
|
||||||
{{template "component/aCustomStatistic" .}}
|
{{template "component/aCustomStatistic" .}}
|
||||||
|
|
|
||||||
|
|
@ -108,9 +108,9 @@
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
{{template "page/body_scripts" .}}
|
{{template "page/body_scripts" .}}
|
||||||
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "qrcode/qrious2.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/otpauth/otpauth.umd.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "otpauth/otpauth.umd.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "js/model/setting.js" }}"></script>
|
||||||
{{template "component/aSidebar" .}}
|
{{template "component/aSidebar" .}}
|
||||||
{{template "component/aThemeSwitch" .}}
|
{{template "component/aThemeSwitch" .}}
|
||||||
{{template "component/aSettingListItem" .}}
|
{{template "component/aSettingListItem" .}}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{{ template "page/head_start" .}}
|
{{ template "page/head_start" .}}
|
||||||
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
|
<script src="{{ asset "moment/moment.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "moment/moment-jalali.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "vue/vue.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
<script src="{{ asset "ant-design-vue/antd.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "js/util/index.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "qrcode/qrious2.min.js" }}"></script>
|
||||||
<style>
|
<style>
|
||||||
.subscription-page .subscription-link-box {
|
.subscription-page .subscription-link-box {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -251,6 +251,6 @@
|
||||||
{{ end }}</textarea>
|
{{ end }}</textarea>
|
||||||
|
|
||||||
{{template "component/aThemeSwitch" .}}
|
{{template "component/aThemeSwitch" .}}
|
||||||
<script src="{{ .base_path }}assets/js/subscription.js?{{ .cur_ver }}"></script>
|
<script src="{{ asset "js/subscription.js" }}"></script>
|
||||||
|
|
||||||
{{ template "page/body_end" .}}
|
{{ template "page/body_end" .}}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{{ template "page/head_start" .}}
|
{{ template "page/head_start" .}}
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
|
href="{{ asset "codemirror/codemirror.min.css" }}">
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
href="{{ asset "codemirror/fold/foldgutter.css" }}">
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
|
href="{{ asset "codemirror/xq.min.css" }}">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
<link rel="stylesheet" href="{{ asset "codemirror/lint/lint.css" }}">
|
||||||
{{ template "page/head_end" .}}
|
{{ template "page/head_end" .}}
|
||||||
|
|
||||||
{{ template "page/body_start" .}}
|
{{ template "page/body_start" .}}
|
||||||
|
|
@ -149,20 +149,20 @@
|
||||||
</a-layout>
|
</a-layout>
|
||||||
{{template "page/body_scripts" .}}
|
{{template "page/body_scripts" .}}
|
||||||
<script
|
<script
|
||||||
src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
|
src="{{ asset "js/model/outbound.js" }}"></script>
|
||||||
<script
|
<script
|
||||||
src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
|
src="{{ asset "codemirror/codemirror.min.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
|
<script src="{{ asset "codemirror/javascript.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
|
<script src="{{ asset "codemirror/jshint.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
|
<script src="{{ asset "codemirror/jsonlint.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
|
<script src="{{ asset "codemirror/lint/lint.js" }}"></script>
|
||||||
<script
|
<script
|
||||||
src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
|
src="{{ asset "codemirror/lint/javascript-lint.js" }}"></script>
|
||||||
<script
|
<script
|
||||||
src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
|
src="{{ asset "codemirror/hint/javascript-hint.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
|
<script src="{{ asset "codemirror/fold/foldcode.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
|
<script src="{{ asset "codemirror/fold/foldgutter.js" }}"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
|
<script src="{{ asset "codemirror/fold/brace-fold.js" }}"></script>
|
||||||
{{template "component/aSidebar" .}}
|
{{template "component/aSidebar" .}}
|
||||||
{{template "component/aThemeSwitch" .}}
|
{{template "component/aThemeSwitch" .}}
|
||||||
{{template "component/aTableSortable" .}}
|
{{template "component/aTableSortable" .}}
|
||||||
|
|
|
||||||
10
web/web.go
10
web/web.go
|
|
@ -111,6 +111,16 @@ func EmbeddedAssets() embed.FS {
|
||||||
return assetsFS
|
return assetsFS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EmbeddedPublicAssets returns the embedded fingerprinted assets filesystem for reuse by other servers.
|
||||||
|
func EmbeddedPublicAssets() embed.FS {
|
||||||
|
return publicAssetsFS
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedAssetsManifest returns the embedded fingerprinted asset manifest bytes.
|
||||||
|
func EmbeddedAssetsManifest() []byte {
|
||||||
|
return append([]byte(nil), assetsManifestRaw...)
|
||||||
|
}
|
||||||
|
|
||||||
// Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs.
|
// Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue