344 lines
6.3 KiB
Go
344 lines
6.3 KiB
Go
package templates
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"mal/internal/domain"
|
|
"net/url"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type genreOption struct {
|
|
Genre domain.Genre
|
|
Selected bool
|
|
StartsInactive bool
|
|
}
|
|
|
|
func dict(values ...any) (map[string]any, error) {
|
|
if len(values)%2 != 0 {
|
|
return nil, fmt.Errorf("dict expects even number of values, got %d", len(values))
|
|
}
|
|
out := make(map[string]any, len(values)/2)
|
|
for i := 0; i < len(values); i += 2 {
|
|
key, ok := values[i].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("dict key at index %d is not a string", i)
|
|
}
|
|
out[key] = values[i+1]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func jsonAttr(v any) (template.HTMLAttr, error) {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return "", fmt.Errorf("json marshal: %w", err)
|
|
}
|
|
return template.HTMLAttr(b), nil
|
|
}
|
|
|
|
func genresParams(genres []int) string {
|
|
if len(genres) == 0 {
|
|
return ""
|
|
}
|
|
var b strings.Builder
|
|
for i, g := range genres {
|
|
if i > 0 {
|
|
b.WriteByte('&')
|
|
}
|
|
b.WriteString("genres=")
|
|
b.WriteString(strconv.Itoa(g))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func browseURL(v map[string]any, overrides map[string]any) (string, error) {
|
|
values := url.Values{}
|
|
setQueryValue(values, "q", stringValue(v["Query"]))
|
|
setQueryValue(values, "type", stringValue(v["Type"]))
|
|
setQueryValue(values, "status", stringValue(v["Status"]))
|
|
setQueryValue(values, "order_by", stringValue(v["OrderBy"]))
|
|
setQueryValue(values, "sort", stringValue(v["Sort"]))
|
|
setQueryValue(values, "studio", stringValue(v["Studio"]))
|
|
if sfw, ok := v["SFW"]; ok {
|
|
values.Set("sfw", strconv.FormatBool(boolValue(sfw)))
|
|
}
|
|
for _, genre := range intSliceValue(v["Genres"]) {
|
|
values.Add("genres", strconv.Itoa(genre))
|
|
}
|
|
setQueryValue(values, "page", stringValue(v["Page"]))
|
|
|
|
for key, raw := range overrides {
|
|
switch key {
|
|
case "genres":
|
|
values.Del("genres")
|
|
for _, genre := range intSliceValue(raw) {
|
|
values.Add("genres", strconv.Itoa(genre))
|
|
}
|
|
case "sfw":
|
|
values.Set("sfw", strconv.FormatBool(boolValue(raw)))
|
|
default:
|
|
setQueryValue(values, key, stringValue(raw))
|
|
}
|
|
}
|
|
|
|
encoded := values.Encode()
|
|
if encoded == "" {
|
|
return "/browse", nil
|
|
}
|
|
return "/browse?" + encoded, nil
|
|
}
|
|
|
|
func setQueryValue(values url.Values, key string, value string) {
|
|
if value == "" {
|
|
values.Del(key)
|
|
return
|
|
}
|
|
values.Set(key, value)
|
|
}
|
|
|
|
func stringValue(v any) string {
|
|
switch value := v.(type) {
|
|
case nil:
|
|
return ""
|
|
case string:
|
|
return value
|
|
case int:
|
|
if value == 0 {
|
|
return ""
|
|
}
|
|
return strconv.Itoa(value)
|
|
case int64:
|
|
if value == 0 {
|
|
return ""
|
|
}
|
|
return strconv.FormatInt(value, 10)
|
|
case float64:
|
|
if value == 0 {
|
|
return ""
|
|
}
|
|
return strconv.Itoa(int(value))
|
|
default:
|
|
return fmt.Sprint(v)
|
|
}
|
|
}
|
|
|
|
func boolValue(v any) bool {
|
|
switch value := v.(type) {
|
|
case bool:
|
|
return value
|
|
case string:
|
|
return value == "true"
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func intSliceValue(v any) []int {
|
|
switch value := v.(type) {
|
|
case []int:
|
|
return value
|
|
case []int64:
|
|
out := make([]int, 0, len(value))
|
|
for _, item := range value {
|
|
out = append(out, int(item))
|
|
}
|
|
return out
|
|
case []any:
|
|
out := make([]int, 0, len(value))
|
|
for _, item := range value {
|
|
n := toInt(item)
|
|
if n > 0 {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
return out
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func hasGenre(id int, genres []int) bool {
|
|
return slices.Contains(genres, id)
|
|
}
|
|
|
|
func orderedGenreOptions(genres []domain.Genre, selectedGenres []int) []genreOption {
|
|
selected := make(map[int]struct{}, len(selectedGenres))
|
|
for _, genreID := range selectedGenres {
|
|
selected[genreID] = struct{}{}
|
|
}
|
|
|
|
out := make([]genreOption, 0, len(genres))
|
|
inactive := make([]genreOption, 0, len(genres))
|
|
for _, genre := range genres {
|
|
_, isSelected := selected[genre.MalID]
|
|
option := genreOption{Genre: genre, Selected: isSelected}
|
|
if isSelected {
|
|
out = append(out, option)
|
|
continue
|
|
}
|
|
inactive = append(inactive, option)
|
|
}
|
|
|
|
if len(out) > 0 && len(inactive) > 0 {
|
|
inactive[0].StartsInactive = true
|
|
}
|
|
|
|
return append(out, inactive...)
|
|
}
|
|
|
|
func div(a, b float64) float64 {
|
|
if b == 0 {
|
|
return 0
|
|
}
|
|
return a / b
|
|
}
|
|
|
|
func ceilDiv(a, b int) int {
|
|
if b == 0 {
|
|
return 0
|
|
}
|
|
return (a + b - 1) / b
|
|
}
|
|
|
|
func idiv(a, b int) int {
|
|
if b == 0 {
|
|
return 0
|
|
}
|
|
return a / b
|
|
}
|
|
|
|
func atoi(v any) int {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
n, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
func seq(v any) []int {
|
|
count := 0
|
|
switch n := v.(type) {
|
|
case int:
|
|
count = n
|
|
case int64:
|
|
if n > int64(^uint(0)>>1) {
|
|
count = 0
|
|
} else {
|
|
count = int(n)
|
|
}
|
|
}
|
|
if count <= 0 {
|
|
return []int{}
|
|
}
|
|
res := make([]int, count)
|
|
for i := 0; i < count; i++ {
|
|
res[i] = i
|
|
}
|
|
return res
|
|
}
|
|
|
|
func toInt(v any) int {
|
|
switch n := v.(type) {
|
|
case int:
|
|
return n
|
|
case int64:
|
|
if n > int64(^uint(0)>>1) {
|
|
return 0
|
|
}
|
|
return int(n)
|
|
case float64:
|
|
if n > float64(int(^uint(0)>>1)) {
|
|
return 0
|
|
}
|
|
return int(n)
|
|
case string:
|
|
i, err := strconv.Atoi(n)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return i
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func percent(current, total float64) float64 {
|
|
if total == 0 {
|
|
return 0
|
|
}
|
|
return (current / total) * 100
|
|
}
|
|
|
|
func formatNumber(n int) string {
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
s := fmt.Sprintf("%d", n)
|
|
var parts []string
|
|
for i := len(s); i > 0; i -= 3 {
|
|
start := max(i-3, 0)
|
|
parts = append([]string{s[start:i]}, parts...)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func formatDate(dateStr string) string {
|
|
t, err := time.Parse(time.RFC3339, dateStr)
|
|
if err != nil {
|
|
t, err = time.Parse("2006-01-02T15:04:05+00:00", dateStr)
|
|
if err != nil {
|
|
return dateStr
|
|
}
|
|
}
|
|
return t.Format("Jan 2, 2006")
|
|
}
|
|
|
|
func nextSort(sort string) string {
|
|
if sort == "asc" {
|
|
return "desc"
|
|
}
|
|
return "asc"
|
|
}
|
|
|
|
func posterURL(webp, jpg any, width, height int) string {
|
|
if webp != nil {
|
|
if s, ok := webp.(string); ok && s != "" {
|
|
return s
|
|
}
|
|
}
|
|
if jpg != nil {
|
|
if s, ok := jpg.(string); ok && s != "" {
|
|
return s
|
|
}
|
|
}
|
|
return fmt.Sprintf("https://placehold.co/%dx%d?text=No+Image", width, height)
|
|
}
|
|
|
|
func episodeRangeStart(epNum, step int) int {
|
|
if epNum < 1 || step < 1 {
|
|
return 1
|
|
}
|
|
return ((epNum-1)/step)*step + 1
|
|
}
|
|
|
|
func assetURL(assetPath string) string {
|
|
info, err := os.Stat(strings.TrimPrefix(assetPath, "/"))
|
|
if err != nil {
|
|
return assetPath
|
|
}
|
|
|
|
values := url.Values{}
|
|
values.Set("v", fmt.Sprintf("%d-%d", info.ModTime().Unix(), info.Size()))
|
|
return assetPath + "?" + values.Encode()
|
|
}
|