HUGE refactor. working towards web UI

This commit is contained in:
ari melody 2026-06-08 02:49:39 +01:00
parent dd54e8cc49
commit 1f94eecca9
Signed by: ari
GPG key ID: 60B5F0386E3DDB7E
29 changed files with 1669 additions and 162 deletions

59
templates/html.go Normal file
View file

@ -0,0 +1,59 @@
package templates
import (
_ "embed"
"html/template"
"strings"
"time"
)
//go:embed "html/layout.html"
var layoutHTML string
//go:embed "html/index.html"
var indexHTML string
//go:embed "html/vod-list.html"
var vodListHTML string
//go:embed "html/vod.html"
var vodHTML string
type (
Crumb struct {
Name string
Path string
}
)
var templateFuncs = template.FuncMap{
"FormatTime": func (time time.Time, format string) string {
return time.Format(format)
},
"ToLower": func (str string) string {
return strings.ToLower(str)
},
"ToUpper": func (str string) string {
return strings.ToUpper(str)
},
"Crumbs": func (path string) []Crumb {
split := strings.Split(strings.TrimSuffix(path, "/"), "/")
crumbs := make([]Crumb, len(split))
for i := range crumbs {
segment := split[i]
crumbs[i] = Crumb{
segment,
strings.Repeat("../", len(crumbs)-i-1),
}
}
return crumbs
},
}
var BaseTemplate = template.Must(template.New("base").Funcs(templateFuncs).Parse(
strings.Join([]string{
layoutHTML,
// headerHTML,
// footerHTML,
}, "\n"),
))
var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(indexHTML))
var VODListTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(vodListHTML))
var VODTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(vodHTML))

18
templates/html/index.html Normal file
View file

@ -0,0 +1,18 @@
{{define "head"}}
<title>Melody VOD Manager</title>
<link rel="stylesheet" href="/style/index.css">
{{end}}
{{define "content"}}
<main>
<h1>Melody VOD Manager</h1>
<a href="/vods">Vods</a>
<p>Templates:</p>
<ul>
<li><a href="/templates/title">Title</a></li>
<li><a href="/templates/description">Description</a></li>
<li><a href="/templates/tags">Tags</a></li>
</ul>
</main>
{{end}}

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{block "head" .}}{{end}}
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/vods">Vods</a>
</nav>
</header>
{{block "content" .}}
<main>
<h1>hello, world!</h1>
<p>
this is a template page.
you probably shouldn't be seeing this!
</p>
</main>
{{end}}
</body>
</html>

View file

@ -0,0 +1,55 @@
{{define "head"}}
<title>Listing "{{.Directory}}" - Melody VOD Manager</title>
<link rel="stylesheet" href="/style/vod-list.css">
{{end}}
{{define "content"}}
<main>
<h1>VODs</h1>
<nav>&rarr; {{range Crumbs .Directory}}<a href="./{{.Path}}">{{.Name}}</a>/{{end}}</nav>
<table>
<thead>
<tr>
<th colspan="2">Name</th>
<th>Size</th>
</tr>
</thead>
<tbody>
{{range $VOD := .VODs}}
<tr>
<td>📼</td>
<td><a href="/vods{{$.Directory}}/{{$VOD.Path}}">{{$VOD.Title}}</a></td>
<td>&mdash;</td>
</tr>
{{end}}
{{range $Name := .Directories}}
<tr>
<td>📁</td>
<td><a href="/vods{{$.Directory}}/{{$Name}}">{{$Name}}</a></td>
<td>&mdash;</td>
</tr>
{{end}}
{{range $File := .Files}}
<tr>
<td>📄</td>
<td><a href="/vods{{$.Directory}}/{{$File.Name}}">{{$File.Name}}</a></td>
<td>{{$File.Size}}</td>
</tr>
{{end}}
</tbody>
</table>
<h2>Actions</h2>
<div class="actions">
<p>
<a href="/vods{{.Directory}}?init=true">Initialise as VOD Directory</a>
&mdash; Create a <code>metadata.toml</code> file here, initialising this directory as a VOD.
</p>
</div>
</main>
{{end}}

72
templates/html/vod.html Normal file
View file

