Files
iptvc/app/web/templates.go
T
2026-07-19 19:14:55 +08:00

177 lines
4.4 KiB
Go

/*
* Copyright (c) 2025-2026, Антон Аксенов
* This file is part of iptvc project
* MIT License: https://git.axenov.dev/IPTV/iptvc/src/branch/master/LICENSE
*/
package web
import (
"axenov/iptv-checker/app/config"
"bytes"
"embed"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"math"
"net/http"
"strconv"
"time"
)
//go:embed views/*.html
var viewsFS embed.FS
// TemplateManager управляет загрузкой и рендерингом шаблонов
type TemplateManager struct {
templates map[string]*template.Template
funcMap template.FuncMap
}
// NewTemplateManager создаёт новый менеджер шаблонов
func NewTemplateManager(cfg *config.Config) (*TemplateManager, error) {
funcMap := template.FuncMap{
"baseUrl": func(path string) string {
base := cfg.Site.BaseUrl
if path == "" {
return base
}
return base + "/" + path
},
"formatDate": formatDate,
"formatTimeAgo": formatTimeAgo,
"statusClass": func(v PlaylistView) string {
return statusClass(&v)
},
"statusBadge": func(v PlaylistView) string {
return statusBadge(&v)
},
"jsonChannels": func(channels map[string]ChannelView) template.JS {
arr := make([]ChannelView, 0, len(channels))
for _, ch := range channels {
arr = append(arr, ch)
}
b, _ := json.Marshal(arr)
return template.JS(b)
},
"seq": func(start, end int) []int {
result := make([]int, 0, end-start+1)
for i := start; i <= end; i++ {
result = append(result, i)
}
return result
},
"sub": func(a, b int) int {
return a - b
},
"add": func(a, b int) int {
return a + b
},
"channelCount": func(v PlaylistView) int {
return len(v.Channels)
},
"groupCount": func(v PlaylistView) int {
return len(v.Groups)
},
"hasTag": containsTag,
}
tm := &TemplateManager{
templates: make(map[string]*template.Template),
funcMap: funcMap,
}
// parse base template
baseContent, err := viewsFS.ReadFile("views/base.html")
if err != nil {
return nil, fmt.Errorf("cannot read base.html: %w", err)
}
pages := []string{"list", "details", "notfound"}
for _, page := range pages {
pageContent, err := viewsFS.ReadFile(fmt.Sprintf("views/%s.html", page))
if err != nil {
return nil, fmt.Errorf("cannot read %s.html: %w", page, err)
}
tmpl := template.New("base").Funcs(funcMap)
tmpl, err = tmpl.Parse(string(baseContent))
if err != nil {
return nil, fmt.Errorf("cannot parse base template: %w", err)
}
tmpl, err = tmpl.Parse(string(pageContent))
if err != nil {
return nil, fmt.Errorf("cannot parse %s template: %w", page, err)
}
tm.templates[page] = tmpl
}
return tm, nil
}
// Render рендерит шаблон страницы
func (tm *TemplateManager) Render(w io.Writer, page string, data *PageData) {
tmpl, ok := tm.templates[page]
if !ok {
http.Error(w.(http.ResponseWriter), "Template not found", http.StatusInternalServerError)
return
}
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, "base", data); err != nil {
log.Printf("Template error (%s): %s", page, err)
http.Error(w.(http.ResponseWriter), "Template error", http.StatusInternalServerError)
return
}
w.Write(buf.Bytes())
}
// formatTimeAgo возвращает относительное время на русском
func formatTimeAgo(ts int64) string {
if ts == 0 {
return "неизвестно"
}
diff := time.Now().Unix() - ts
if diff < 0 {
diff = 0
}
if diff < 60 {
return "только что"
}
if diff < 3600 {
minutes := diff / 60
return strconv.FormatInt(minutes, 10) + " " + getNounForm(int(minutes), "минуту", "минуты", "минут") + " назад"
}
if diff < 86400 {
hours := diff / 3600
return strconv.FormatInt(hours, 10) + " " + getNounForm(int(hours), "час", "часа", "часов") + " назад"
}
if diff < 604800 {
days := diff / 86400
return strconv.FormatInt(days, 10) + " " + getNounForm(int(days), "день", "дня", "дней") + " назад"
}
return formatDate(ts)
}
// getNounForm возвращает правильную форму существительного для числительного
func getNounForm(number int, form1, form2, form5 string) string {
abs := int(math.Abs(float64(number)))
lastDigit := abs % 10
lastTwoDigits := abs % 100
if lastTwoDigits >= 11 && lastTwoDigits <= 14 {
return form5
}
if lastDigit == 1 {
return form1
}
if lastDigit >= 2 && lastDigit <= 4 {
return form2
}
return form5
}