mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2025-10-29 09:15:33 +00:00
hot reload, search refactor begin
This commit is contained in:
@@ -8,6 +8,8 @@ full_bin = "./tmp/musenalm --dir ./pb_data serve"
|
||||
cmd = "go build -tags=dev,fts5,sqlite_icu -o ./tmp/musenalm ."
|
||||
delay = 400
|
||||
exclude_dir = [
|
||||
"views/routes",
|
||||
"views/layouts",
|
||||
"views/assets",
|
||||
"views/node_modules",
|
||||
"tmp",
|
||||
@@ -32,7 +34,7 @@ log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = ["npm --prefix views/ run build"]
|
||||
pre_cmd = [""]
|
||||
rerun = false
|
||||
rerun_delay = 250
|
||||
send_interrupt = true
|
||||
|
||||
21
app/pb.go
21
app/pb.go
@@ -120,20 +120,21 @@ func (app *App) Serve() error {
|
||||
|
||||
// INFO: hot reloading for poor people
|
||||
if app.MAConfig.Debug {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
watcher, err := EngineWatcher(engine)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create watcher: %w.", err)
|
||||
app.PB.Logger().Error("Failed to create watcher, continuing without", "error", err)
|
||||
} else {
|
||||
watcher.AddRecursive(LAYOUT_DIR)
|
||||
watcher.AddRecursive(ROUTES_DIR)
|
||||
engine.Debug()
|
||||
rwatcher, err := RefreshWatcher(engine)
|
||||
if err != nil {
|
||||
app.PB.Logger().Error("Failed to create watcher, continuing without", "error", err)
|
||||
} else {
|
||||
rwatcher.Add("./views/assets")
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
go app.watchFN(watcher, engine)
|
||||
if err := watcher.Add(LAYOUT_DIR); err != nil {
|
||||
return fmt.Errorf("Failed to watch layout directory: %w.", err)
|
||||
}
|
||||
|
||||
if err := watcher.Add(ROUTES_DIR); err != nil {
|
||||
return fmt.Errorf("Failed to watch routes directory: %w.", err)
|
||||
}
|
||||
}
|
||||
|
||||
app.PB.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error {
|
||||
|
||||
136
app/watch.go
Normal file
136
app/watch.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
const (
|
||||
WATCHER_DEBOUNCE = 300 * time.Millisecond
|
||||
)
|
||||
|
||||
// INFO: this is hot reload for poor people
|
||||
type Watcher struct {
|
||||
*fsnotify.Watcher
|
||||
}
|
||||
|
||||
func RefreshWatcher(engine *templating.Engine) (*Watcher, error) {
|
||||
watcher := Watcher{}
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
watcher.Watcher = w
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
var reloadTimer *time.Timer
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename) != 0 {
|
||||
if reloadTimer != nil {
|
||||
reloadTimer.Stop()
|
||||
}
|
||||
reloadTimer = time.AfterFunc(WATCHER_DEBOUNCE, func() {
|
||||
log.Println("Changes detected, reloading templates...")
|
||||
engine.Refresh()
|
||||
})
|
||||
}
|
||||
|
||||
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||
fi, statErr := os.Stat(event.Name)
|
||||
if statErr == nil && fi.IsDir() {
|
||||
_ = watcher.Add(event.Name)
|
||||
log.Printf("Now watching new directory: %s", event.Name)
|
||||
}
|
||||
}
|
||||
|
||||
case err := <-watcher.Errors:
|
||||
if err != nil {
|
||||
log.Printf("fsnotify error: %v\n", err)
|
||||
}
|
||||
|
||||
case <-done:
|
||||
watcher.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &watcher, nil
|
||||
|
||||
}
|
||||
|
||||
func EngineWatcher(engine *templating.Engine) (*Watcher, error) {
|
||||
watcher := Watcher{}
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
watcher.Watcher = w
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
var reloadTimer *time.Timer
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename) != 0 {
|
||||
if reloadTimer != nil {
|
||||
reloadTimer.Stop()
|
||||
}
|
||||
reloadTimer = time.AfterFunc(WATCHER_DEBOUNCE, func() {
|
||||
log.Println("Changes detected, reloading templates...")
|
||||
engine.Reload()
|
||||
})
|
||||
}
|
||||
|
||||
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||
fi, statErr := os.Stat(event.Name)
|
||||
if statErr == nil && fi.IsDir() {
|
||||
_ = watcher.Add(event.Name)
|
||||
log.Printf("Now watching new directory: %s", event.Name)
|
||||
}
|
||||
}
|
||||
|
||||
case err := <-watcher.Errors:
|
||||
if err != nil {
|
||||
log.Printf("fsnotify error: %v\n", err)
|
||||
}
|
||||
|
||||
case <-done:
|
||||
watcher.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &watcher, nil
|
||||
}
|
||||
|
||||
func (w *Watcher) AddRecursive(root string) error {
|
||||
return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
werr := w.Add(path)
|
||||
if werr != nil {
|
||||
return werr
|
||||
}
|
||||
log.Printf("Now watching directory: %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -22,6 +22,15 @@ func REntriesAgents_Agent(app core.App, id string) ([]*REntriesAgents, error) {
|
||||
)
|
||||
}
|
||||
|
||||
func REntriesAgents_Entry(app core.App, id string) ([]*REntriesAgents, error) {
|
||||
return TableByField[[]*REntriesAgents](
|
||||
app,
|
||||
RelationTableName(ENTRIES_TABLE, AGENTS_TABLE),
|
||||
ENTRIES_TABLE,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
func RContentsAgents_Agent(app core.App, id string) ([]*RContentsAgents, error) {
|
||||
return TableByField[[]*RContentsAgents](
|
||||
app,
|
||||
|
||||
@@ -104,3 +104,13 @@ func (p *AlmanachPage) getAbbr(app core.App, data map[string]interface{}) error
|
||||
func (p *AlmanachPage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
|
||||
return engine.Response200(request, TEMPLATE_ALMANACH, data)
|
||||
}
|
||||
|
||||
type AlmanachResult struct {
|
||||
Entry *dbmodels.Entry
|
||||
Places []*dbmodels.Place
|
||||
Series []*dbmodels.Series
|
||||
Contents []*dbmodels.Content
|
||||
}
|
||||
|
||||
type AlmanachData struct {
|
||||
}
|
||||
|
||||
247
pages/suche.go
247
pages/suche.go
@@ -1,12 +1,13 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -47,82 +48,41 @@ func (p *SuchePage) Setup(router *router.Router[*core.RequestEvent], app core.Ap
|
||||
})
|
||||
|
||||
router.GET(URL_SUCHE, func(e *core.RequestEvent) error {
|
||||
if !slices.Contains(availableTypes, e.Request.PathValue("type")) {
|
||||
return engine.Response404(e, nil, nil)
|
||||
paras, err := NewParameters(e)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, nil)
|
||||
}
|
||||
|
||||
q := e.Request.URL.Query().Get(PARAM_QUERY)
|
||||
q = strings.TrimSpace(q)
|
||||
if q != "" {
|
||||
return p.SimpleSearchRequest(app, engine, e)
|
||||
}
|
||||
// if e.Request.URL.Query().Get(PARAM_QUERY) != "" {
|
||||
// return p.SearchRequest(app, engine, e)
|
||||
// }
|
||||
return p.DefaultRequest(app, engine, e)
|
||||
data := make(map[string]interface{})
|
||||
data["parameters"] = paras
|
||||
return engine.Response200(e, p.Template+paras.Collection+"/", data, p.Layout)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SuchePage) SimpleSearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||
t := e.Request.PathValue("type")
|
||||
if t == "reihen" {
|
||||
return p.SimpleSearchReihenRequest(app, engine, e)
|
||||
}
|
||||
if t == "baende" {
|
||||
return p.SimpleSearchBaendeRequest(app, engine, e)
|
||||
}
|
||||
// if t == "beitraege" {
|
||||
// return p.SimpleSearchBeitraegeRequest(app, engine, e)
|
||||
// }
|
||||
// if t == "personen" {
|
||||
// return p.SimpleSearchPersonenRequest(app, engine, e)
|
||||
// }
|
||||
func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||
return engine.Response404(e, nil, nil)
|
||||
}
|
||||
|
||||
const (
|
||||
REIHEN_PARAM_TITLE = "title"
|
||||
REIHEN_PARAM_ANNOTATIONS = "annotations"
|
||||
REIHEN_PARAM_REFERENCES = "references"
|
||||
)
|
||||
func (p *SuchePage) SimpleSearchBaendeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, pp Parameters) error {
|
||||
data := make(map[string]interface{})
|
||||
params, err := NewSimpleParameters(e, pp)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, nil)
|
||||
}
|
||||
|
||||
func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||
q := e.Request.URL.Query().Get(PARAM_QUERY)
|
||||
data := p.CommonData(app, engine, e)
|
||||
data["q"] = q
|
||||
|
||||
hasTitle := e.Request.URL.Query().Get(REIHEN_PARAM_TITLE) == "on"
|
||||
hasAnnotations := e.Request.URL.Query().Get(REIHEN_PARAM_ANNOTATIONS) == "on"
|
||||
hasReferences := e.Request.URL.Query().Get(REIHEN_PARAM_REFERENCES) == "on"
|
||||
|
||||
if !hasTitle && !hasAnnotations && !hasReferences {
|
||||
query := dbmodels.NormalizeQuery(params.Query)
|
||||
if len(query) == 0 {
|
||||
engine.Response404(e, nil, nil)
|
||||
}
|
||||
|
||||
fields := []string{}
|
||||
options := map[string]bool{}
|
||||
if hasTitle {
|
||||
fields = append(fields, dbmodels.SERIES_TITLE_FIELD)
|
||||
options[REIHEN_PARAM_TITLE] = true
|
||||
}
|
||||
if hasAnnotations {
|
||||
fields = append(fields, dbmodels.ANNOTATION_FIELD)
|
||||
options[REIHEN_PARAM_ANNOTATIONS] = true
|
||||
}
|
||||
if hasReferences {
|
||||
fields = append(fields, dbmodels.REFERENCES_FIELD)
|
||||
options[REIHEN_PARAM_REFERENCES] = true
|
||||
}
|
||||
data["options"] = options
|
||||
|
||||
query := dbmodels.NormalizeQuery(q)
|
||||
if len(q) == 0 {
|
||||
fields := params.FieldSetBaende()
|
||||
if len(fields) == 0 {
|
||||
return engine.Response404(e, nil, nil)
|
||||
}
|
||||
|
||||
ids, err := dbmodels.FTS5Search(app, dbmodels.SERIES_TABLE, dbmodels.FTS5QueryRequest{
|
||||
ids, err := dbmodels.FTS5Search(app, dbmodels.ENTRIES_TABLE, dbmodels.FTS5QueryRequest{
|
||||
Fields: fields,
|
||||
Query: query,
|
||||
})
|
||||
@@ -130,43 +90,20 @@ func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.E
|
||||
return engine.Response500(e, err, nil)
|
||||
}
|
||||
|
||||
idsany := []any{}
|
||||
for _, id := range ids {
|
||||
idsany = append(idsany, id.ID)
|
||||
}
|
||||
|
||||
series, err := dbmodels.SeriessesForIds(app, idsany)
|
||||
rmap, bmap, err := dbmodels.EntriesForSeriesses(app, series)
|
||||
idsany := datatypes.ToAny(ids)
|
||||
entries, err := dbmodels.Entries_IDs(app, idsany)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, nil)
|
||||
}
|
||||
|
||||
dbmodels.Sort_Series_Title(series)
|
||||
data["series"] = series
|
||||
data["relations"] = rmap
|
||||
data["entries"] = bmap
|
||||
dbmodels.Sort_Entries_Title_Year(entries)
|
||||
data["entries"] = entries
|
||||
data["count"] = len(entries)
|
||||
|
||||
data["count"] = len(series)
|
||||
// TODO: get relavant agents, years and places
|
||||
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
|
||||
const (
|
||||
BAENDE_PARAM_ALM_NR = "alm"
|
||||
BAENDE_PARAM_TITLE = "title"
|
||||
BAENDE_PARAM_SERIES = "series"
|
||||
BAENDE_PARAM_PERSONS = "persons"
|
||||
BAENDE_PARAM_PLACES = "places"
|
||||
BAENDE_PARAM_REFS = "references"
|
||||
BAENDE_PARAM_ANNOTATIONS = "annotations"
|
||||
// INFO: this is expanded search only:
|
||||
BAENDE_PARAM_PSEUDONYMS = "pseudonyms"
|
||||
// INFO: this is a filter type & expanded search:
|
||||
BAENDE_PARAM_STATE = "state" // STATE: "full" "partial" "none"
|
||||
)
|
||||
|
||||
func (p *SuchePage) SimpleSearchBaendeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||
eids := []any{}
|
||||
for _, entry := range entries {
|
||||
eids = append(eids, entry.Id)
|
||||
}
|
||||
|
||||
return engine.Response404(e, nil, nil)
|
||||
}
|
||||
@@ -182,19 +119,125 @@ const (
|
||||
// INFO: these are filter types & expanded search:
|
||||
BEITRAEGE_PARAM_TYPE = "type"
|
||||
BEITRAEGE_PARAM_SCANS = "scans"
|
||||
|
||||
REIHEN_PARAM_TITLE = "title"
|
||||
REIHEN_PARAM_ANNOTATIONS = "annotations"
|
||||
REIHEN_PARAM_REFERENCES = "references"
|
||||
|
||||
BAENDE_PARAM_ALM_NR = "alm"
|
||||
BAENDE_PARAM_TITLE = "title"
|
||||
BAENDE_PARAM_SERIES = "series"
|
||||
BAENDE_PARAM_PERSONS = "persons"
|
||||
BAENDE_PARAM_PLACES = "pubdata"
|
||||
BAENDE_PARAM_REFS = "references"
|
||||
BAENDE_PARAM_ANNOTATIONS = "annotations"
|
||||
BAENDE_PARAM_YEAR = "year"
|
||||
// INFO: this is expanded search only:
|
||||
BAENDE_PARAM_PSEUDONYMS = "pseudonyms"
|
||||
// INFO: this is a filter type & expanded search:
|
||||
BAENDE_PARAM_STATE = "state" // STATE: "full" "partial" "none"
|
||||
)
|
||||
|
||||
func (p *SuchePage) DefaultRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||
data := p.CommonData(app, engine, e)
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
var ErrInvalidCollectionType = fmt.Errorf("Invalid collection type")
|
||||
var ErrNoQuery = fmt.Errorf("No query")
|
||||
|
||||
type Parameters struct {
|
||||
Extended bool
|
||||
Collection string
|
||||
Query string
|
||||
}
|
||||
|
||||
func (p *SuchePage) CommonData(app core.App, engine *templating.Engine, e *core.RequestEvent) map[string]interface{} {
|
||||
data := map[string]interface{}{}
|
||||
data["type"] = e.Request.PathValue("type")
|
||||
data[PARAM_EXTENDED] = false
|
||||
if e.Request.URL.Query().Get(PARAM_EXTENDED) == "true" {
|
||||
data[PARAM_EXTENDED] = true
|
||||
func NewParameters(e *core.RequestEvent) (*Parameters, error) {
|
||||
collection := e.Request.PathValue("type")
|
||||
if !slices.Contains(availableTypes, collection) {
|
||||
return nil, ErrInvalidCollectionType
|
||||
}
|
||||
return data
|
||||
|
||||
extended := false
|
||||
if e.Request.URL.Query().Get(PARAM_EXTENDED) == "true" {
|
||||
extended = true
|
||||
}
|
||||
|
||||
return &Parameters{
|
||||
Collection: collection,
|
||||
Extended: extended,
|
||||
Query: e.Request.URL.Query().Get(PARAM_QUERY),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type SimpleParameters struct {
|
||||
Parameters
|
||||
Annotations bool
|
||||
Persons bool
|
||||
Title bool
|
||||
Alm bool
|
||||
Series bool
|
||||
Places bool
|
||||
Refs bool
|
||||
Year bool
|
||||
}
|
||||
|
||||
func NewSimpleParameters(e *core.RequestEvent, p Parameters) (*SimpleParameters, error) {
|
||||
q := e.Request.URL.Query().Get(PARAM_QUERY)
|
||||
if q == "" {
|
||||
return nil, ErrNoQuery
|
||||
}
|
||||
|
||||
alm := e.Request.URL.Query().Get(BAENDE_PARAM_ALM_NR) == "on"
|
||||
title := e.Request.URL.Query().Get(BAENDE_PARAM_TITLE) == "on"
|
||||
series := e.Request.URL.Query().Get(BAENDE_PARAM_SERIES) == "on"
|
||||
persons := e.Request.URL.Query().Get(BAENDE_PARAM_PERSONS) == "on"
|
||||
places := e.Request.URL.Query().Get(BAENDE_PARAM_PLACES) == "on"
|
||||
refs := e.Request.URL.Query().Get(BAENDE_PARAM_REFS) == "on"
|
||||
annotations := e.Request.URL.Query().Get(BAENDE_PARAM_ANNOTATIONS) == "on"
|
||||
year := e.Request.URL.Query().Get(BAENDE_PARAM_YEAR) == "on"
|
||||
|
||||
// TODO: sanity check here if any single field is selected
|
||||
|
||||
return &SimpleParameters{
|
||||
Parameters: p,
|
||||
Alm: alm,
|
||||
Title: title,
|
||||
Series: series,
|
||||
Persons: persons,
|
||||
Places: places,
|
||||
Refs: refs,
|
||||
Annotations: annotations,
|
||||
Year: year,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p SimpleParameters) FieldSetBaende() []string {
|
||||
fields := []string{}
|
||||
if p.Alm {
|
||||
fields = append(fields, dbmodels.MUSENALMID_FIELD)
|
||||
}
|
||||
if p.Title {
|
||||
fields = append(fields,
|
||||
dbmodels.TITLE_STMT_FIELD,
|
||||
dbmodels.SUBTITLE_STMT_FIELD,
|
||||
dbmodels.INCIPIT_STMT_FIELD,
|
||||
dbmodels.VARIANT_TITLE_FIELD,
|
||||
dbmodels.PARALLEL_TITLE_FIELD,
|
||||
)
|
||||
}
|
||||
if p.Series {
|
||||
fields = append(fields, dbmodels.SERIES_TABLE)
|
||||
}
|
||||
if p.Persons {
|
||||
fields = append(fields, dbmodels.RESPONSIBILITY_STMT_FIELD, dbmodels.AGENTS_TABLE)
|
||||
}
|
||||
if p.Places {
|
||||
fields = append(fields, dbmodels.PLACE_STMT_FIELD, dbmodels.PLACES_TABLE, dbmodels.PUBLICATION_STMT_FIELD)
|
||||
}
|
||||
if p.Refs {
|
||||
fields = append(fields, dbmodels.REFERENCES_FIELD)
|
||||
}
|
||||
if p.Annotations {
|
||||
fields = append(fields, dbmodels.ANNOTATION_FIELD)
|
||||
}
|
||||
if p.Year {
|
||||
fields = append(fields, dbmodels.YEAR_FIELD)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -5,20 +5,64 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/functions"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
ASSETS_URL_PREFIX = "/assets"
|
||||
RELOAD_TEMPLATE = `
|
||||
<script type="module">
|
||||
(function () {
|
||||
let relto = -1;
|
||||
const scheme = location.protocol === "https:" ? "wss" : "ws";
|
||||
// Hardcode port 9000 here:
|
||||
const url = scheme + "://" + location.hostname + ":9000/pb/reload";
|
||||
|
||||
function connect() {
|
||||
const socket = new WebSocket(url);
|
||||
|
||||
socket.addEventListener("open", function () {
|
||||
console.log("Reload socket connected (port 9000).");
|
||||
});
|
||||
|
||||
socket.addEventListener("message", function (evt) {
|
||||
if (evt.data === "reload") {
|
||||
console.log("Received reload signal. Reloading...");
|
||||
if (relto !== -1) clearTimeout(relto);
|
||||
relto = setTimeout(() => location.reload(), 0);
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener("close", function () {
|
||||
console.log("Reload socket closed. Reconnecting in 3 seconds...");
|
||||
setTimeout(connect, 3000);
|
||||
});
|
||||
|
||||
socket.addEventListener("error", function (err) {
|
||||
console.error("Reload socket error:", err);
|
||||
// We'll let onclose handle reconnection.
|
||||
});
|
||||
}
|
||||
|
||||
// Initiate the first connection attempt.
|
||||
connect();
|
||||
})();
|
||||
</script>
|
||||
`
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
regmu *sync.Mutex
|
||||
debug bool
|
||||
ws *WsServer
|
||||
onceWS sync.Once
|
||||
|
||||
// NOTE: LayoutRegistry and TemplateRegistry have their own syncronization & cache and do not require a mutex here
|
||||
LayoutRegistry *LayoutRegistry
|
||||
@@ -44,6 +88,26 @@ func NewEngine(layouts, templates *fs.FS) *Engine {
|
||||
return &e
|
||||
}
|
||||
|
||||
func (e *Engine) Debug() {
|
||||
e.debug = true
|
||||
|
||||
e.onceWS.Do(func() {
|
||||
e.ws = NewWsServer()
|
||||
go e.startWsServerOnPort9000()
|
||||
})
|
||||
}
|
||||
|
||||
func (e *Engine) startWsServerOnPort9000() {
|
||||
// We'll create a basic default mux here and mount /pb/reload
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/pb/reload", websocket.Handler(e.ws.Handler))
|
||||
|
||||
log.Println("[Engine Debug] Starting separate WebSocket server on :9000 for live reload...")
|
||||
if err := http.ListenAndServe(":9000", mux); err != nil {
|
||||
log.Println("[Engine Debug] WebSocket server error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) funcs() error {
|
||||
e.mu.Lock()
|
||||
e.mu.Unlock()
|
||||
@@ -113,6 +177,12 @@ func (e *Engine) Reload() {
|
||||
e.Load()
|
||||
}
|
||||
|
||||
func (e *Engine) Refresh() {
|
||||
if e.debug && e.ws != nil {
|
||||
e.ws.BroadcastReload()
|
||||
}
|
||||
}
|
||||
|
||||
// INFO: fn is a function that returns either one value or two values, the second one being an error
|
||||
func (e *Engine) AddFunc(name string, fn interface{}) {
|
||||
e.mu.Lock()
|
||||
@@ -234,7 +304,15 @@ func (e *Engine) Response200(request *core.RequestEvent, path string, ld map[str
|
||||
return e.Response500(request, err, ld)
|
||||
}
|
||||
|
||||
return request.HTML(http.StatusOK, builder.String())
|
||||
tstring := builder.String()
|
||||
if e.debug {
|
||||
idx := strings.LastIndex(tstring, "</body>")
|
||||
if idx != -1 {
|
||||
tstring = tstring[:idx] + RELOAD_TEMPLATE + tstring[idx:]
|
||||
}
|
||||
}
|
||||
|
||||
return request.HTML(http.StatusOK, tstring)
|
||||
}
|
||||
|
||||
func requestData(request *core.RequestEvent) map[string]interface{} {
|
||||
|
||||
57
templating/ws.go
Normal file
57
templating/ws.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package templating
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
// WsServer manages all active WebSocket connections so we can broadcast.
|
||||
type WsServer struct {
|
||||
mu sync.Mutex
|
||||
conns map[*websocket.Conn]bool
|
||||
}
|
||||
|
||||
// NewWsServer creates a WsServer.
|
||||
func NewWsServer() *WsServer {
|
||||
return &WsServer{
|
||||
conns: make(map[*websocket.Conn]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Handler is invoked for each new WebSocket connection.
|
||||
func (s *WsServer) Handler(conn *websocket.Conn) {
|
||||
s.mu.Lock()
|
||||
s.conns[conn] = true
|
||||
s.mu.Unlock()
|
||||
log.Println("[WsServer] Connected:", conn.RemoteAddr())
|
||||
|
||||
// Read in a loop until an error (client disconnect).
|
||||
var msg string
|
||||
for {
|
||||
if err := websocket.Message.Receive(conn, &msg); err != nil {
|
||||
log.Println("[WsServer] Disconnected:", conn.RemoteAddr())
|
||||
s.mu.Lock()
|
||||
delete(s.conns, conn)
|
||||
s.mu.Unlock()
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastReload sends a "reload" message to all connected clients.
|
||||
func (s *WsServer) BroadcastReload() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for conn := range s.conns {
|
||||
err := websocket.Message.Send(conn, "reload")
|
||||
if err != nil {
|
||||
log.Println("[WsServer] Broadcast error:", err)
|
||||
conn.Close()
|
||||
delete(s.conns, conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
62
views/.air.toml
Normal file
62
views/.air.toml
Normal file
@@ -0,0 +1,62 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
full_bin = ""
|
||||
cmd = "npm run build"
|
||||
delay = 400
|
||||
exclude_dir = [
|
||||
"assets",
|
||||
"node_modules",
|
||||
"tmp",
|
||||
"vendor",
|
||||
"testdata",
|
||||
"data_git",
|
||||
"cache_gnd",
|
||||
"cache_geonames",
|
||||
"pb_data",
|
||||
"Almanach-Bilder",
|
||||
"Static-Bilder",
|
||||
]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "js", "css", "xsl"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = [""]
|
||||
rerun = false
|
||||
rerun_delay = 250
|
||||
send_interrupt = true
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = true
|
||||
|
||||
[proxy]
|
||||
app_port = 8090
|
||||
enabled = false
|
||||
proxy_port = 8081
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = true
|
||||
keep_scroll = true
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,4 @@
|
||||
{{ $model := . }}
|
||||
|
||||
{{ if and .startpage .record }}
|
||||
{{ template "hero" . }}
|
||||
{{ end }}
|
||||
@@ -17,7 +16,7 @@
|
||||
{{ template "alphabet" Dict "active" .letter "letters" .letters "search" .search }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="mt-2 border-b border-zinc-300 w-full"></div>
|
||||
<div class="mt-2 border-b w-full"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -42,7 +41,6 @@
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if .series }}
|
||||
<div class="mb-1 max-w-[60rem] hyphens-auto">
|
||||
{{ range $id, $r := .series }}
|
||||
|
||||
61
views/routes/suche/baende/body.gohtml
Normal file
61
views/routes/suche/baende/body.gohtml
Normal file
@@ -0,0 +1,61 @@
|
||||
{{ $model := . }}
|
||||
|
||||
|
||||
<div id="searchcontrol" class="container-normal">
|
||||
{{- template "_heading" $model.parameters.Parameters -}}
|
||||
<div id="" class="border-l border-zinc-300 px-8 py-10 relative">
|
||||
{{- if not $model.parameters.Extended -}}
|
||||
<form
|
||||
id="searchform"
|
||||
class="w-full font-serif"
|
||||
method="get"
|
||||
action="/suche/baende"
|
||||
autocomplete="off">
|
||||
<div class="searchformcolumn">
|
||||
{{- $q := "" }}
|
||||
{{- if $model.parameters.Query -}}
|
||||
{{- q = $model.parameters.Query -}}
|
||||
{{- end -}}
|
||||
{{ template "_searchboxsimple" Arr $model.parameters.Parameters true $q }}
|
||||
<fieldset class="selectgroup">
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="alm" id="alm" checked />
|
||||
<label for="alm">Almanach-Nr.</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="title" id="title" checked />
|
||||
<label for="title">Titel</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="series" id="series" checked />
|
||||
<label for="series">Reihentitel</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="persons" id="persons" checked />
|
||||
<label for="persons">Personen & Verlage</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="pubdata" id="pubdata" checked />
|
||||
<label for="pubdata">Orte</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="year" id="year" checked />
|
||||
<label for="year">Jahr</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="references" id="references" checked />
|
||||
<label for="references">Nachweise</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="annotations" id="annotations" checked />
|
||||
<label for="annotations">Anmerkungen</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
{{ template "infotextsimple" true }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{- template "_fieldscript" -}}
|
||||
1
views/routes/suche/baende/head.gohtml
Normal file
1
views/routes/suche/baende/head.gohtml
Normal file
@@ -0,0 +1 @@
|
||||
<title>{{ .site.title }}: Suche – Bände</title>
|
||||
48
views/routes/suche/beitraege/body.gohtml
Normal file
48
views/routes/suche/beitraege/body.gohtml
Normal file
@@ -0,0 +1,48 @@
|
||||
{{ $model := . }}
|
||||
|
||||
|
||||
<div id="searchcontrol" class="container-normal">
|
||||
{{- template "_heading" $model.parameters.Parameters -}}
|
||||
<div id="" class="border-l border-zinc-300 px-8 py-10 relative">
|
||||
{{- if not $model.parameters.Extended -}}
|
||||
<form
|
||||
id="searchform"
|
||||
class="w-full font-serif"
|
||||
method="get"
|
||||
action="/suche/baende"
|
||||
autocomplete="off">
|
||||
<div class="searchformcolumn">
|
||||
{{- $q := "" }}
|
||||
{{- if $model.parameters.Query -}}
|
||||
{{- q = $model.parameters.Query -}}
|
||||
{{- end -}}
|
||||
{{ template "_searchboxsimple" Arr $model.parameters.Parameters true $q }}
|
||||
<fieldset class="selectgroup">
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="number" id="number" checked />
|
||||
<label for="number">Almanach-Nr.</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="title" id="title" checked />
|
||||
<label for="title">Titelinformationen</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="entry" id="entry" checked />
|
||||
<label for="entry">Bandtitel</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="person" id="person" checked />
|
||||
<label for="person">Personen & Pseudonyme</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="annotations" id="annotations" checked />
|
||||
<label for="annotations">Anmerkungen</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
{{ template "infotextsimple" true }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{- template "_fieldscript" -}}
|
||||
1
views/routes/suche/beitraege/head.gohtml
Normal file
1
views/routes/suche/beitraege/head.gohtml
Normal file
@@ -0,0 +1 @@
|
||||
<title>{{ .site.title }}: Suche – Beiträge</title>
|
||||
17
views/routes/suche/components/_fieldscript.gohtml
Normal file
17
views/routes/suche/components/_fieldscript.gohtml
Normal file
@@ -0,0 +1,17 @@
|
||||
<script type="module">
|
||||
let fieldset = document.querySelector("fieldset.selectgroup");
|
||||
let checkboxes = Array.from(fieldset.querySelectorAll('input[type="checkbox"]'));
|
||||
fieldset.addEventListener("change", (event) => {
|
||||
let target = event.target;
|
||||
if (target.type === "checkbox") {
|
||||
let name = target.name;
|
||||
let checked = target.checked;
|
||||
if (!checked) {
|
||||
let allchecked = checkboxes.filter((checkbox) => checkbox.checked);
|
||||
if (allchecked.length === 0) {
|
||||
target.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
53
views/routes/suche/components/_heading.gohtml
Normal file
53
views/routes/suche/components/_heading.gohtml
Normal file
@@ -0,0 +1,53 @@
|
||||
{{- $model := . -}}
|
||||
<div id="searchheading" class="flex flex-row justify-between min-h-14 items-end relative">
|
||||
<nav id="searchnav" class="flex flex-row items-end">
|
||||
<div
|
||||
class="align-bottom text-lg h-min self-end pb-0.5 italic font-bold
|
||||
text-zinc-800">
|
||||
Suche nach:
|
||||
</div>
|
||||
<!--
|
||||
<a
|
||||
href="/suche/reihen"
|
||||
class="block no-underline"
|
||||
{{ if eq $model.Collection "reihen" }}aria-current="page"{{- end -}}
|
||||
>Reihen</a
|
||||
>
|
||||
-->
|
||||
<a
|
||||
href="/suche/baende"
|
||||
class="block no-underline"
|
||||
{{ if eq $model.Collection "baende" }}aria-current="page"{{- end -}}
|
||||
>Bänden</a
|
||||
>
|
||||
<a
|
||||
href="/suche/beitraege"
|
||||
class="block no-underline"
|
||||
{{ if eq $model.Collection "beitraege" }}aria-current="page"{{- end -}}
|
||||
>Beiträgen</a
|
||||
>
|
||||
<!--
|
||||
<a
|
||||
href="/suche/personen"
|
||||
class="block no-underline"
|
||||
{{ if eq $model.Collection "personen" }}aria-current="page"{{- end -}}
|
||||
>Personen</a
|
||||
>
|
||||
-->
|
||||
</nav>
|
||||
<h1
|
||||
class="text-3xl font-bold px-3 relative translate-y-[45%] w-min whitespace-nowrap
|
||||
bg-stone-50 mr-24 z-20">
|
||||
Suche · <span class="">
|
||||
{{- if eq $model.Collection "reihen" -}}
|
||||
Reihen
|
||||
{{- else if eq $model.Collection "personen" -}}
|
||||
Personen & Körperschaften
|
||||
{{- else if eq $model.Collection "baende" -}}
|
||||
Bände
|
||||
{{- else if eq $model.Collection "beitraege" -}}
|
||||
Beiträge
|
||||
{{- end -}}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
@@ -1,10 +1,11 @@
|
||||
{{ $model := index . 0 }}
|
||||
{{ $parameters := index . 0 }}
|
||||
{{ $extendable := index . 1 }}
|
||||
{{ $q := index . 2 }}
|
||||
|
||||
|
||||
<label for="q" class="hidden">Suchbegriffe</label>
|
||||
<input
|
||||
{{ if $model.q }}value="{{ $model.q }}"{{- end -}}
|
||||
{{ if $q }}value="{{ $q }}"{{- end -}}
|
||||
type="search"
|
||||
name="q"
|
||||
minlength="3"
|
||||
@@ -16,7 +17,7 @@
|
||||
|
||||
{{ if $extendable }}
|
||||
<a
|
||||
href="/suche/{{ $model.type }}?extended=true"
|
||||
href="/suche/{{ $parameters.Collection }}?extended=true"
|
||||
class="whitespace-nowrap self-end block col-span-2">
|
||||
<i class="ri-arrow-right-long-line"></i> Erweiterte Suche
|
||||
</a>
|
||||
@@ -97,44 +97,6 @@
|
||||
{{- else if eq $model.type "baende" -}}
|
||||
<!-- INFO: Bände -->
|
||||
{{- if not $model.extended -}}
|
||||
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
|
||||
{{ template "searchboxsimple" Arr . true }}
|
||||
<fieldset class="selectgroup">
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="number" id="number" checked />
|
||||
<label for="number">Almanach-Nr.</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="title" id="title" checked />
|
||||
<label for="title">Titel</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="series" id="series" checked />
|
||||
<label for="series">Reihentitel</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="person" id="person" checked />
|
||||
<label for="person">Personen & Verlage</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="pubdata" id="pubdata" checked />
|
||||
<label for="pubdata">Orte</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="year" id="year" checked />
|
||||
<label for="year">Jahr</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="references" id="references" checked />
|
||||
<label for="references">Nachweise</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="annotations" id="annotations" checked />
|
||||
<label for="annotations">Anmerkungen</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
{{ template "infotextsimple" true }}
|
||||
</div>
|
||||
{{- else -}}
|
||||
Extended search Bände
|
||||
{{- end -}}
|
||||
@@ -144,28 +106,6 @@
|
||||
{{- if not $model.extended -}}
|
||||
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
|
||||
{{ template "searchboxsimple" Arr . true }}
|
||||
<fieldset class="selectgroup">
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="number" id="number" checked />
|
||||
<label for="number">Almanach-Nr.</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="title" id="title" checked />
|
||||
<label for="title">Titelinformationen</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="entry" id="entry" checked />
|
||||
<label for="entry">Bandtitel</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="person" id="person" checked />
|
||||
<label for="person">Personen & Pseudonyme</label>
|
||||
</div>
|
||||
<div class="selectgroup-option">
|
||||
<input type="checkbox" name="annotations" id="annotations" checked />
|
||||
<label for="annotations">Anmerkungen</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
{{ template "infotextsimple" true }}
|
||||
</div>
|
||||
{{- else -}}
|
||||
@@ -173,21 +113,3 @@
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
let fieldset = document.querySelector("fieldset.selectgroup");
|
||||
let checkboxes = Array.from(fieldset.querySelectorAll('input[type="checkbox"]'));
|
||||
fieldset.addEventListener("change", (event) => {
|
||||
let target = event.target;
|
||||
if (target.type === "checkbox") {
|
||||
let name = target.name;
|
||||
let checked = target.checked;
|
||||
if (!checked) {
|
||||
let allchecked = checkboxes.filter((checkbox) => checkbox.checked);
|
||||
if (allchecked.length === 0) {
|
||||
target.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
<title>{{ .site.title }}: Suche</title>
|
||||
1
views/routes/suche/reihen/head.gohtml
Normal file
1
views/routes/suche/reihen/head.gohtml
Normal file
@@ -0,0 +1 @@
|
||||
<title>{{ .site.title }}: Suche – Reihen</title>
|
||||
@@ -295,6 +295,10 @@
|
||||
@apply decoration-slate-900 line-through;
|
||||
}
|
||||
|
||||
#searchform .searchformcolumn {
|
||||
@apply grid grid-cols-12 gap-y-3 w-full gap-x-4;
|
||||
}
|
||||
|
||||
#persontype a {
|
||||
@apply px-1.5 border-b-[5px] border-transparent hover:border-zinc-200 no-underline font-serif mx-2.5;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user