@ -0,0 +1,72 @@
{{define "head"}}
<title>{{.Title}}</title>
<link rel="stylesheet" href="/style/vod.css">
{{end}}
{{define "content"}}
<main>
<h1>VOD Details</h1>
<nav>&rarr; {{range Crumbs .Directory}}<a href="./{{.Path}}">{{.Name}}</a>/{{end}}</nav>
<form method="POST">
<div class="metadata-top">
<div>
<label for="title">Title</label>
<input type="text" name="title" value="{{.Title}}" />
</div>
<div>
<label for="part">Part</label>
<input type="number" min="0" name="part" value="{{.Part}}" />
</div>
<div>
<label for="date">Date</label>
<input type="date" name="date" value="{{FormatTime .Date "2006-01-02"}}" />
</div>
</div>
<p>Formatted Title: <strong>{{.TitleFormatted}}</strong></p>
<p>Uploaded: <span id="upload-state">{{if .Uploaded}}Yes{{else}}No{{end}}</span></p>
<p>Tags:</p>
<ul>
{{range .Tags}}
<li><code>{{.}}</code></li>
{{end}}
</ul>
<p>Category:</p>
<blockquote>
<label for="category-enabled">Enabled</label>
<input type="checkbox" name="category-enabled" {{if .Category}}checked{{end}} />
<label for="category-name">Name</label>
<input type="text" name="category-name" value="{{if ne .Category nil}}{{.Category.Name}}{{end}}" />
<label for="category-type">Type</label>
<select name="category-type">
<option value="entertainment" {{if ne .Category nil}}{{if eq .Category.Type "entertainment"}}selected{{end}}{{end}}>Entertainment</option>
<option value="gaming" {{if ne .Category nil}}{{if eq .Category.Type "gaming"}}selected{{end}}{{end}}>Gaming</option>
</select>
<label for="category-url">URL</label>
<input type="text" name="category-url" value="{{if ne .Category nil}}{{.Category.Url}}{{end}}" />
</blockquote>
<p>Formatted Description:</p>
<p><pre class="description">{{.DescriptionFormatted}}</pre></p>
<button type="submit">Save</button>
</form>
<hr>
<p>Footage Directory: <code>"{{.FootageDir}}"</code></p>
<p>Footage Blocks:</p>
<ul>
{{range .Footage}}
<li><code>{{.Name}} ({{.Size}})</code></li>
{{end}}
</ul>
</main>
<script type="module" src="/script/vod.js" defer></script>
{{end}}

108
templates/text.go Normal file
View file

@ -0,0 +1,108 @@
package templates
import (
"context"
"fmt"
"log/slog"
"os"
"path"
"strings"
"text/template"
"time"
"arimelody.space/melody-vod-manager/scanner"
)
type (
TextTemplates struct {
Title *template.Template
Description *template.Template
DefaultTags []string
}
TextTemplateData struct {
Title string
Part uint
Date time.Time
Category *scanner.Category
}
)
const DefaultTitleTemplate =
"{{.Title}} - {{FormatTime .Date \"02 Jan 2006\"}}"
const DefaultDescriptionTemplate =
"Streamed on {{FormatTime .Date \"02 January 2006\"}}"
func FetchTemplates(ctx context.Context, templateDir string) (*TextTemplates, error) {
tmpl := TextTemplates{}
logger := slog.Default().WithGroup("TEMPLATE")
var tagsPath = path.Join(templateDir, "tags.txt")
var titlePath = path.Join(templateDir, "title.txt")
var descriptionPath = path.Join(templateDir, "description.txt")
// tags
if tagsFile, err := os.ReadFile(tagsPath); err == nil {
tmpl.DefaultTags = strings.Split(string(tagsFile), "\n")
} else {
if !os.IsNotExist(err) { return nil, err }
logger.Info(fmt.Sprintf(
"%s not found. No default tags will be used.",
tagsPath,
))
tmpl.DefaultTags = []string{}
}
// title
titleTemplate := template.New("title").Funcs(templateFuncs)
if titleFile, err := os.ReadFile(titlePath); err == nil {
tmpl.Title, err = titleTemplate.Parse(string(titleFile))
if err != nil {
return nil, fmt.Errorf("failed to parse title template: %v", err)
}
} else {
if !os.IsNotExist(err) { return nil, err }
logger.Info(
fmt.Sprintf(
"%s not found. Falling back to default template:",
titlePath,
),
"template",
DefaultTitleTemplate,
)
tmpl.Title, err = titleTemplate.Parse(DefaultTitleTemplate)
if err != nil { panic(err) }
os.WriteFile(titlePath, []byte(DefaultTitleTemplate), 0644)
}
// description
descriptionTemplate := template.New("description").Funcs(templateFuncs)
if descriptionFile, err := os.ReadFile(descriptionPath); err == nil {
tmpl.Description, err = descriptionTemplate.Parse(string(descriptionFile))
if err != nil {
return nil, fmt.Errorf("failed to parse description template: %v", err)
}
} else {
if !os.IsNotExist(err) { return nil, err }
logger.Info(
fmt.Sprintf(
"%s not found. Falling back to default template:",
descriptionPath,
),
"template",
DefaultDescriptionTemplate,
)
tmpl.Description, err = descriptionTemplate.Parse(DefaultDescriptionTemplate)
if err != nil { panic(err) }
os.WriteFile(descriptionPath, []byte(DefaultDescriptionTemplate), 0644)
}
return &tmpl, nil
}