initialize next generation ui
This commit is contained in:
parent
0c994944aa
commit
ed599fef3f
19 changed files with 648 additions and 46 deletions
17
Makefile
17
Makefile
|
@ -2,9 +2,13 @@ PROJECT_NAME := yoake
|
|||
MODULE_PATH := github.com/eternal-flame-AD/${PROJECT_NAME}
|
||||
|
||||
CMD_DIR := cmd
|
||||
WASM_DIR := wasm
|
||||
|
||||
COMMANDS := $(patsubst ${CMD_DIR}/%,%,$(shell find ${CMD_DIR}/ -mindepth 1 -maxdepth 1 -type d))
|
||||
WASM_APPS := $(patsubst ${WASM_DIR}/%,%.wasm,$(shell find ${WASM_DIR}/ -mindepth 1 -maxdepth 1 -type d))
|
||||
|
||||
COMMANDSDIST = $(addprefix dist/,${COMMANDS})
|
||||
WASM_APPSDIST = $(addprefix dist/web/,${WASM_APPS})
|
||||
ifeq ($(INSTALLDEST),)
|
||||
INSTALLDEST := /opt/${PROJECT_NAME}
|
||||
endif
|
||||
|
@ -16,8 +20,8 @@ install:
|
|||
mkdir -p $(INSTALLDEST)
|
||||
cp -r dist/* $(INSTALLDEST)
|
||||
|
||||
build: webroot $(COMMANDSDIST)
|
||||
chmod -R 755 $(COMMANDSDIST)
|
||||
build: webroot $(COMMANDSDIST) $(WASM_APPSDIST)
|
||||
chmod -R 755 $(COMMANDSDIST) $(WASM_APPSDIST)
|
||||
|
||||
dev:
|
||||
while true; do \
|
||||
|
@ -30,6 +34,7 @@ dev:
|
|||
|
||||
webroot: $(wildcard webroot/**) FORCE
|
||||
mkdir -p dist
|
||||
mkdir -p dist/web
|
||||
cp -r assets dist
|
||||
cp -r webroot dist
|
||||
(cd dist/webroot; ../../scripts/webroot-build.fish)
|
||||
|
@ -42,6 +47,13 @@ clean:
|
|||
rm -rf dist/webroot
|
||||
rm -rf dist
|
||||
|
||||
dist/web/%.wasm: ${WASM_DIR}/% FORCE
|
||||
GOOS=js GOARCH=wasm CGO_ENABLED=0 go build -buildvcs\
|
||||
-ldflags "-X ${MODULE_PATH}/internal/version.tagVersion=$(VERSION) \
|
||||
-X ${MODULE_PATH}/internal/version.buildDate=$(BUILDDATE) \
|
||||
-s -w" \
|
||||
-o $@ ${MODULE_PATH}/$<
|
||||
|
||||
dist/%: ${CMD_DIR}/% FORCE
|
||||
go build -buildvcs\
|
||||
-ldflags "-X ${MODULE_PATH}/internal/version.tagVersion=$(VERSION) \
|
||||
|
@ -49,5 +61,6 @@ dist/%: ${CMD_DIR}/% FORCE
|
|||
-gcflags "$(GOGCFLAGS)" \
|
||||
-o $@ ${MODULE_PATH}/$<
|
||||
|
||||
|
||||
.PHONY: build clean
|
||||
FORCE:
|
4
go.mod
4
go.mod
|
@ -7,7 +7,7 @@ require (
|
|||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/dgraph-io/badger/v3 v3.2103.4
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.3
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.4
|
||||
github.com/eternal-flame-AD/yubigo v0.0.0-20221005082707-ce0c8989e8b1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c
|
||||
|
@ -16,10 +16,12 @@ require (
|
|||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/jinzhu/configor v1.2.1
|
||||
github.com/labstack/echo/v4 v4.9.1
|
||||
github.com/maxence-charriere/go-app/v9 v9.6.7
|
||||
github.com/spf13/afero v1.9.3
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/twilio/twilio-go v1.1.1
|
||||
github.com/vanng822/go-premailer v1.20.1
|
||||
golang.org/x/mod v0.7.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
)
|
||||
|
||||
|
|
8
go.sum
8
go.sum
|
@ -88,8 +88,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.3 h1:nFOxi6mbE8wpd5aHbSGvjbnaEjYC156IICWTteSgEIo=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.3/go.mod h1:OpqESxf/LXsssooWBPzAoIAC2PtloCT1CmA+glQKYV8=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.4 h1:MSHdwn+lCby8HWm3q4NZRWUejCNlJF86RbX+YWim6/A=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.4/go.mod h1:OpqESxf/LXsssooWBPzAoIAC2PtloCT1CmA+glQKYV8=
|
||||
github.com/eternal-flame-AD/yubigo v0.0.0-20221005082707-ce0c8989e8b1 h1:B+ad4UMWwNAUsZhLLQCCrEx+cfLsbf0+AbbcfG7RIv0=
|
||||
github.com/eternal-flame-AD/yubigo v0.0.0-20221005082707-ce0c8989e8b1/go.mod h1:kRnqsWaIjqWNPoCV14+cxs/B9eClc0hKL/I2a3LKOQ4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
|
@ -217,6 +217,8 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb
|
|||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/maxence-charriere/go-app/v9 v9.6.7 h1:t+wofnLjVsptBB7MNevsFymMYaMIX2hGjLdWgsIFgq4=
|
||||
github.com/maxence-charriere/go-app/v9 v9.6.7/go.mod h1:UlniES44R5JoD4HsjMNrAqWXSzyw0smM0Ox+QwnO/IE=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
|
@ -323,6 +325,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
4
internal/uinext/README.md
Normal file
4
internal/uinext/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# uinext
|
||||
|
||||
Next generation UI based on go-app
|
||||
|
48
internal/uinext/apicall/apicall.go
Normal file
48
internal/uinext/apicall/apicall.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package apicall
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/maxence-charriere/go-app/v9/pkg/app"
|
||||
)
|
||||
|
||||
func GET(ctx app.Context, path string, dispatch func(app.Context, *http.Response, error)) {
|
||||
resp, err := http.Get(path)
|
||||
ctx.Dispatch(func(ctx app.Context) {
|
||||
dispatch(ctx, resp, err)
|
||||
})
|
||||
}
|
||||
|
||||
func GetJSON(ctx app.Context, path string, result interface{}, dispatch func(app.Context, error)) {
|
||||
resp, err := http.Get(path)
|
||||
if err != nil {
|
||||
ctx.Dispatch(func(ctx app.Context) {
|
||||
dispatch(ctx, err)
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.Defer(func(app.Context) { resp.Body.Close() })
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
|
||||
ctx.Dispatch(func(ctx app.Context) {
|
||||
err = dec.Decode(result)
|
||||
dispatch(ctx, err)
|
||||
})
|
||||
}
|
||||
|
||||
type RequestAuth struct {
|
||||
Present bool
|
||||
Valid bool
|
||||
Roles []string
|
||||
Expire time.Time
|
||||
Ident UserIdent
|
||||
}
|
||||
|
||||
type UserIdent struct {
|
||||
Username string `json:"username"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
55
internal/uinext/compo/navbar.go
Normal file
55
internal/uinext/compo/navbar.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package compo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/uinext/webapp"
|
||||
"github.com/eternal-flame-AD/yoake/internal/version"
|
||||
"github.com/maxence-charriere/go-app/v9/pkg/app"
|
||||
)
|
||||
|
||||
// source: includes/navbar.tpl.html
|
||||
|
||||
type Navbar struct {
|
||||
app.Compo
|
||||
|
||||
LoginUsername string
|
||||
}
|
||||
|
||||
func (n *Navbar) renderBrand() app.UI {
|
||||
return app.A().Class("navbar-brand", "col-md-3", "col-lg-2", "me-0", "px-3", "fs-6").
|
||||
Href(webapp.Singleton.BasePath+"/").
|
||||
Body(
|
||||
app.Text("夜明け"),
|
||||
app.Small().Class("fw-lighter", "text-muted", "px-2").Text(fmt.Sprintf("%s - %s", version.Version, version.Date)),
|
||||
)
|
||||
}
|
||||
|
||||
func (n *Navbar) renderNavBtn() app.UI {
|
||||
return app.Button().Class("navbar-toggler", "position-absolute", "d-md-none", "collapsed").
|
||||
Type("button").
|
||||
Aria("aria-label", "Toggle navigation").
|
||||
Attr("data-bs-toggle", "collapse").
|
||||
Attr("data-bs-target", "#sidebar").
|
||||
Attr("aria-controls", "sidebarMenu").
|
||||
Attr("aria-expanded", "false").
|
||||
Body(
|
||||
app.Span().Class("navbar-toggler-icon"),
|
||||
)
|
||||
}
|
||||
|
||||
func (n *Navbar) renderAuthUsername() app.UI {
|
||||
return app.Div().Class("navbar-nav").Body(
|
||||
app.Div().Class("nav-item", "text-nowrap", "px-3").Body(
|
||||
app.Text(n.LoginUsername),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (n *Navbar) Render() app.UI {
|
||||
return app.Nav().Class("navbar", "sticky-top", "flex-md-nowrap", "p-0").Body(
|
||||
n.renderBrand(),
|
||||
n.renderNavBtn(),
|
||||
n.renderAuthUsername(),
|
||||
)
|
||||
}
|
137
internal/uinext/compo/sidebar.go
Normal file
137
internal/uinext/compo/sidebar.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package compo
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/uinext/apicall"
|
||||
"github.com/eternal-flame-AD/yoake/internal/uinext/webapp"
|
||||
"github.com/maxence-charriere/go-app/v9/pkg/app"
|
||||
)
|
||||
|
||||
type Sidebar struct {
|
||||
app.Compo
|
||||
|
||||
routePath string
|
||||
|
||||
Auth apicall.RequestAuth
|
||||
}
|
||||
|
||||
func (c *Sidebar) OnMount(ctx app.Context) {
|
||||
ctx.Async(func() {
|
||||
apicall.GetJSON(ctx, "/api/auth/auth.json", &c.Auth, func(ctx app.Context, err error) {
|
||||
if err != nil {
|
||||
log.Printf("Failed to get auth info: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Got auth info: %+v", c.Auth)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Sidebar) OnNav(ctx app.Context) {
|
||||
c.routePath = ctx.Page().URL().Path
|
||||
c.Update()
|
||||
}
|
||||
|
||||
func (c *Sidebar) Render() app.UI {
|
||||
return app.Nav().ID("sidebar").
|
||||
Class("col-md-3", "col-lg-2", "d-md-block", "bg-light", "sidebar", "collapse").
|
||||
Body(
|
||||
app.Div().Class("position-sticky", "pt-3", "sidebar-sticky").Body(
|
||||
&SidebarItem{
|
||||
Name: "Dashboard",
|
||||
Link: webapp.Singleton.BasePath + "/",
|
||||
CurPath: c.routePath,
|
||||
Auth: c.Auth,
|
||||
},
|
||||
SidebarHeading("Entertainment"),
|
||||
&SidebarItem{
|
||||
Name: "YouTube Playlist",
|
||||
Link: webapp.Singleton.BasePath + "/entertainment/youtube",
|
||||
CurPath: c.routePath,
|
||||
Auth: c.Auth,
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type SidebarItem struct {
|
||||
app.Compo
|
||||
|
||||
Name string
|
||||
Link string
|
||||
CurPath string
|
||||
|
||||
state SidebarItemState
|
||||
checkAccess func(auth apicall.RequestAuth) bool
|
||||
|
||||
Auth apicall.RequestAuth
|
||||
HasAccess bool
|
||||
}
|
||||
|
||||
func (c *SidebarItem) OnMount(ctx app.Context) {
|
||||
c.Update()
|
||||
}
|
||||
|
||||
func (c *SidebarItem) OnUpdate(ctx app.Context) {
|
||||
if c.checkAccess != nil {
|
||||
c.HasAccess = c.checkAccess(c.Auth)
|
||||
} else {
|
||||
c.HasAccess = true
|
||||
}
|
||||
|
||||
if c.Link == c.CurPath {
|
||||
if c.HasAccess {
|
||||
c.state = SidebarItemStateActive
|
||||
} else {
|
||||
c.state = SidebarItemStateAccessDenied
|
||||
}
|
||||
} else {
|
||||
if c.HasAccess {
|
||||
c.state = SidebarItemStateAvailable
|
||||
} else {
|
||||
c.state = SidebaritemStateAuthRequired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SidebarItemState int
|
||||
|
||||
const (
|
||||
SidebarItemStateUnknown SidebarItemState = 0
|
||||
SidebarItemStateActive SidebarItemState = 1
|
||||
SidebarItemStateAvailable SidebarItemState = 2
|
||||
SidebaritemStateAuthRequired SidebarItemState = 3
|
||||
SidebarItemStateAccessDenied SidebarItemState = 4
|
||||
)
|
||||
|
||||
func (c *SidebarItem) Render() app.UI {
|
||||
// TODO: allow theming
|
||||
var img app.HTMLImg
|
||||
switch c.state {
|
||||
case SidebaritemStateAuthRequired:
|
||||
img = app.Img().Src(webapp.Singleton.TrimaImgBase + "icon_t_vista_procedure_ineligible.gif")
|
||||
case SidebarItemStateAccessDenied:
|
||||
img = app.Img().Src(webapp.Singleton.TrimaImgBase + "icon_t_vista_procedure_invalid.gif")
|
||||
case SidebarItemStateActive:
|
||||
img = app.Img().Src(webapp.Singleton.TrimaImgBase + "icon_t_vista_procedure_optimal.gif")
|
||||
case SidebarItemStateAvailable:
|
||||
img = app.Img().Src(webapp.Singleton.TrimaImgBase + "icon_t_vista_procedure_valid.gif")
|
||||
default:
|
||||
img = app.Img().Src(webapp.Singleton.TrimaImgBase + "icon_t_vista_procedure_questionable.gif")
|
||||
}
|
||||
return app.Ul().Class("nav", "flex-column").Body(
|
||||
app.Li().Class("nav-item").Body(
|
||||
app.A().Class("nav-link").Href(c.Link).Body(
|
||||
app.Span().Class("px-1").Body(img.Style("height", "2.5rem").Style("width", "2.5rem")),
|
||||
app.Text(c.Name),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func SidebarHeading(title string) app.UI {
|
||||
return app.H6().Class("sidebar-heading", "d-flex", "justify-content-between", "align-items-center", "px-3", "mt-4", "mb-1", "text-muted").Body(
|
||||
app.Span().Text(title),
|
||||
)
|
||||
}
|
108
internal/uinext/ui/app.go
Normal file
108
internal/uinext/ui/app.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/uinext/compo"
|
||||
"github.com/eternal-flame-AD/yoake/internal/uinext/ui/page"
|
||||
"github.com/eternal-flame-AD/yoake/internal/uinext/webapp"
|
||||
"github.com/eternal-flame-AD/yoake/internal/util"
|
||||
"github.com/eternal-flame-AD/yoake/internal/version"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/maxence-charriere/go-app/v9/pkg/app"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
app.Compo
|
||||
|
||||
navbar *compo.Navbar
|
||||
sidebar *compo.Sidebar
|
||||
}
|
||||
|
||||
func (a *App) Render() app.UI {
|
||||
if a.navbar == nil {
|
||||
a.navbar = &compo.Navbar{}
|
||||
}
|
||||
if a.sidebar == nil {
|
||||
a.sidebar = &compo.Sidebar{}
|
||||
}
|
||||
return app.Div().ID("app").Body(
|
||||
a.navbar,
|
||||
app.Div().Class("row").Body(
|
||||
a.sidebar,
|
||||
&page.Router{
|
||||
Rules: []page.RouterRule{
|
||||
{
|
||||
CheckNav: page.RouterMatchPathRegexp(regexp.MustCompile(`^/$`)),
|
||||
Page: &page.Dashboard{},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const BasePath = "/uinext"
|
||||
|
||||
func Register(g *echo.Group) {
|
||||
webapp.Singleton.BasePath = BasePath
|
||||
webapp.Singleton.TrimaImgBase = "https://yumechi.jp/img/trima/"
|
||||
|
||||
handler := &app.Handler{
|
||||
Name: "Yoake PMS",
|
||||
ShortName: "夜明け",
|
||||
Description: "Yoake PMS - 夜明け",
|
||||
RawHeaders: util.Join(headTagBootstrap, headTagDayjs, headTagCustom),
|
||||
Lang: "en",
|
||||
AutoUpdateInterval: 10 * time.Second,
|
||||
BackgroundColor: "#FEDFE1",
|
||||
ThemeColor: "#FEDFE1",
|
||||
LoadingLabel: "Loading...",
|
||||
Version: version.Version + "-" + version.Date,
|
||||
Icon: app.Icon{
|
||||
Default: webapp.Singleton.TrimaImgBase + "icon_squeeze.gif",
|
||||
},
|
||||
}
|
||||
|
||||
app.RouteWithRegexp("^"+BasePath+"/.*", new(App))
|
||||
|
||||
g.Group("/web").GET("*", func(c echo.Context) error {
|
||||
handler.ServeHTTP(c.Response(), c.Request())
|
||||
return nil
|
||||
})
|
||||
g.GET("/web/app.wasm", func(c echo.Context) error {
|
||||
tryPaths := []string{
|
||||
"web/app.wasm",
|
||||
"dist/web/app.wasm",
|
||||
"app.wasm",
|
||||
}
|
||||
for _, path := range tryPaths {
|
||||
if stat, err := os.Stat(path); err == nil && !stat.IsDir() {
|
||||
return c.File(path)
|
||||
}
|
||||
}
|
||||
return c.NoContent(404)
|
||||
})
|
||||
g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if util.Contain(staticFiles, strings.ToLower(c.Request().URL.Path)) {
|
||||
handler.ServeHTTP(c.Response(), c.Request())
|
||||
return nil
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
g.Group(BasePath).Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
next(c)
|
||||
if !c.Response().Committed {
|
||||
handler.ServeHTTP(c.Response(), c.Request())
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
45
internal/uinext/ui/page/dashboard.go
Normal file
45
internal/uinext/ui/page/dashboard.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package page
|
||||
|
||||
import "github.com/maxence-charriere/go-app/v9/pkg/app"
|
||||
|
||||
type Dashboard struct {
|
||||
app.Compo
|
||||
}
|
||||
|
||||
func (d *Dashboard) Render() app.UI {
|
||||
return BasePage(BasicHeading("Dashboard"),
|
||||
app.Div().Class("container").Body(
|
||||
app.Div().Class("row").Body(
|
||||
app.Div().Class("col").Body(
|
||||
&Card{
|
||||
Header: app.Text("Welcome"),
|
||||
BodyClass: []string{"text-center"},
|
||||
Body: []app.UI{
|
||||
app.Blockquote().Class("blockquote").Body(
|
||||
app.P().Text("夜明け前が一番暗い"),
|
||||
app.P().Text("The night is darkest just before the dawn."),
|
||||
),
|
||||
app.Hr(),
|
||||
app.Div().ID("welcome").Body(
|
||||
app.P().Body(
|
||||
app.Text("Welcome to yoake.yumechi.jp, Yumechi's "),
|
||||
Abbr("PIM", "Personal Information Manager", "initialism", "https://en.wikipedia.org/wiki/Personal_information_manager"),
|
||||
app.Text("."),
|
||||
),
|
||||
app.P().Body(
|
||||
app.Text("Built with "),
|
||||
Abbr("Echo", "Echo HTTP Framework", "", "https://echo.labstack.com/"),
|
||||
app.Text(", "),
|
||||
Abbr("Bootstrap", "Bootstrap CSS Framework", "", "https://getbootstrap.com/"),
|
||||
app.Text(", and "),
|
||||
Abbr("go-app", "Go PWA Framework", "", "https://go-app.dev/"),
|
||||
app.Text("."),
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
45
internal/uinext/ui/page/partial.go
Normal file
45
internal/uinext/ui/page/partial.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package page
|
||||
|
||||
import "github.com/maxence-charriere/go-app/v9/pkg/app"
|
||||
|
||||
func BasePage(elems ...app.UI) app.UI {
|
||||
return app.Main().Class("col-md-9", "ms-sm-auto", "col-lg-10", "px-md-4").Body(elems...)
|
||||
}
|
||||
|
||||
func BasicHeading(name string) app.UI {
|
||||
return app.Div().Body(
|
||||
app.H1().Class("page-header").Text(name),
|
||||
app.Hr(),
|
||||
)
|
||||
}
|
||||
|
||||
type Card struct {
|
||||
app.Compo
|
||||
|
||||
HeaderClass []string
|
||||
Header app.UI
|
||||
|
||||
BodyClass []string
|
||||
Body []app.UI
|
||||
}
|
||||
|
||||
func (c *Card) Render() app.UI {
|
||||
return app.Div().Class("card", "border").Body(
|
||||
app.Div().Class(append(c.HeaderClass, "card-header")...).Body(c.Header),
|
||||
app.Div().Class(append(c.BodyClass, "card-body")...).Body(c.Body...),
|
||||
)
|
||||
}
|
||||
|
||||
func Abbr(abbr string, title string, class string, href string) app.UI {
|
||||
abbrEle := app.Abbr()
|
||||
if title != "" {
|
||||
abbrEle = abbrEle.Title(title)
|
||||
}
|
||||
if class != "" {
|
||||
abbrEle = abbrEle.Class(class)
|
||||
}
|
||||
if href != "" {
|
||||
return app.A().Target("_blank").Rel("noopener noreferrer").Href(href).Body(abbrEle.Text(abbr))
|
||||
}
|
||||
return abbrEle.Text(abbr)
|
||||
}
|
59
internal/uinext/ui/page/router.go
Normal file
59
internal/uinext/ui/page/router.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/uinext/webapp"
|
||||
"github.com/maxence-charriere/go-app/v9/pkg/app"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
app.Compo
|
||||
matchedPage app.UI
|
||||
|
||||
Rules []RouterRule
|
||||
NotFound app.UI
|
||||
}
|
||||
|
||||
func (r *Router) OnNav(ctx app.Context) {
|
||||
defer r.Update()
|
||||
for _, rule := range r.Rules {
|
||||
urlCopy := *ctx.Page().URL()
|
||||
if !rule.NoStripPrefix {
|
||||
bp := webapp.Singleton.BasePath
|
||||
if bp != "" {
|
||||
urlCopy.Path = strings.TrimPrefix(urlCopy.Path, bp)
|
||||
urlCopy.RawPath = strings.TrimPrefix(urlCopy.RawPath, bp)
|
||||
}
|
||||
}
|
||||
if rule.CheckNav(&urlCopy) {
|
||||
r.matchedPage = rule.Page
|
||||
return
|
||||
}
|
||||
}
|
||||
r.matchedPage = nil
|
||||
}
|
||||
|
||||
func (r *Router) Render() app.UI {
|
||||
if r.matchedPage != nil {
|
||||
return r.matchedPage
|
||||
}
|
||||
if r.NotFound != nil {
|
||||
return r.NotFound
|
||||
}
|
||||
return BasePage(BasicHeading("404 Not Found"), app.P().Text("The page you are looking for does not exist."))
|
||||
}
|
||||
|
||||
type RouterRule struct {
|
||||
NoStripPrefix bool
|
||||
CheckNav func(*url.URL) bool
|
||||
Page app.UI
|
||||
}
|
||||
|
||||
func RouterMatchPathRegexp(regex *regexp.Regexp) func(*url.URL) bool {
|
||||
return func(u *url.URL) bool {
|
||||
return regex.MatchString(u.Path)
|
||||
}
|
||||
}
|
36
internal/uinext/ui/static.go
Normal file
36
internal/uinext/ui/static.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package ui
|
||||
|
||||
var headTagDayjs = []string{
|
||||
`<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.6/dayjs.min.js"
|
||||
integrity="sha256-EfJOqCcshFS/2TxhArURu3Wn8b/XDA4fbPWKSwZ+1B8=" crossorigin="anonymous"></script>`,
|
||||
` <script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.6/plugin/relativeTime.js"
|
||||
integrity="sha256-muryXOPFkVJcJO1YFmhuKyXYmGDT2TYVxivG0MCgRzg=" crossorigin="anonymous"></script>`,
|
||||
` <script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.6/plugin/localizedFormat.js"
|
||||
integrity="sha256-g+gxm1xmRq4IecSRujv2eKyUCo/i1b5kRnWNcSbYEO0=" crossorigin="anonymous"></script>`,
|
||||
`<script>
|
||||
dayjs.extend(window.dayjs_plugin_relativeTime);
|
||||
dayjs.extend(window.dayjs_plugin_localizedFormat);</script>`,
|
||||
}
|
||||
|
||||
var headTagBootstrap = []string{
|
||||
`<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.min.js"
|
||||
integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>`,
|
||||
`<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">`,
|
||||
`<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3"
|
||||
crossorigin="anonymous"></script>`,
|
||||
}
|
||||
|
||||
var headTagCustom = []string{
|
||||
`<link rel="stylesheet" href="/style.css">`,
|
||||
`<link rel="stylesheet" href="/dashboard.css">`,
|
||||
}
|
||||
|
||||
var staticFiles = []string{
|
||||
"/app-worker.js",
|
||||
"/app.js",
|
||||
"/app.css",
|
||||
"/manifest.webmanifest",
|
||||
"/wasm_exec.js",
|
||||
}
|
9
internal/uinext/webapp/webapp.go
Normal file
9
internal/uinext/webapp/webapp.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package webapp
|
||||
|
||||
type IWebApp struct {
|
||||
BasePath string
|
||||
|
||||
TrimaImgBase string
|
||||
}
|
||||
|
||||
var Singleton IWebApp
|
|
@ -1,3 +1,5 @@
|
|||
//go:build linux
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
|
|
|
@ -32,6 +32,14 @@ func AntiJoin[T comparable](a []T, b []T) []T {
|
|||
return result
|
||||
}
|
||||
|
||||
func Join[T any](vals ...[]T) []T {
|
||||
result := make([]T, 0)
|
||||
for _, val := range vals {
|
||||
result = append(result, val...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func Reverse[T any](a []T) []T {
|
||||
var result []T
|
||||
for i := len(a) - 1; i >= 0; i-- {
|
||||
|
|
|
@ -1,48 +1,9 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
tagVersion = ""
|
||||
buildDate = "unknown"
|
||||
|
||||
Date = "unknown"
|
||||
Version = func() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
var vcsRevision string
|
||||
var vcsTime time.Time
|
||||
var vcsModified bool
|
||||
for _, setting := range info.Settings {
|
||||
switch setting.Key {
|
||||
case "vcs.revision":
|
||||
vcsRevision = setting.Value
|
||||
case "vcs.time":
|
||||
vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
|
||||
case "vcs.modified":
|
||||
vcsModified = setting.Value != "false"
|
||||
}
|
||||
}
|
||||
if tagVersion != "" {
|
||||
vcsRevision = tagVersion
|
||||
}
|
||||
|
||||
if vcsModified {
|
||||
Date = buildDate
|
||||
return vcsRevision + "+devel"
|
||||
} else {
|
||||
Date = buildDate
|
||||
if !vcsTime.IsZero() {
|
||||
Date = vcsTime.Format("2006-01-02T15:04Z07:00")
|
||||
}
|
||||
return vcsRevision
|
||||
}
|
||||
|
||||
}()
|
||||
Date = buildDate
|
||||
Version = tagVersion
|
||||
)
|
||||
|
|
44
internal/version/version_vcs.go
Normal file
44
internal/version/version_vcs.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
//go:build !tinygo
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
Version = "unknown"
|
||||
return
|
||||
}
|
||||
|
||||
var vcsRevision string
|
||||
var vcsTime time.Time
|
||||
var vcsModified bool
|
||||
for _, setting := range info.Settings {
|
||||
switch setting.Key {
|
||||
case "vcs.revision":
|
||||
vcsRevision = setting.Value
|
||||
case "vcs.time":
|
||||
vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
|
||||
case "vcs.modified":
|
||||
vcsModified = setting.Value != "false"
|
||||
}
|
||||
}
|
||||
if tagVersion != "" {
|
||||
vcsRevision = tagVersion
|
||||
}
|
||||
|
||||
if vcsModified {
|
||||
Date = buildDate
|
||||
Version = vcsRevision + "+devel"
|
||||
} else {
|
||||
Date = buildDate
|
||||
if !vcsTime.IsZero() {
|
||||
Date = vcsTime.Format("2006-01-02T15:04Z07:00")
|
||||
}
|
||||
Version = vcsRevision
|
||||
}
|
||||
}
|
|
@ -14,10 +14,12 @@ import (
|
|||
"github.com/eternal-flame-AD/yoake/internal/echoerror"
|
||||
"github.com/eternal-flame-AD/yoake/internal/entertainment"
|
||||
"github.com/eternal-flame-AD/yoake/internal/filestore"
|
||||
"github.com/eternal-flame-AD/yoake/internal/gomod"
|
||||
"github.com/eternal-flame-AD/yoake/internal/health"
|
||||
"github.com/eternal-flame-AD/yoake/internal/servetpl"
|
||||
"github.com/eternal-flame-AD/yoake/internal/session"
|
||||
"github.com/eternal-flame-AD/yoake/internal/twilio"
|
||||
"github.com/eternal-flame-AD/yoake/internal/uinext/ui"
|
||||
"github.com/eternal-flame-AD/yoake/internal/utilapi"
|
||||
"github.com/eternal-flame-AD/yoake/server"
|
||||
"github.com/gorilla/context"
|
||||
|
@ -66,6 +68,11 @@ func Init(hostname string, comm *comm.Communicator, database db.DB, fs filestore
|
|||
e.Use(middleware.RequestLoggerWithConfig(lc))
|
||||
}
|
||||
|
||||
{
|
||||
goproxy := e.Group("/goproxy")
|
||||
e.Use(gomod.Register("/goproxy", goproxy))
|
||||
}
|
||||
|
||||
api := e.Group("/api", echoerror.Middleware(echoerror.JSONWriter))
|
||||
{
|
||||
canvaslms.Register(api.Group("/canvas", logMiddleware("api_canvas", nil)), comm)
|
||||
|
@ -112,5 +119,6 @@ func Init(hostname string, comm *comm.Communicator, database db.DB, fs filestore
|
|||
logMiddleware("template", servetpl.ServeTemplateDir(webroot.Root)),
|
||||
logMiddleware("static", middleware.Static(webroot.Root)))
|
||||
|
||||
ui.Register(e.Group(""))
|
||||
server.RegisterHostname(hostname, &server.Host{Echo: e})
|
||||
}
|
||||
|
|
14
wasm/app/app.go
Normal file
14
wasm/app/app.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
//go:build wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/eternal-flame-AD/yoake/internal/uinext/ui"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/maxence-charriere/go-app/v9/pkg/app"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ui.Register(echo.New().Group(""))
|
||||
app.RunWhenOnBrowser()
|
||||
}
|
Loading…
Add table
Reference in a new issue