mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/0.7.0/seo' into test
This commit is contained in:
commit
04f048b829
22
docs/docs.go
22
docs/docs.go
|
@ -4865,6 +4865,26 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/custom.css": {
|
||||
"get": {
|
||||
"description": "get site robots information",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"site"
|
||||
],
|
||||
"summary": "get site robots information",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/installation/base-info": {
|
||||
"post": {
|
||||
"description": "init base info",
|
||||
|
@ -6944,7 +6964,7 @@ const docTemplate = `{
|
|||
"login": {
|
||||
"$ref": "#/definitions/schema.SiteLoginResp"
|
||||
},
|
||||
"siteseo": {
|
||||
"site_seo": {
|
||||
"$ref": "#/definitions/schema.SiteSeoReq"
|
||||
},
|
||||
"theme": {
|
||||
|
|
|
@ -4853,6 +4853,26 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/custom.css": {
|
||||
"get": {
|
||||
"description": "get site robots information",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"site"
|
||||
],
|
||||
"summary": "get site robots information",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/installation/base-info": {
|
||||
"post": {
|
||||
"description": "init base info",
|
||||
|
@ -6932,7 +6952,7 @@
|
|||
"login": {
|
||||
"$ref": "#/definitions/schema.SiteLoginResp"
|
||||
},
|
||||
"siteseo": {
|
||||
"site_seo": {
|
||||
"$ref": "#/definitions/schema.SiteSeoReq"
|
||||
},
|
||||
"theme": {
|
||||
|
|
|
@ -1292,7 +1292,7 @@ definitions:
|
|||
$ref: '#/definitions/schema.SiteInterfaceResp'
|
||||
login:
|
||||
$ref: '#/definitions/schema.SiteLoginResp'
|
||||
siteseo:
|
||||
site_seo:
|
||||
$ref: '#/definitions/schema.SiteSeoReq'
|
||||
theme:
|
||||
$ref: '#/definitions/schema.SiteThemeResp'
|
||||
|
@ -4814,6 +4814,19 @@ paths:
|
|||
summary: vote up
|
||||
tags:
|
||||
- Activity
|
||||
/custom.css:
|
||||
get:
|
||||
description: get site robots information
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
summary: get site robots information
|
||||
tags:
|
||||
- site
|
||||
/installation/base-info:
|
||||
post:
|
||||
consumes:
|
||||
|
|
|
@ -3,25 +3,13 @@ package server
|
|||
import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/day"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
|
||||
brotli "github.com/anargu/gin-brotli"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/router"
|
||||
"github.com/answerdev/answer/ui"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
)
|
||||
|
||||
// NewHTTPServer new http server.
|
||||
|
@ -44,6 +32,16 @@ func NewHTTPServer(debug bool,
|
|||
r.Use(brotli.Brotli(brotli.DefaultCompression), middleware.ExtractAndSetAcceptLanguage)
|
||||
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
|
||||
|
||||
dev := os.Getenv("DEVCODE")
|
||||
if dev != "" {
|
||||
r.SetFuncMap(funcMap)
|
||||
r.LoadHTMLGlob("../../ui/template/*")
|
||||
} else {
|
||||
html, _ := fs.Sub(ui.Template, "template")
|
||||
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
|
||||
r.SetHTMLTemplate(htmlTemplate)
|
||||
}
|
||||
|
||||
viewRouter.Register(r)
|
||||
|
||||
rootGroup := r.Group("")
|
||||
|
@ -70,116 +68,6 @@ func NewHTTPServer(debug bool,
|
|||
cmsauthV1.Use(authUserMiddleware.CmsAuth())
|
||||
answerRouter.RegisterAnswerCmsAPIRouter(cmsauthV1)
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"replaceHTMLTag": func(src string, tags ...string) string {
|
||||
p := `(?U)<(\d+)>.+</(\d+)>`
|
||||
|
||||
re := regexp.MustCompile(p)
|
||||
ms := re.FindAllStringSubmatch(src, -1)
|
||||
for _, mi := range ms {
|
||||
if mi[1] == mi[2] {
|
||||
i, err := strconv.Atoi(mi[1])
|
||||
if err != nil || len(tags) < i {
|
||||
break
|
||||
}
|
||||
|
||||
src = strings.ReplaceAll(src, mi[0], tags[i-1])
|
||||
}
|
||||
}
|
||||
|
||||
return src
|
||||
},
|
||||
"join": func(sep string, elems ...string) string {
|
||||
return strings.Join(elems, sep)
|
||||
},
|
||||
"templateHTML": func(data string) template.HTML {
|
||||
return template.HTML(data)
|
||||
},
|
||||
"translator": func(la i18n.Language, data string, params ...interface{}) string {
|
||||
trans := translator.GlobalTrans.Tr(la, data)
|
||||
|
||||
if len(params) > 0 && len(params)%2 == 0 {
|
||||
for i := 0; i < len(params); i += 2 {
|
||||
k := converter.InterfaceToString(params[i])
|
||||
v := converter.InterfaceToString(params[i+1])
|
||||
trans = strings.ReplaceAll(trans, "{{ "+k+" }}", v)
|
||||
}
|
||||
}
|
||||
|
||||
return trans
|
||||
},
|
||||
"timeFormatISO": func(tz string, timestamp int64) string {
|
||||
_, _ = time.LoadLocation(tz)
|
||||
return time.Unix(timestamp, 0).Format("2006-01-02T15:04:05.000Z")
|
||||
},
|
||||
"translatorTimeFormatLongDate": func(la i18n.Language, tz string, timestamp int64) string {
|
||||
trans := translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_time")
|
||||
return day.Format(timestamp, trans, tz)
|
||||
},
|
||||
"translatorTimeFormat": func(la i18n.Language, tz string, timestamp int64) string {
|
||||
var (
|
||||
now = time.Now().Unix()
|
||||
between int64 = 0
|
||||
trans string
|
||||
)
|
||||
_, _ = time.LoadLocation(tz)
|
||||
if now > timestamp {
|
||||
between = now - timestamp
|
||||
}
|
||||
|
||||
if between <= 1 {
|
||||
return translator.GlobalTrans.Tr(la, "ui.dates.now")
|
||||
}
|
||||
|
||||
if between > 1 && between < 60 {
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_seconds_ago")
|
||||
return strings.ReplaceAll(trans, "{{count}}", converter.IntToString(between))
|
||||
}
|
||||
|
||||
if between >= 60 && between < 3600 {
|
||||
min := math.Floor(float64(between / 60))
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_minutes_ago")
|
||||
return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(min, 'f', 0, 64))
|
||||
}
|
||||
|
||||
if between >= 3600 && between < 3600*24 {
|
||||
h := math.Floor(float64(between / 3600))
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_hours_ago")
|
||||
return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(h, 'f', 0, 64))
|
||||
}
|
||||
|
||||
if between >= 3600*24 &&
|
||||
between < 3600*24*366 &&
|
||||
time.Unix(timestamp, 0).Format("2006") == time.Unix(now, 0).Format("2006") {
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date")
|
||||
return day.Format(timestamp, trans, tz)
|
||||
}
|
||||
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_year")
|
||||
return day.Format(timestamp, trans, tz)
|
||||
},
|
||||
"wrapComments": func(comments []*schema.GetCommentResp, la i18n.Language, tz string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"comments": comments,
|
||||
"language": la,
|
||||
"timezone": tz,
|
||||
}
|
||||
},
|
||||
"urlTitle": func(title string) string {
|
||||
return htmltext.UrlTitle(title)
|
||||
},
|
||||
}
|
||||
r.SetFuncMap(funcMap)
|
||||
|
||||
dev := os.Getenv("DEVCODE")
|
||||
if dev != "" {
|
||||
r.LoadHTMLGlob("../../ui/template/*")
|
||||
} else {
|
||||
html, _ := fs.Sub(ui.Template, "template")
|
||||
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
|
||||
r.SetHTMLTemplate(htmlTemplate)
|
||||
}
|
||||
|
||||
templateRouter.RegisterTemplateRouter(rootGroup)
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/day"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
)
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"replaceHTMLTag": func(src string, tags ...string) string {
|
||||
p := `(?U)<(\d+)>.+</(\d+)>`
|
||||
|
||||
re := regexp.MustCompile(p)
|
||||
ms := re.FindAllStringSubmatch(src, -1)
|
||||
for _, mi := range ms {
|
||||
if mi[1] == mi[2] {
|
||||
i, err := strconv.Atoi(mi[1])
|
||||
if err != nil || len(tags) < i {
|
||||
break
|
||||
}
|
||||
|
||||
src = strings.ReplaceAll(src, mi[0], tags[i-1])
|
||||
}
|
||||
}
|
||||
|
||||
return src
|
||||
},
|
||||
"join": func(sep string, elems ...string) string {
|
||||
return strings.Join(elems, sep)
|
||||
},
|
||||
"templateHTML": func(data string) template.HTML {
|
||||
return template.HTML(data)
|
||||
},
|
||||
"translator": func(la i18n.Language, data string, params ...interface{}) string {
|
||||
trans := translator.GlobalTrans.Tr(la, data)
|
||||
|
||||
if len(params) > 0 && len(params)%2 == 0 {
|
||||
for i := 0; i < len(params); i += 2 {
|
||||
k := converter.InterfaceToString(params[i])
|
||||
v := converter.InterfaceToString(params[i+1])
|
||||
trans = strings.ReplaceAll(trans, "{{ "+k+" }}", v)
|
||||
}
|
||||
}
|
||||
|
||||
return trans
|
||||
},
|
||||
"timeFormatISO": func(tz string, timestamp int64) string {
|
||||
_, _ = time.LoadLocation(tz)
|
||||
return time.Unix(timestamp, 0).Format("2006-01-02T15:04:05.000Z")
|
||||
},
|
||||
"translatorTimeFormatLongDate": func(la i18n.Language, tz string, timestamp int64) string {
|
||||
trans := translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_time")
|
||||
return day.Format(timestamp, trans, tz)
|
||||
},
|
||||
"translatorTimeFormat": func(la i18n.Language, tz string, timestamp int64) string {
|
||||
var (
|
||||
now = time.Now().Unix()
|
||||
between int64 = 0
|
||||
trans string
|
||||
)
|
||||
_, _ = time.LoadLocation(tz)
|
||||
if now > timestamp {
|
||||
between = now - timestamp
|
||||
}
|
||||
|
||||
if between <= 1 {
|
||||
return translator.GlobalTrans.Tr(la, "ui.dates.now")
|
||||
}
|
||||
|
||||
if between > 1 && between < 60 {
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_seconds_ago")
|
||||
return strings.ReplaceAll(trans, "{{count}}", converter.IntToString(between))
|
||||
}
|
||||
|
||||
if between >= 60 && between < 3600 {
|
||||
min := math.Floor(float64(between / 60))
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_minutes_ago")
|
||||
return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(min, 'f', 0, 64))
|
||||
}
|
||||
|
||||
if between >= 3600 && between < 3600*24 {
|
||||
h := math.Floor(float64(between / 3600))
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_hours_ago")
|
||||
return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(h, 'f', 0, 64))
|
||||
}
|
||||
|
||||
if between >= 3600*24 &&
|
||||
between < 3600*24*366 &&
|
||||
time.Unix(timestamp, 0).Format("2006") == time.Unix(now, 0).Format("2006") {
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date")
|
||||
return day.Format(timestamp, trans, tz)
|
||||
}
|
||||
|
||||
trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_year")
|
||||
return day.Format(timestamp, trans, tz)
|
||||
},
|
||||
"wrapComments": func(comments []*schema.GetCommentResp, la i18n.Language, tz string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"comments": comments,
|
||||
"language": la,
|
||||
"timezone": tz,
|
||||
}
|
||||
},
|
||||
"urlTitle": func(title string) string {
|
||||
return htmltext.UrlTitle(title)
|
||||
},
|
||||
}
|
|
@ -81,6 +81,10 @@ func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInf
|
|||
log.Error(err)
|
||||
}
|
||||
|
||||
resp.CustomCssHtml, err = tc.siteInfoService.GetSiteCustomCssHTML(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.Year = fmt.Sprintf("%d", time.Now().Year())
|
||||
return resp
|
||||
}
|
||||
|
@ -376,11 +380,12 @@ func (tc *TemplateController) UserInfo(ctx *gin.Context) {
|
|||
req := &schema.GetOtherUserInfoByUsernameReq{}
|
||||
req.Username = username
|
||||
userinfo, err := tc.templateRenderController.UserInfo(ctx, req)
|
||||
if !userinfo.Has {
|
||||
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if !userinfo.Has {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
@ -414,6 +419,9 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
|
|||
data["description"] = siteInfo.Description
|
||||
data["language"] = handler.GetLang(ctx)
|
||||
data["timezone"] = siteInfo.Interface.TimeZone
|
||||
data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead
|
||||
data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
|
||||
data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter
|
||||
_, ok := data["path"]
|
||||
if !ok {
|
||||
data["path"] = ""
|
||||
|
|
|
@ -155,6 +155,22 @@ func (sc *SiteInfoController) GetRobots(ctx *gin.Context) {
|
|||
ctx.String(http.StatusOK, resp.Robots)
|
||||
}
|
||||
|
||||
// GetRobots get site robots information
|
||||
// @Summary get site robots information
|
||||
// @Description get site robots information
|
||||
// @Tags site
|
||||
// @Produce json
|
||||
// @Success 200 {string} txt ""
|
||||
// @Router /custom.css [get]
|
||||
func (sc *SiteInfoController) GetCss(ctx *gin.Context) {
|
||||
resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx)
|
||||
if err != nil {
|
||||
ctx.String(http.StatusOK, "")
|
||||
return
|
||||
}
|
||||
ctx.String(http.StatusOK, resp.CustomCss)
|
||||
}
|
||||
|
||||
// UpdateSeo update site seo information
|
||||
// @Summary update site seo information
|
||||
// @Description update site seo information
|
||||
|
|
|
@ -32,6 +32,7 @@ func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) {
|
|||
r.GET("/sitemap/:page", a.templateController.SitemapPage)
|
||||
|
||||
r.GET("/robots.txt", a.siteInfoController.GetRobots)
|
||||
r.GET("/custom.css", a.siteInfoController.GetCss)
|
||||
|
||||
r.GET("/", a.templateController.Index)
|
||||
r.GET("/index", a.templateController.Index)
|
||||
|
|
|
@ -162,19 +162,20 @@ type SiteInfoResp struct {
|
|||
Login *SiteLoginResp `json:"login"`
|
||||
Theme *SiteThemeResp `json:"theme"`
|
||||
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
|
||||
SiteSeo *SiteSeoReq `json:"site_seo"`
|
||||
SiteSeo *SiteSeoReq `json:"site__seo"`
|
||||
}
|
||||
type TemplateSiteInfoResp struct {
|
||||
General *SiteGeneralResp `json:"general"`
|
||||
Interface *SiteInterfaceResp `json:"interface"`
|
||||
Branding *SiteBrandingResp `json:"branding"`
|
||||
SiteSeo *SiteSeoReq `json:"site_seo"`
|
||||
Title string
|
||||
Year string
|
||||
Canonical string
|
||||
JsonLD string
|
||||
Keywords string
|
||||
Description string
|
||||
General *SiteGeneralResp `json:"general"`
|
||||
Interface *SiteInterfaceResp `json:"interface"`
|
||||
Branding *SiteBrandingResp `json:"branding"`
|
||||
SiteSeo *SiteSeoReq `json:"site_seo"`
|
||||
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
|
||||
Title string
|
||||
Year string
|
||||
Canonical string
|
||||
JsonLD string
|
||||
Keywords string
|
||||
Description string
|
||||
}
|
||||
|
||||
// UpdateSMTPConfigReq get smtp config request
|
||||
|
|
|
@ -1 +1,44 @@
|
|||
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Answer</title><script defer="defer" src="/static/js/main.9de9552b.js"></script><link href="/static/css/main.d4180d41.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#doc-spinner{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}#doc-spinner .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="doc-spinner"><div class="spinner"></div></div></div></body></html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Answer</title>
|
||||
<script defer="defer" src="/static/js/main.9de9552b.js"></script>
|
||||
<link href="/static/css/main.d4180d41.css" rel="stylesheet">
|
||||
<link href="/custom.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<style>
|
||||
@keyframes _doc-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
#doc-spinner {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
#doc-spinner .spinner {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: -0.125em;
|
||||
border: 0.25rem solid currentColor;
|
||||
border-right-color: transparent;
|
||||
color: rgba(108, 117, 125, 0.75);
|
||||
border-radius: 50%;
|
||||
animation: 0.75s linear infinite _doc-spin;
|
||||
}
|
||||
</style>
|
||||
<div id="doc-spinner"><div class="spinner"></div></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{{templateHTML .FooterCode}}
|
||||
</html>
|
||||
{{end}}
|
||||
|
|
|
@ -11,13 +11,14 @@
|
|||
<link rel="canonical" href="{{.siteinfo.Canonical}}" />
|
||||
<link rel="manifest" href="/manifest.json"/>
|
||||
<link href="{{.cssPath}}" rel="stylesheet" />
|
||||
<link href="/custom.css" rel="stylesheet">
|
||||
<script defer="defer" src="{{.scriptPath}}"></script>
|
||||
{{templateHTML .HeadCode}}
|
||||
{{if $.siteinfo.JsonLD }}{{ .siteinfo.JsonLD | templateHTML}}{{end}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
{{templateHTML .HeaderCode}}
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<nav id="header" class="sticky-top navbar navbar-expand-lg navbar-dark">
|
||||
|
|
Loading…
Reference in New Issue