Improved the datetimes on the log pages.

Added an experimental template fragment optimisation.
The template generator can handle time.Time
The forum and profile templates now have guest and member variants generated for them.
Interpreted templates are no longer loaded, if there's a generated version of it.

Added absolute time on hover to the topic, topics, forum, and forums templates.
We now use lang instead of index in the stylesheets for phrases.
Renamed the .trash_label CSS class to .delete_label
Use the new toArr and concat template functions to reduce the amount of boilerplate in the theme stylesheets.
Removed bits of redundant code here and there in the stylesheets.
Added a .CurrentUser.Loggedin to profiles to make them slightly faster.
Shortened some themeStmt names.
Moved GzipResponseWriter, theme.RunTmpl and theme.GetTmpl from theme_list.go to theme.go
The fallback theme now falls back onto the last theme loaded, if the fallback theme doesn't exist.
Added the abstime template function for formatting absolute times a little more nicely.

Began work on the login logs.

Removed the alerts_no_new_alerts phrase.
Renamed the forums_topics_suffix phrase to forums.topics_suffix.
This commit is contained in:
Azareal 2018-12-14 14:08:53 +10:00
parent 7abd3220de
commit bdf7fa40d5
29 changed files with 416 additions and 269 deletions

View File

@ -241,13 +241,14 @@ func seedTables(adapter qgen.Adapter) error {
return nil return nil
} }
func copyInsertMap(in map[string]interface{}) (out map[string]interface{}) { // ? - What is this for?
/*func copyInsertMap(in map[string]interface{}) (out map[string]interface{}) {
out = make(map[string]interface{}) out = make(map[string]interface{})
for col, value := range in { for col, value := range in {
out[col] = value out[col] = value
} }
return out return out
} }*/
type LitStr string type LitStr string

View File

@ -493,6 +493,22 @@ func createTables(adapter qgen.Adapter) error {
}, },
) )
// TODO: Implement this
/*
qgen.Install.CreateTable("login_logs", "", "",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"lid", "int", 0, false, true, ""},
qgen.DBTableColumn{"uid", "int", 0, false, false, ""},
qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""},
},
[]qgen.DBTableKey{
qgen.DBTableKey{"lid", "primary"},
},
)
*/
qgen.Install.CreateTable("moderation_logs", "", "", qgen.Install.CreateTable("moderation_logs", "", "",
[]qgen.DBTableColumn{ []qgen.DBTableColumn{
qgen.DBTableColumn{"action", "varchar", 100, false, false, ""}, qgen.DBTableColumn{"action", "varchar", 100, false, false, ""},

View File

@ -2,6 +2,7 @@ package common
import ( import (
"database/sql" "database/sql"
"time"
"github.com/Azareal/Gosora/query_gen" "github.com/Azareal/Gosora/query_gen"
) )
@ -55,10 +56,12 @@ func (store *SQLModLogStore) GlobalCount() (logCount int) {
func buildLogList(rows *sql.Rows) (logs []LogItem, err error) { func buildLogList(rows *sql.Rows) (logs []LogItem, err error) {
for rows.Next() { for rows.Next() {
var log LogItem var log LogItem
err := rows.Scan(&log.Action, &log.ElementID, &log.ElementType, &log.IPAddress, &log.ActorID, &log.DoneAt) var doneAt time.Time
err := rows.Scan(&log.Action, &log.ElementID, &log.ElementType, &log.IPAddress, &log.ActorID, &doneAt)
if err != nil { if err != nil {
return logs, err return logs, err
} }
log.DoneAt = doneAt.Format("2006-01-02 15:04:05")
logs = append(logs, log) logs = append(logs, log)
} }
return logs, rows.Err() return logs, rows.Err()

View File

@ -1,7 +1,11 @@
package common package common
import "database/sql" import (
import "github.com/Azareal/Gosora/query_gen" "database/sql"
"time"
"github.com/Azareal/Gosora/query_gen"
)
var RegLogs RegLogStore var RegLogs RegLogStore
@ -83,10 +87,12 @@ func (store *SQLRegLogStore) GetOffset(offset int, perPage int) (logs []RegLogIt
for rows.Next() { for rows.Next() {
var log RegLogItem var log RegLogItem
err := rows.Scan(&log.ID, &log.Username, &log.Email, &log.FailureReason, &log.Success, &log.IPAddress, &log.DoneAt) var doneAt time.Time
err := rows.Scan(&log.ID, &log.Username, &log.Email, &log.FailureReason, &log.Success, &log.IPAddress, &doneAt)
if err != nil { if err != nil {
return logs, err return logs, err
} }
log.DoneAt = doneAt.Format("2006-01-02 15:04:05")
logs = append(logs, log) logs = append(logs, log)
} }
return logs, rows.Err() return logs, rows.Err()

View File

@ -108,7 +108,8 @@ func InitPhrases(lang string) error {
for index, phraseName := range phraseNames { for index, phraseName := range phraseNames {
phrase, ok := langPack.TmplPhrases[phraseName] phrase, ok := langPack.TmplPhrases[phraseName]
if !ok { if !ok {
log.Print("Couldn't find template phrase '" + phraseName + "'") log.Printf("langPack.TmplPhrases: %+v\n", langPack.TmplPhrases)
panic("Couldn't find template phrase '" + phraseName + "'")
} }
phraseSet[index] = []byte(phrase) phraseSet[index] = []byte(phrase)
} }

View File

@ -16,10 +16,19 @@ import (
"github.com/Azareal/Gosora/common/templates" "github.com/Azareal/Gosora/common/templates"
) )
var Ctemplates []string var Ctemplates []string // TODO: Use this to filter out top level templates we don't need
var Templates = template.New("") var Templates = template.New("")
var PrebuildTmplList []func(User, *Header) CTmpl var PrebuildTmplList []func(User, *Header) CTmpl
func skipCTmpl(key string) bool {
for _, tmpl := range Ctemplates {
if strings.HasSuffix(key, "/"+tmpl+".html") {
return true
}
}
return false
}
type CTmpl struct { type CTmpl struct {
Name string Name string
Filename string Filename string
@ -65,6 +74,8 @@ var Template_forum_handle = func(pi ForumPage, w io.Writer) error {
} }
return Templates.ExecuteTemplate(w, mapping+".html", pi) return Templates.ExecuteTemplate(w, mapping+".html", pi)
} }
var Template_forum_guest_handle = Template_forum_handle
var Template_forum_member_handle = Template_forum_handle
// nolint // nolint
var Template_forums_handle = func(pi ForumsPage, w io.Writer) error { var Template_forums_handle = func(pi ForumsPage, w io.Writer) error {
@ -83,6 +94,8 @@ var Template_profile_handle = func(pi ProfilePage, w io.Writer) error {
} }
return Templates.ExecuteTemplate(w, mapping+".html", pi) return Templates.ExecuteTemplate(w, mapping+".html", pi)
} }
var Template_profile_guest_handle = Template_profile_handle
var Template_profile_member_handle = Template_profile_handle
// nolint // nolint
var Template_create_topic_handle = func(pi CreateTopicPage, w io.Writer) error { var Template_create_topic_handle = func(pi CreateTopicPage, w io.Writer) error {
@ -245,7 +258,7 @@ func CompileTemplates() error {
varList = make(map[string]tmpl.VarItem) varList = make(map[string]tmpl.VarItem)
header.Title = "User 526" header.Title = "User 526"
ppage := ProfilePage{header, replyList, user, 0, 0} // TODO: Use the score from user to generate the currentScore and nextScore ppage := ProfilePage{header, replyList, user, 0, 0} // TODO: Use the score from user to generate the currentScore and nextScore
profileTmpl, err := compile("profile", "common.ProfilePage", ppage) profileTmpl, err := compileByLoggedin("profile", "common.ProfilePage", ppage)
if err != nil { if err != nil {
return err return err
} }
@ -284,7 +297,7 @@ func CompileTemplates() error {
forumItem := BlankForum(1, "general-forum.1", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0) forumItem := BlankForum(1, "general-forum.1", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0)
header.Title = "General Forum" header.Title = "General Forum"
forumPage := ForumPage{header, topicsList, forumItem, Paginator{[]int{1}, 1, 1}} forumPage := ForumPage{header, topicsList, forumItem, Paginator{[]int{1}, 1, 1}}
forumTmpl, err := compile("forum", "common.ForumPage", forumPage) forumTmpl, err := compileByLoggedin("forum", "common.ForumPage", forumPage)
if err != nil { if err != nil {
return err return err
} }
@ -495,7 +508,21 @@ func writeTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string)
getterstr += "\treturn " + templateName + "_frags\n" getterstr += "\treturn " + templateName + "_frags\n"
} }
getterstr += "}\nreturn nil\n}\n" getterstr += "}\nreturn nil\n}\n"
out += "\n// nolint\nfunc init() {\n" + c.FragOut + "\n" + getterstr + "}\n" out += "\n// nolint\nfunc init() {\n"
var bodyMap = make(map[string]string) //map[body]fragmentPrefix
for _, frag := range c.FragOut {
var fragmentPrefix string
front := frag.TmplName + "_frags[" + strconv.Itoa(frag.Index) + "]"
fp, ok := bodyMap[frag.Body]
if !ok {
fragmentPrefix = front + " = []byte(`" + frag.Body + "`)\n"
bodyMap[frag.Body] = front
} else {
fragmentPrefix = front + " = " + fp + "\n"
}
out += fragmentPrefix
}
out += "\n" + getterstr + "}\n"
err := writeFile(prefix+"template_list.go", out) err := writeFile(prefix+"template_list.go", out)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -573,6 +600,17 @@ func InitTemplates() error {
return template.HTML(phrases.GetTmplPhrase(phraseName)) return template.HTML(phrases.GetTmplPhrase(phraseName))
} }
// TODO: Implement this in the template generator too
fmap["langf"] = func(phraseNameInt interface{}, args ...interface{}) interface{} {
phraseName, ok := phraseNameInt.(string)
if !ok {
panic("phraseNameInt is not a string")
}
// TODO: Log non-existent phrases?
// TODO: Optimise TmplPhrasef so we don't use slow Sprintf there
return template.HTML(phrases.GetTmplPhrasef(phraseName, args...))
}
fmap["level"] = func(levelInt interface{}) interface{} { fmap["level"] = func(levelInt interface{}) interface{} {
level, ok := levelInt.(int) level, ok := levelInt.(int)
if !ok { if !ok {
@ -581,6 +619,15 @@ func InitTemplates() error {
return template.HTML(phrases.GetLevelPhrase(level)) return template.HTML(phrases.GetLevelPhrase(level))
} }
fmap["abstime"] = func(timeInt interface{}) interface{} {
time, ok := timeInt.(time.Time)
if !ok {
panic("timeInt is not a time.Time")
}
//return time.String()
return time.Format("2006-01-02 15:04:05")
}
fmap["scope"] = func(name interface{}) interface{} { fmap["scope"] = func(name interface{}) interface{} {
return "" return ""
} }
@ -606,6 +653,10 @@ func InitTemplates() error {
for index, path := range templateFiles { for index, path := range templateFiles {
path = strings.Replace(path, "\\", "/", -1) path = strings.Replace(path, "\\", "/", -1)
log.Print("templateFile: ", path) log.Print("templateFile: ", path)
if skipCTmpl(path) {
log.Print("skipping")
continue
}
templateFileMap[path] = index templateFileMap[path] = index
} }
@ -616,6 +667,10 @@ func InitTemplates() error {
for _, path := range overrideFiles { for _, path := range overrideFiles {
path = strings.Replace(path, "\\", "/", -1) path = strings.Replace(path, "\\", "/", -1)
log.Print("overrideFile: ", path) log.Print("overrideFile: ", path)
if skipCTmpl(path) {
log.Print("skipping")
continue
}
index, ok := templateFileMap["templates/"+strings.TrimPrefix(path, "templates/overrides/")] index, ok := templateFileMap["templates/"+strings.TrimPrefix(path, "templates/overrides/")]
if !ok { if !ok {
log.Print("not ok: templates/" + strings.TrimPrefix(path, "templates/overrides/")) log.Print("not ok: templates/" + strings.TrimPrefix(path, "templates/overrides/"))

View File

@ -49,7 +49,7 @@ type CTemplateSet struct {
TemplateFragmentCount map[string]int TemplateFragmentCount map[string]int
FragOnce map[string]bool FragOnce map[string]bool
fragmentCursor map[string]int fragmentCursor map[string]int
FragOut string FragOut []OutFrag
fragBuf []Fragment fragBuf []Fragment
varList map[string]VarItem varList map[string]VarItem
localVars map[string]map[string]VarItemReflect localVars map[string]map[string]VarItemReflect
@ -88,9 +88,11 @@ func NewCTemplateSet() *CTemplateSet {
"dock": true, "dock": true,
"elapsed": true, "elapsed": true,
"lang": true, "lang": true,
"level": true, //"langf":true,
"scope": true, "level": true,
"dyntmpl": true, "abstime": true,
"scope": true,
"dyntmpl": true,
}, },
} }
} }
@ -124,6 +126,12 @@ type Skipper struct {
Index int Index int
} }
type OutFrag struct {
TmplName string
Index int
Body string
}
func (c *CTemplateSet) CompileByLoggedin(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (stub string, gout string, mout string, err error) { func (c *CTemplateSet) CompileByLoggedin(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (stub string, gout string, mout string, err error) {
c.importMap = map[string]string{} c.importMap = map[string]string{}
for index, item := range c.baseImportMap { for index, item := range c.baseImportMap {
@ -366,9 +374,8 @@ func (c *CTemplateSet) compile(name string, content, expects string, expectsInt
fout += "return nil\n}\n" fout += "return nil\n}\n"
var writeFrag = func(tmplName string, index int, body string) { var writeFrag = func(tmplName string, index int, body string) {
fragmentPrefix := tmplName + "_frags[" + strconv.Itoa(index) + "]" + " = []byte(`" + body + "`)\n" //c.detail("writing ", fragmentPrefix)
c.detail("writing ", fragmentPrefix) c.FragOut = append(c.FragOut, OutFrag{tmplName, index, body})
c.FragOut += fragmentPrefix
} }
for _, frag := range c.fragBuf { for _, frag := range c.fragBuf {
@ -925,6 +932,7 @@ ArgLoop:
notident = true notident = true
con.PushPhrase(len(c.langIndexToName) - 1) con.PushPhrase(len(c.langIndexToName) - 1)
break ArgLoop break ArgLoop
// TODO: Implement langf
case "level": case "level":
// TODO: Implement level literals // TODO: Implement level literals
leftOperand := node.Args[pos+1].String() leftOperand := node.Args[pos+1].String()
@ -936,6 +944,17 @@ ArgLoop:
litString("phrases.GetLevelPhrase("+leftParam+")", false) litString("phrases.GetLevelPhrase("+leftParam+")", false)
c.importMap[langPkg] = langPkg c.importMap[langPkg] = langPkg
break ArgLoop break ArgLoop
case "abstime":
// TODO: Implement level literals
leftOperand := node.Args[pos+1].String()
if len(leftOperand) == 0 {
panic("The leftoperand for function abstime cannot be left blank")
}
leftParam, _ := c.compileIfVarSub(con, leftOperand)
// TODO: Refactor this
litString(leftParam+".Format(\"2006-01-02 15:04:05\")", false)
c.importMap["time"] = "time"
break ArgLoop
case "scope": case "scope":
literal = true literal = true
break ArgLoop break ArgLoop
@ -1255,6 +1274,19 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
case reflect.Int64: case reflect.Int64:
c.importMap["strconv"] = "strconv" c.importMap["strconv"] = "strconv"
base = "[]byte(strconv.FormatInt(" + varname + ", 10))" base = "[]byte(strconv.FormatInt(" + varname + ", 10))"
case reflect.Struct:
// TODO: Avoid clashing with other packages which have structs named Time
if val.Type().Name() == "Time" {
c.importMap["time"] = "time"
base = "[]byte(" + varname + ".String())"
} else {
if !val.IsValid() {
panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")
}
fmt.Println("Unknown Struct Name:", varname)
fmt.Println("Unknown Struct:", val.Type().Name())
panic("-- I don't know what this variable's type is o.o\n")
}
default: default:
if !val.IsValid() { if !val.IsValid() {
panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?") panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")

View File

@ -12,6 +12,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"text/template" "text/template"
@ -50,6 +51,7 @@ type Theme struct {
RunOnDock func(string) string //(dock string) (sbody string) RunOnDock func(string) string //(dock string) (sbody string)
// This variable should only be set and unset by the system, not the theme meta file // This variable should only be set and unset by the system, not the theme meta file
// TODO: Should we phase out Active and make the default theme store the primary source of truth?
Active bool Active bool
} }
@ -95,6 +97,16 @@ func (theme *Theme) LoadStaticFiles() error {
} }
return phrase return phrase
} }
fmap["toArr"] = func(args ...interface{}) []interface{} {
return args
}
fmap["concat"] = func(args ...interface{}) interface{} {
var out string
for _, arg := range args {
out += arg.(string)
}
return out
}
theme.ResourceTemplates.Funcs(fmap) theme.ResourceTemplates.Funcs(fmap)
template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + theme.Name + "/public/*.css")) template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + theme.Name + "/public/*.css"))
@ -128,6 +140,7 @@ func (theme *Theme) AddThemeStaticFiles() error {
// TODO: Prepare resource templates for each loaded langpack? // TODO: Prepare resource templates for each loaded langpack?
err = theme.ResourceTemplates.ExecuteTemplate(&b, filename, CSSData{Phrases: phraseMap}) err = theme.ResourceTemplates.ExecuteTemplate(&b, filename, CSSData{Phrases: phraseMap})
if err != nil { if err != nil {
log.Print("Failed in adding static file '" + path + "' for default theme '" + theme.Name + "'")
return err return err
} }
data = b.Bytes() data = b.Bytes()
@ -267,16 +280,16 @@ func (theme *Theme) MapTemplates() {
func (theme *Theme) setActive(active bool) error { func (theme *Theme) setActive(active bool) error {
var sink bool var sink bool
err := themeStmts.isThemeDefault.QueryRow(theme.Name).Scan(&sink) err := themeStmts.isDefault.QueryRow(theme.Name).Scan(&sink)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return err return err
} }
hasTheme := err != sql.ErrNoRows hasTheme := err != sql.ErrNoRows
if hasTheme { if hasTheme {
_, err = themeStmts.updateTheme.Exec(active, theme.Name) _, err = themeStmts.update.Exec(active, theme.Name)
} else { } else {
_, err = themeStmts.addTheme.Exec(theme.Name, active) _, err = themeStmts.add.Exec(theme.Name, active)
} }
if err != nil { if err != nil {
return err return err
@ -331,3 +344,120 @@ func (theme Theme) BuildDock(dock string) (sbody string) {
} }
return "" return ""
} }
type GzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (w GzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
// NEW method of doing theme templates to allow one user to have a different theme to another. Under construction.
// TODO: Generate the type switch instead of writing it by hand
// TODO: Cut the number of types in half
func (theme *Theme) RunTmpl(template string, pi interface{}, w io.Writer) error {
// Unpack this to avoid an indirect call
gzw, ok := w.(GzipResponseWriter)
if ok {
w = gzw.Writer
}
var getTmpl = theme.GetTmpl(template)
switch tmplO := getTmpl.(type) {
case *func(CustomPagePage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(CustomPagePage), w)
case *func(TopicPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(TopicPage), w)
case *func(TopicListPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(TopicListPage), w)
case *func(ForumPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(ForumPage), w)
case *func(ForumsPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(ForumsPage), w)
case *func(ProfilePage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(ProfilePage), w)
case *func(CreateTopicPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(CreateTopicPage), w)
case *func(IPSearchPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(IPSearchPage), w)
case *func(AccountDashPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(AccountDashPage), w)
case *func(ErrorPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(ErrorPage), w)
case *func(Page, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(Page), w)
case func(CustomPagePage, io.Writer) error:
return tmplO(pi.(CustomPagePage), w)
case func(TopicPage, io.Writer) error:
return tmplO(pi.(TopicPage), w)
case func(TopicListPage, io.Writer) error:
return tmplO(pi.(TopicListPage), w)
case func(ForumPage, io.Writer) error:
return tmplO(pi.(ForumPage), w)
case func(ForumsPage, io.Writer) error:
return tmplO(pi.(ForumsPage), w)
case func(ProfilePage, io.Writer) error:
return tmplO(pi.(ProfilePage), w)
case func(CreateTopicPage, io.Writer) error:
return tmplO(pi.(CreateTopicPage), w)
case func(IPSearchPage, io.Writer) error:
return tmplO(pi.(IPSearchPage), w)
case func(AccountDashPage, io.Writer) error:
return tmplO(pi.(AccountDashPage), w)
case func(ErrorPage, io.Writer) error:
return tmplO(pi.(ErrorPage), w)
case func(Page, io.Writer) error:
return tmplO(pi.(Page), w)
case nil, string:
mapping, ok := theme.TemplatesMap[template]
if !ok {
mapping = template
}
return Templates.ExecuteTemplate(w, mapping+".html", pi)
default:
log.Print("theme ", theme)
log.Print("template ", template)
log.Print("pi ", pi)
log.Print("tmplO ", tmplO)
log.Print("getTmpl ", getTmpl)
valueOf := reflect.ValueOf(tmplO)
log.Print("initial valueOf.Type()", valueOf.Type())
for valueOf.Kind() == reflect.Interface || valueOf.Kind() == reflect.Ptr {
valueOf = valueOf.Elem()
log.Print("valueOf.Elem().Type() ", valueOf.Type())
}
log.Print("deferenced valueOf.Type() ", valueOf.Type())
log.Print("valueOf.Kind() ", valueOf.Kind())
return errors.New("Unknown template type")
}
}
// GetTmpl attempts to get the template for a specific theme, otherwise it falls back on the default template pointer, which if absent will fallback onto the template interpreter
func (theme *Theme) GetTmpl(template string) interface{} {
// TODO: Figure out why we're getting a nil pointer here when transpiled templates are disabled, I would have assumed that we would just fall back to !ok on this
// Might have something to do with it being the theme's TmplPtr map, investigate.
tmpl, ok := theme.TmplPtr[template]
if ok {
return tmpl
}
tmpl, ok = TmplPtrMap[template]
if ok {
return tmpl
}
return template
}

View File

@ -9,7 +9,6 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"reflect"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -29,22 +28,21 @@ var fallbackTheme = "cosora"
var overridenTemplates = make(map[string]bool) // ? What is this used for? var overridenTemplates = make(map[string]bool) // ? What is this used for?
type ThemeStmts struct { type ThemeStmts struct {
getThemes *sql.Stmt getAll *sql.Stmt
isThemeDefault *sql.Stmt isDefault *sql.Stmt
updateTheme *sql.Stmt update *sql.Stmt
addTheme *sql.Stmt add *sql.Stmt
} }
var themeStmts ThemeStmts var themeStmts ThemeStmts
func init() { func init() {
DefaultThemeBox.Store(fallbackTheme)
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
themeStmts = ThemeStmts{ themeStmts = ThemeStmts{
getThemes: acc.Select("themes").Columns("uname, default").Prepare(), getAll: acc.Select("themes").Columns("uname, default").Prepare(),
isThemeDefault: acc.Select("themes").Columns("default").Where("uname = ?").Prepare(), isDefault: acc.Select("themes").Columns("default").Where("uname = ?").Prepare(),
updateTheme: acc.Update("themes").Set("default = ?").Where("uname = ?").Prepare(), update: acc.Update("themes").Set("default = ?").Where("uname = ?").Prepare(),
addTheme: acc.Insert("themes").Columns("uname, default").Fields("?,?").Prepare(), add: acc.Insert("themes").Columns("uname, default").Fields("?,?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -57,7 +55,11 @@ func NewThemeList() (themes ThemeList, err error) {
if err != nil { if err != nil {
return themes, err return themes, err
} }
if len(themeFiles) == 0 {
return themes, errors.New("You don't have any themes")
}
var lastTheme, defaultTheme string
for _, themeFile := range themeFiles { for _, themeFile := range themeFiles {
if !themeFile.IsDir() { if !themeFile.IsDir() {
continue continue
@ -77,6 +79,14 @@ func NewThemeList() (themes ThemeList, err error) {
return themes, err return themes, err
} }
if theme.Name == "" {
return themes, errors.New("Theme " + themePath + " doesn't have a name set in theme.json")
}
if theme.Name == fallbackTheme {
defaultTheme = fallbackTheme
}
lastTheme = theme.Name
// TODO: Implement the static file part of this and fsnotify // TODO: Implement the static file part of this and fsnotify
if theme.Path != "" { if theme.Path != "" {
log.Print("Resolving redirect to " + theme.Path) log.Print("Resolving redirect to " + theme.Path)
@ -130,6 +140,12 @@ func NewThemeList() (themes ThemeList, err error) {
themes[theme.Name] = theme themes[theme.Name] = theme
} }
if defaultTheme == "" {
defaultTheme = lastTheme
}
DefaultThemeBox.Store(defaultTheme)
return themes, nil return themes, nil
} }
@ -139,7 +155,7 @@ func (themes ThemeList) LoadActiveStatus() error {
ChangeDefaultThemeMutex.Lock() ChangeDefaultThemeMutex.Lock()
defer ChangeDefaultThemeMutex.Unlock() defer ChangeDefaultThemeMutex.Unlock()
rows, err := themeStmts.getThemes.Query() rows, err := themeStmts.getAll.Query()
if err != nil { if err != nil {
return err return err
} }
@ -290,123 +306,6 @@ func ResetTemplateOverrides() {
log.Print("All of the template overrides have been reset") log.Print("All of the template overrides have been reset")
} }
type GzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (w GzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
// NEW method of doing theme templates to allow one user to have a different theme to another. Under construction.
// TODO: Generate the type switch instead of writing it by hand
// TODO: Cut the number of types in half
func (theme *Theme) RunTmpl(template string, pi interface{}, w io.Writer) error {
// Unpack this to avoid an indirect call
gzw, ok := w.(GzipResponseWriter)
if ok {
w = gzw.Writer
}
var getTmpl = theme.GetTmpl(template)
switch tmplO := getTmpl.(type) {
case *func(CustomPagePage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(CustomPagePage), w)
case *func(TopicPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(TopicPage), w)
case *func(TopicListPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(TopicListPage), w)
case *func(ForumPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(ForumPage), w)
case *func(ForumsPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(ForumsPage), w)
case *func(ProfilePage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(ProfilePage), w)
case *func(CreateTopicPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(CreateTopicPage), w)
case *func(IPSearchPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(IPSearchPage), w)
case *func(AccountDashPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(AccountDashPage), w)
case *func(ErrorPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(ErrorPage), w)
case *func(Page, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(Page), w)
case func(CustomPagePage, io.Writer) error:
return tmplO(pi.(CustomPagePage), w)
case func(TopicPage, io.Writer) error:
return tmplO(pi.(TopicPage), w)
case func(TopicListPage, io.Writer) error:
return tmplO(pi.(TopicListPage), w)
case func(ForumPage, io.Writer) error:
return tmplO(pi.(ForumPage), w)
case func(ForumsPage, io.Writer) error:
return tmplO(pi.(ForumsPage), w)
case func(ProfilePage, io.Writer) error:
return tmplO(pi.(ProfilePage), w)
case func(CreateTopicPage, io.Writer) error:
return tmplO(pi.(CreateTopicPage), w)
case func(IPSearchPage, io.Writer) error:
return tmplO(pi.(IPSearchPage), w)
case func(AccountDashPage, io.Writer) error:
return tmplO(pi.(AccountDashPage), w)
case func(ErrorPage, io.Writer) error:
return tmplO(pi.(ErrorPage), w)
case func(Page, io.Writer) error:
return tmplO(pi.(Page), w)
case nil, string:
mapping, ok := theme.TemplatesMap[template]
if !ok {
mapping = template
}
return Templates.ExecuteTemplate(w, mapping+".html", pi)
default:
log.Print("theme ", theme)
log.Print("template ", template)
log.Print("pi ", pi)
log.Print("tmplO ", tmplO)
log.Print("getTmpl ", getTmpl)
valueOf := reflect.ValueOf(tmplO)
log.Print("initial valueOf.Type()", valueOf.Type())
for valueOf.Kind() == reflect.Interface || valueOf.Kind() == reflect.Ptr {
valueOf = valueOf.Elem()
log.Print("valueOf.Elem().Type() ", valueOf.Type())
}
log.Print("deferenced valueOf.Type() ", valueOf.Type())
log.Print("valueOf.Kind() ", valueOf.Kind())
return errors.New("Unknown template type")
}
}
// GetThemeTemplate attempts to get the template for a specific theme, otherwise it falls back on the default template pointer, which if absent will fallback onto the template interpreter
func (theme *Theme) GetTmpl(template string) interface{} {
// TODO: Figure out why we're getting a nil pointer here when transpiled templates are disabled, I would have assumed that we would just fall back to !ok on this
// Might have something to do with it being the theme's TmplPtr map, investigate.
tmpl, ok := theme.TmplPtr[template]
if ok {
return tmpl
}
tmpl, ok = TmplPtrMap[template]
if ok {
return tmpl
}
return template
}
// CreateThemeTemplate creates a theme template on the current default theme // CreateThemeTemplate creates a theme template on the current default theme
func CreateThemeTemplate(theme string, name string) { func CreateThemeTemplate(theme string, name string) {
Themes[theme].TmplPtr[name] = func(pi Page, w http.ResponseWriter) error { Themes[theme].TmplPtr[name] = func(pi Page, w http.ResponseWriter) error {

View File

@ -118,6 +118,49 @@ func RelativeTime(t time.Time) string {
return fmt.Sprintf("%d hours ago", int(seconds/60/60)) return fmt.Sprintf("%d hours ago", int(seconds/60/60))
} }
// TODO: Finish this faster and more localised version of RelativeTime
/*
// TODO: Write a test for this
// ! Experimental
func RelativeTimeBytes(t time.Time, lang int) []byte {
diff := time.Since(t)
hours := diff.Hours()
seconds := diff.Seconds()
weeks := int(hours / 24 / 7)
months := int(hours / 24 / 31)
switch {
case months > 3:
if t.Year() != time.Now().Year() {
return []byte(t.Format(phrases.RTime.MultiYear(lang)))
}
return []byte(t.Format(phrases.RTime.SingleYear(lang)))
case months > 1:
return phrases.RTime.Months(lang, months)
case months == 1:
return phrases.RTime.Month(lang)
case weeks > 1:
return phrases.RTime.Weeks(lang, weeks)
case int(hours/24) == 7:
return phrases.RTime.Week(lang)
case int(hours/24) == 1:
return phrases.RTime.Day(lang)
case int(hours/24) > 1:
return phrases.RTime.Days(lang, int(hours/24))
case seconds <= 1:
return phrases.RTime.Moment(lang)
case seconds < 60:
return phrases.RTime.Seconds(lang, int(seconds))
case seconds < 120:
return phrases.RTime.Minute(lang)
case seconds < 3600:
return phrases.RTime.Minutes(lang, int(seconds/60))
case seconds < 7200:
return phrases.RTime.Hour(lang)
}
return phrases.RTime.Hours(lang, int(seconds/60/60))
}
*/
// TODO: Write a test for this // TODO: Write a test for this
func ConvertByteUnit(bytes float64) (float64, string) { func ConvertByteUnit(bytes float64) (float64, string) {
switch { switch {

View File

@ -98,7 +98,6 @@
"register_username_too_long_prefix":"The username is too long, max: ", "register_username_too_long_prefix":"The username is too long, max: ",
"register_email_fail":"We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.", "register_email_fail":"We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.",
"alerts_no_new_alerts":"No new alerts",
"alerts_no_actor":"Unable to find the actor", "alerts_no_actor":"Unable to find the actor",
"alerts_no_target_user":"Unable to find the target user", "alerts_no_target_user":"Unable to find the target user",
"alerts_no_linked_topic":"Unable to find the linked topic", "alerts_no_linked_topic":"Unable to find the linked topic",
@ -356,7 +355,7 @@
"forum_locked":"Locked", "forum_locked":"Locked",
"topics_moderate":"Moderate", "topics_moderate":"Moderate",
"topics_replies_suffix":" replies", "topics_replies_suffix":" replies",
"forums_topics_suffix":" topics", "forums.topics_suffix":" topics",
"topics_gap_likes_suffix":" likes", "topics_gap_likes_suffix":" likes",
"topics_likes_suffix":"likes", "topics_likes_suffix":"likes",
"topics_last":"Last", "topics_last":"Last",

View File

@ -45,7 +45,7 @@
<img src="{{.CurrentUser.MicroAvatar}}" /> <img src="{{.CurrentUser.MicroAvatar}}" />
<div class="option_box"> <div class="option_box">
<a href="{{.CurrentUser.Link}}" class="username">{{.CurrentUser.Name}}</a> <a href="{{.CurrentUser.Link}}" class="username">{{.CurrentUser.Name}}</a>
<span class="alerts">{{lang "alerts_no_new_alerts"}}</span> <span class="alerts">{{lang "alerts.no_alerts_short"}}</span>
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -13,7 +13,7 @@
{{if $.CurrentUser.Perms.ViewIPs}}<br /><small style="margin-left: 2px;font-size: 12px;">{{.IPAddress}}</small>{{end}} {{if $.CurrentUser.Perms.ViewIPs}}<br /><small style="margin-left: 2px;font-size: 12px;">{{.IPAddress}}</small>{{end}}
</span> </span>
<span class="to_right"> <span class="to_right">
<span style="font-size: 14px;">{{.DoneAt}}</span> <span style="font-size: 14px;" title="{{.DoneAt}}">{{.DoneAt}}</span>
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>

View File

@ -13,7 +13,7 @@
{{if $.CurrentUser.Perms.ViewIPs}}<br /><small style="margin-left: 2px;font-size: 12px;">{{.IPAddress}}</small>{{end}} {{if $.CurrentUser.Perms.ViewIPs}}<br /><small style="margin-left: 2px;font-size: 12px;">{{.IPAddress}}</small>{{end}}
</span> </span>
<span class="to_right"> <span class="to_right">
<span style="font-size: 14px;">{{.DoneAt}}</span> <span style="font-size: 14px;" title="{{.DoneAt}}">{{.DoneAt}}</span>
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>

View File

@ -13,7 +13,7 @@
{{if $.CurrentUser.Perms.ViewIPs}}<br /><small style="margin-left: 2px;font-size: 12px;">{{.IPAddress}}</small>{{end}} {{if $.CurrentUser.Perms.ViewIPs}}<br /><small style="margin-left: 2px;font-size: 12px;">{{.IPAddress}}</small>{{end}}
</span> </span>
<span class="to_right"> <span class="to_right">
<span style="font-size: 14px;">{{.DoneAt}}</span> <span style="font-size: 14px;" title="{{.DoneAt}}">{{.DoneAt}}</span>
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>

View File

@ -44,6 +44,7 @@
</div> </div>
<div id="profile_right_lane" class="colstack_right"> <div id="profile_right_lane" class="colstack_right">
{{if .CurrentUser.Loggedin}}
{{if .CurrentUser.Perms.BanUsers}} {{if .CurrentUser.Perms.BanUsers}}
<!-- TODO: Inline the display: none; CSS --> <!-- TODO: Inline the display: none; CSS -->
<div id="ban_user_head" class="colstack_item colstack_head hash_hide ban_user_hash" style="display: none;"> <div id="ban_user_head" class="colstack_item colstack_head hash_hide ban_user_hash" style="display: none;">
@ -81,6 +82,7 @@
</div> </div>
</form> </form>
{{end}} {{end}}
{{end}}
<div id="profile_comments_head" class="colstack_item colstack_head hash_hide"> <div id="profile_comments_head" class="colstack_item colstack_head hash_hide">
<div class="rowitem"><h1><a>{{lang "profile_comments_head"}}</a></h1></div> <div class="rowitem"><h1><a>{{lang "profile_comments_head"}}</a></h1></div>

View File

@ -8,7 +8,7 @@
{{if $.CurrentUser.IsMod}}<a href="/profile/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_edit_tooltip"}}" aria-label="{{lang "profile_comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a> {{if $.CurrentUser.IsMod}}<a href="/profile/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_edit_tooltip"}}" aria-label="{{lang "profile_comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a>
<a href="/profile/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_delete_tooltip"}}" aria-label="{{lang "profile_comments_delete_aria"}}"><button class="username delete_item trash_label"></button></a>{{end}} <a href="/profile/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_delete_tooltip"}}" aria-label="{{lang "profile_comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a>{{end}}
<a class="mod_button" href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=user-reply"><button class="username report_item flag_label" title="{{lang "profile_comments_report_tooltip"}}" aria-label="{{lang "profile_comments_report_aria"}}"></button></a> <a class="mod_button" href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=user-reply"><button class="username report_item flag_label" title="{{lang "profile_comments_report_tooltip"}}" aria-label="{{lang "profile_comments_report_aria"}}"></button></a>
@ -30,7 +30,7 @@
<span class="controls"> <span class="controls">
{{if $.CurrentUser.IsMod}} {{if $.CurrentUser.IsMod}}
<a href="/profile/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_edit_tooltip"}}" aria-label="{{lang "profile_comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a> <a href="/profile/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_edit_tooltip"}}" aria-label="{{lang "profile_comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a>
<a href="/profile/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_delete_tooltip"}}" aria-label="{{lang "profile_comments_delete_aria"}}"><button class="username delete_item trash_label"></button></a> <a href="/profile/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_delete_tooltip"}}" aria-label="{{lang "profile_comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a>
{{end}} {{end}}
<a class="mod_button" href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=user-reply"><button class="username report_item flag_label" title="{{lang "profile_comments_report_tooltip"}}" aria-label="{{lang "profile_comments_report_aria"}}"></button></a> <a class="mod_button" href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=user-reply"><button class="username report_item flag_label" title="{{lang "profile_comments_report_tooltip"}}" aria-label="{{lang "profile_comments_report_aria"}}"></button></a>
</span> </span>

View File

@ -65,7 +65,7 @@
{{if .CurrentUser.Perms.EditTopic}}<a href='/topic/edit/{{.Topic.ID}}' class="mod_button open_edit" style="font-weight:normal;" title="{{lang "topic.edit_tooltip"}}" aria-label="{{lang "topic.edit_aria"}}"><button class="username edit_label"></button></a>{{end}} {{if .CurrentUser.Perms.EditTopic}}<a href='/topic/edit/{{.Topic.ID}}' class="mod_button open_edit" style="font-weight:normal;" title="{{lang "topic.edit_tooltip"}}" aria-label="{{lang "topic.edit_aria"}}"><button class="username edit_label"></button></a>{{end}}
{{end}} {{end}}
{{if .CurrentUser.Perms.DeleteTopic}}<a href='/topic/delete/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic.delete_tooltip"}}" aria-label="{{lang "topic.delete_aria"}}"><button class="username trash_label"></button></a>{{end}} {{if .CurrentUser.Perms.DeleteTopic}}<a href='/topic/delete/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic.delete_tooltip"}}" aria-label="{{lang "topic.delete_aria"}}"><button class="username delete_label"></button></a>{{end}}
{{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}<a class="mod_button" href='/topic/unlock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' style="font-weight:normal;" title="{{lang "topic.unlock_tooltip"}}" aria-label="{{lang "topic.unlock_aria"}}"><button class="username unlock_label"></button></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic.lock_tooltip"}}" aria-label="{{lang "topic.lock_aria"}}"><button class="username lock_label"></button></a>{{end}}{{end}} {{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}<a class="mod_button" href='/topic/unlock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' style="font-weight:normal;" title="{{lang "topic.unlock_tooltip"}}" aria-label="{{lang "topic.unlock_aria"}}"><button class="username unlock_label"></button></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic.lock_tooltip"}}" aria-label="{{lang "topic.lock_aria"}}"><button class="username lock_label"></button></a>{{end}}{{end}}

View File

@ -95,7 +95,7 @@
</div> </div>
<div class="action_button_right"> <div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a> <a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile">{{.Topic.RelativeCreatedAt}}</a> <a class="action_button created_at hide_on_mobile" title="{{abstime .Topic.CreatedAt}}">{{.Topic.RelativeCreatedAt}}</a>
{{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IPAddress}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.Topic.IPAddress}}</a>{{end}} {{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IPAddress}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.Topic.IPAddress}}</a>{{end}}
</div> </div>
</div> </div>

View File

@ -28,7 +28,7 @@
</div> </div>
<div class="action_button_right"> <div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a> <a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile">{{.RelativeCreatedAt}}</a> <a class="action_button created_at hide_on_mobile" title="{{abstime .CreatedAt}}">{{.RelativeCreatedAt}}</a>
{{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}{{end}} {{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}{{end}}
</div> </div>
</div> </div>

View File

@ -18,7 +18,7 @@
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_edit_tooltip"}}" aria-label="{{lang "topic.post_edit_aria"}}"><button class="username edit_item edit_label"></button></a>{{end}} {{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_edit_tooltip"}}" aria-label="{{lang "topic.post_edit_aria"}}"><button class="username edit_item edit_label"></button></a>{{end}}
{{end}} {{end}}
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_delete_tooltip"}}" aria-label="{{lang "topic.post_delete_aria"}}"><button class="username delete_item trash_label"></button></a>{{end}} {{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_delete_tooltip"}}" aria-label="{{lang "topic.post_delete_aria"}}"><button class="username delete_item delete_label"></button></a>{{end}}
{{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.IPAddress}}' style="font-weight:normal;" title="{{lang "topic.post_ip_tooltip"}}" aria-label="The poster's IP is {{.IPAddress}}"><button class="username ip_label"></button></a>{{end}} {{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.IPAddress}}' style="font-weight:normal;" title="{{lang "topic.post_ip_tooltip"}}" aria-label="The poster's IP is {{.IPAddress}}"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="{{lang "topic.post_flag_tooltip"}}" aria-label="{{lang "topic.post_flag_aria"}}" rel="nofollow"><button class="username report_item flag_label"></button></a> <a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="{{lang "topic.post_flag_tooltip"}}" aria-label="{{lang "topic.post_flag_aria"}}" rel="nofollow"><button class="username report_item flag_label"></button></a>

View File

@ -27,7 +27,7 @@
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a> <a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span> <span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br> <a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span> <span class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{.RelativeLastReplyAt}}</span>
</span> </span>
</div> </div>
</div> </div>

View File

@ -2,9 +2,9 @@
--header-border-color: hsl(0,0%,80%); --header-border-color: hsl(0,0%,80%);
--element-border-color: hsl(0,0%,85%); --element-border-color: hsl(0,0%,85%);
--element-background-color: white; --element-background-color: white;
--replies-lang-string: "{{index .Phrases "topics_replies_suffix"}}"; --replies-lang-string: "{{lang "topics_replies_suffix" . }}";
--topics-lang-string: "{{index .Phrases "forums_topics_suffix"}}"; --topics-lang-string: "{{lang "forums.topics_suffix" . }}";
--likes-lang-string: "{{index .Phrases "topics_gap_likes_suffix"}}"; --likes-lang-string: "{{lang "topics_gap_likes_suffix" . }}";
--primary-link-color: hsl(0,0%,40%); --primary-link-color: hsl(0,0%,40%);
--primary-text-color: hsl(0,0%,20%); --primary-text-color: hsl(0,0%,20%);
--lightened-primary-text-color: hsl(0,0%,30%); --lightened-primary-text-color: hsl(0,0%,30%);
@ -137,7 +137,7 @@ li {
left: -1px; left: -1px;
} }
.alert_aftercounter:before { .alert_aftercounter:before {
content: "{{index .Phrases "menu_alerts"}}"; content: "{{lang "menu_alerts" . }}";
margin-left: 4px; margin-left: 4px;
} }
@ -401,7 +401,7 @@ h1, h3 {
margin-right: 9px; margin-right: 9px;
} }
.topic_list_title_block .pre_opt:before { .topic_list_title_block .pre_opt:before {
content: "{{index .Phrases "topics_click_topics_to_select"}}"; content: "{{lang "topics_click_topics_to_select" . }}";
font-size: 14px; font-size: 14px;
} }
.topic_list_title, .forum_title { .topic_list_title, .forum_title {
@ -982,7 +982,7 @@ textarea {
content: "(" content: "("
} }
.topic_view_count:after { .topic_view_count:after {
content: "{{index .Phrases "topic.view_count_suffix"}})"; content: "{{lang "topic.view_count_suffix" . }})";
} }
.postImage { .postImage {
width: 100%; width: 100%;
@ -1074,7 +1074,7 @@ textarea {
display: block; display: block;
} }
.like_count:after { .like_count:after {
content: "{{index .Phrases "topic.like_count_suffix"}}"; content: "{{lang "topic.like_count_suffix" . }}";
margin-right: 6px; margin-right: 6px;
} }
@ -1102,31 +1102,31 @@ textarea {
} }
.add_like:before, .remove_like:before { .add_like:before, .remove_like:before {
content: "{{index .Phrases "topic.plus_one"}}"; content: "{{lang "topic.plus_one" . }}";
} }
.button_container .open_edit:after, .edit_item:after{ .button_container .open_edit:after, .edit_item:after{
content: "{{index .Phrases "topic.edit_button_text"}}"; content: "{{lang "topic.edit_button_text" . }}";
} }
.delete_item:after { .delete_item:after {
content: "{{index .Phrases "topic.delete_button_text"}}"; content: "{{lang "topic.delete_button_text" . }}";
} }
.ip_item_button:after { .ip_item_button:after {
content: "{{index .Phrases "topic.ip_button_text"}}"; content: "{{lang "topic.ip_button_text" . }}";
} }
.lock_item:after { .lock_item:after {
content: "{{index .Phrases "topic.lock_button_text"}}"; content: "{{lang "topic.lock_button_text" . }}";
} }
.unlock_item:after { .unlock_item:after {
content: "{{index .Phrases "topic.unlock_button_text"}}"; content: "{{lang "topic.unlock_button_text" . }}";
} }
.pin_item:after { .pin_item:after {
content: "{{index .Phrases "topic.pin_button_text"}}"; content: "{{lang "topic.pin_button_text" . }}";
} }
.unpin_item:after { .unpin_item:after {
content: "{{index .Phrases "topic.unpin_button_text"}}"; content: "{{lang "topic.unpin_button_text" . }}";
} }
.report_item:after { .report_item:after {
content: "{{index .Phrases "topic.report_button_text"}}"; content: "{{lang "topic.report_button_text" .}}";
} }
#ip_search_container .rowlist .rowitem { #ip_search_container .rowlist .rowitem {
@ -1803,7 +1803,7 @@ textarea {
content: ""; content: "";
} }
.like_count:before { .like_count:before {
content: "{{index .Phrases "topic.plus"}}"; content: "{{lang "topic.plus" . }}";
font-weight: normal; font-weight: normal;
} }
.created_at { .created_at {

View File

@ -214,31 +214,31 @@
margin-right: 6px; margin-right: 6px;
} }
.perm_preset_no_access:before { .perm_preset_no_access:before {
content: "{{index .Phrases "panel_perms_no_access" }}"; content: "{{lang "panel_perms_no_access" . }}";
color: hsl(0,100%,20%); color: hsl(0,100%,20%);
} }
.perm_preset_read_only:before, .perm_preset_can_post:before { .perm_preset_read_only:before, .perm_preset_can_post:before {
color: hsl(120,100%,20%); color: hsl(120,100%,20%);
} }
.perm_preset_read_only:before { .perm_preset_read_only:before {
content: "{{index .Phrases "panel_perms_read_only" }}"; content: "{{lang "panel_perms_read_only" . }}";
} }
.perm_preset_can_post:before { .perm_preset_can_post:before {
content: "{{index .Phrases "panel_perms_can_post" }}"; content: "{{lang "panel_perms_can_post" . }}";
} }
.perm_preset_can_moderate:before { .perm_preset_can_moderate:before {
content: "{{index .Phrases "panel_perms_can_moderate" }}"; content: "{{lang "panel_perms_can_moderate" . }}";
color: hsl(240,100%,20%); color: hsl(240,100%,20%);
} }
.perm_preset_quasi_mod:before { .perm_preset_quasi_mod:before {
content: "{{index .Phrases "panel_perms_quasi_mod" }}"; content: "{{lang "panel_perms_quasi_mod" . }}";
} }
.perm_preset_custom:before { .perm_preset_custom:before {
content: "{{index .Phrases "panel_perms_custom" }}"; content: "{{lang "panel_perms_custom" . }}";
color: hsl(0,0%,20%); color: hsl(0,0%,20%);
} }
.perm_preset_default:before { .perm_preset_default:before {
content: "{{index .Phrases "panel_perms_default" }}"; content: "{{lang "panel_perms_default" . }}";
} }
.panel_submitrow .rowitem { .panel_submitrow .rowitem {

View File

@ -777,27 +777,13 @@ input[type=checkbox]:checked + label .sel {
.button_container .open_edit:after, .edit_item:after { .button_container .open_edit:after, .edit_item:after {
content: "{{lang "topic.edit_button_text" . }}"; content: "{{lang "topic.edit_button_text" . }}";
} }
.delete_item:after {
content: "{{lang "topic.delete_button_text" . }}";
}
.ip_item_button:after { .ip_item_button:after {
content: "{{lang "topic.ip_button_text" . }}"; content: "{{lang "topic.ip_button_text" . }}";
} }{{$p := .}}
.lock_item:after { {{range (toArr "delete" "lock" "unlock" "pin" "unpin" "report")}}
content: "{{lang "topic.lock_button_text" . }}"; .{{.}}_item:after {
} content: "{{lang (concat "topic." . "_button_text") ($p) }}";
.unlock_item:after { }{{end}}
content: "{{lang "topic.unlock_button_text" . }}";
}
.pin_item:after {
content: "{{lang "topic.pin_button_text" . }}";
}
.unpin_item:after {
content: "{{lang "topic.unpin_button_text" . }}";
}
.report_item:after {
content: "{{lang "topic.report_button_text" . }}";
}
.like_count:after { .like_count:after {
content: "{{lang "topic.like_count_suffix" . }}"; content: "{{lang "topic.like_count_suffix" . }}";
} }

View File

@ -100,7 +100,7 @@ li {
font-size: 14px; font-size: 14px;
} }
.alert_aftercounter:before { .alert_aftercounter:before {
content: "{{index .Phrases "menu_alerts"}}"; content: "{{lang "menu_alerts" . }}";
} }
.menu_alerts .alertList, .hide_on_big, .show_on_mobile { .menu_alerts .alertList, .hide_on_big, .show_on_mobile {
@ -290,54 +290,30 @@ a {
} }
.like_label:before { .like_label:before {
content: "{{index .Phrases "topic.plus_one"}}"; content: "{{lang "topic.plus_one" . }}";
} }{{$out := .}}
.edit_label:before { {{range (toArr "edit" "delete" "pin" "lock" "unlock" "unpin" "ip" "flag")}}
content: "{{index .Phrases "topic.edit_button_text"}}"; .{{.}}_label:before {
} content: "{{lang (concat "topic." . "_button_text") ($out) }}";
.trash_label:before { }{{end}}
content: "{{index .Phrases "topic.delete_button_text"}}";
}
.pin_label:before {
content: "{{index .Phrases "topic.pin_button_text"}}";
}
.lock_label:before {
content: "{{index .Phrases "topic.lock_button_text"}}";
}
.unlock_label:before {
content: "{{index .Phrases "topic.unlock_button_text"}}";
}
.unpin_label:before {
content: "{{index .Phrases "topic.unpin_button_text"}}";
}
.ip_label:before {
content: "{{index .Phrases "topic.ip_button_text"}}";
}
.flag_label:before {
content: "{{index .Phrases "topic.flag_button_text"}}";
}
.like_count_label, .like_count { .like_count_label, .like_count {
display: none; display: none;
} }
.like_count_label:before { .like_count_label:before {
content: "{{index .Phrases "topics_likes_suffix"}}"; content: "{{lang "topics_likes_suffix" . }}";
} }
.has_likes .like_count_label { .has_likes .like_count_label, .has_likes .like_count {
font-size: 12px; font-size: 12px;
display: block; display: block;
float: left; float: left;
line-height: 19px; line-height: 19px;
} }
.has_likes .like_count { .has_likes .like_count {
font-size: 12px;
display: block;
float: left;
line-height: 19px;
margin-right: 2px; margin-right: 2px;
} }
.like_count:before { .like_count:before {
content: "{{index .Phrases "pipe"}}"; content: "{{lang "pipe" . }}";
margin-right: 5px; margin-right: 5px;
} }
@ -695,21 +671,21 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
} }
.topic_list_title_block .pre_opt:before { .topic_list_title_block .pre_opt:before {
content: "{{index .Phrases "topics_click_topics_to_select"}}"; content: "{{lang "topics_click_topics_to_select" . }}";
font-size: 14px; font-size: 14px;
} }
.create_topic_opt a:before { .create_topic_opt a:before {
content: "{{index .Phrases "topics_new_topic"}}"; content: "{{lang "topics_new_topic" . }}";
margin-left: 3px; margin-left: 3px;
} }
.locked_opt a:before { .locked_opt a:before {
content: "{{index .Phrases "forum_locked"}}"; content: "{{lang "forum_locked" . }}";
} }
.mod_opt a { .mod_opt a {
margin-left: 4px; margin-left: 4px;
} }
.mod_opt a:after { .mod_opt a:after {
content: "{{index .Phrases "topics_moderate"}}"; content: "{{lang "topics_moderate" . }}";
padding-left: 1px; padding-left: 1px;
} }
.create_topic_opt { .create_topic_opt {
@ -835,10 +811,10 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
white-space: nowrap; white-space: nowrap;
} }
.topic_list .lastReplyAt:before { .topic_list .lastReplyAt:before {
content: "{{index .Phrases "topics_last"}}: "; content: "{{lang "topics_last" . }}: ";
} }
.topic_list .starter:before { .topic_list .starter:before {
content: "{{index .Phrases "topics_starter"}}: "; content: "{{lang "topics_starter" . }}: ";
} }
.topic_middle { .topic_middle {
display: none; display: none;
@ -906,7 +882,7 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
font-weight: normal; font-weight: normal;
} }
#profile_left_pane .report_item:after { #profile_left_pane .report_item:after {
content: "{{index .Phrases "topic.report_button_text"}}"; content: "{{lang "topic.report_button_text" . }}";
} }
#profile_left_lane .profileName { #profile_left_lane .profileName {
font-size: 18px; font-size: 18px;

View File

@ -31,10 +31,10 @@
} }
.edit_button:before { .edit_button:before {
content: "{{index .Phrases "panel_edit_button_text"}}"; content: "{{lang "panel_edit_button_text" . }}";
} }
.delete_button:after { .delete_button:after {
content: "{{index .Phrases "panel_delete_button_text"}}"; content: "{{lang "panel_delete_button_text" . }}";
} }
#panel_forums .rowitem { #panel_forums .rowitem {

View File

@ -469,10 +469,10 @@ input, select {
white-space: nowrap; white-space: nowrap;
} }
.topic_list .lastReplyAt:before { .topic_list .lastReplyAt:before {
content: "{{index .Phrases "topics_last"}}: "; content: "{{lang "topics_last" . }}: ";
} }
.topic_list .starter:before { .topic_list .starter:before {
content: "{{index .Phrases "topics_starter"}}: "; content: "{{lang "topics_starter" . }}: ";
} }
@supports not (display: grid) { @supports not (display: grid) {
@ -682,7 +682,7 @@ button.username {
.edit_label:before { .edit_label:before {
content: "🖊️"; content: "🖊️";
} }
.trash_label:before { .delete_label:before {
content: "🗑️"; content: "🗑️";
} }
.pin_label:before, .unpin_label:before { .pin_label:before, .unpin_label:before {
@ -802,22 +802,20 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
display: none; display: none;
} }
.alert { /* TODO: Can we just set .alert on the alert_success and .alert_error ones? */
.alert, .alert_success, .alert_error {
display: block; display: block;
padding: 5px; padding: 5px;
margin-bottom: 10px; margin-bottom: 10px;
}
.alert {
border: 1px solid hsl(0, 0%, 80%); border: 1px solid hsl(0, 0%, 80%);
} }
.alert_success { .alert_success {
display: block; border: 1px solid #A2FC00;
padding: 5px; background-color: #DAF7A6;
border: 1px solid A2FC00;
margin-bottom: 10px;
background-color: DAF7A6;
} }
.alert_error { .alert_error {
display: block;
padding: 5px;
border: 1px solid #FF004B; border: 1px solid #FF004B;
margin-bottom: 8px; margin-bottom: 8px;
background-color: #FEB7CC; background-color: #FEB7CC;
@ -920,7 +918,7 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
font-size: 18px; font-size: 18px;
} }
#profile_left_lane .report_item:after { #profile_left_lane .report_item:after {
content: "{{index .Phrases "topic.report_button_text"}}"; content: "{{lang "topic.report_button_text" . }}";
} }
#profile_right_lane { #profile_right_lane {
width: calc(100% - 245px); width: calc(100% - 245px);

View File

@ -9,10 +9,10 @@
} }
.edit_button:before { .edit_button:before {
content: "{{index .Phrases "panel_edit_button_text"}}"; content: "{{lang "panel_edit_button_text" . }}";
} }
.delete_button:after { .delete_button:after {
content: "{{index .Phrases "panel_delete_button_text"}}"; content: "{{lang "panel_delete_button_text" . }}";
} }
.tag-mini { .tag-mini {
@ -104,32 +104,32 @@
} }
.perm_preset_no_access:before { .perm_preset_no_access:before {
content: "{{index .Phrases "panel_perms_no_access" }}"; content: "{{lang "panel_perms_no_access" . }}";
color: maroon; color: maroon;
} }
.perm_preset_read_only:before, .perm_preset_can_post:before { .perm_preset_read_only:before, .perm_preset_can_post:before {
color: green; color: green;
} }
.perm_preset_read_only:before { .perm_preset_read_only:before {
content: "{{index .Phrases "panel_perms_read_only" }}"; content: "{{lang "panel_perms_read_only" . }}";
} }
.perm_preset_can_post:before { .perm_preset_can_post:before {
content: "{{index .Phrases "panel_perms_can_post" }}"; content: "{{lang "panel_perms_can_post" . }}";
} }
.perm_preset_can_moderate:before { .perm_preset_can_moderate:before {
content: "{{index .Phrases "panel_perms_can_moderate" }}"; content: "{{lang "panel_perms_can_moderate" . }}";
color: darkblue; color: darkblue;
} }
.perm_preset_quasi_mod:before { .perm_preset_quasi_mod:before {
content: "{{index .Phrases "panel_perms_quasi_mod" }}"; content: "{{lang "panel_perms_quasi_mod" . }}";
color: darkblue; color: darkblue;
} }
.perm_preset_custom:before { .perm_preset_custom:before {
content: "{{index .Phrases "panel_perms_custom" }}"; content: "{{lang "panel_perms_custom" . }}";
color: black; color: black;
} }
.perm_preset_default:before { .perm_preset_default:before {
content: "{{index .Phrases "panel_perms_default" }}"; content: "{{lang "panel_perms_default" . }}";
} }
#panel_dashboard_right .colstack_head { #panel_dashboard_right .colstack_head {