wip6
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user