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
}
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{})
for col, value := range in {
out[col] = value
}
return out
}
}*/
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.DBTableColumn{
qgen.DBTableColumn{"action", "varchar", 100, false, false, ""},

View File

@ -2,6 +2,7 @@ package common
import (
"database/sql"
"time"
"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) {
for rows.Next() {
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 {
return logs, err
}
log.DoneAt = doneAt.Format("2006-01-02 15:04:05")
logs = append(logs, log)
}
return logs, rows.Err()

View File

@ -1,7 +1,11 @@
package common
import "database/sql"
import "github.com/Azareal/Gosora/query_gen"
import (
"database/sql"
"time"
"github.com/Azareal/Gosora/query_gen"
)
var RegLogs RegLogStore
@ -83,10 +87,12 @@ func (store *SQLRegLogStore) GetOffset(offset int, perPage int) (logs []RegLogIt
for rows.Next() {
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 {
return logs, err
}
log.DoneAt = doneAt.Format("2006-01-02 15:04:05")
logs = append(logs, log)
}
return logs, rows.Err()

View File

@ -108,7 +108,8 @@ func InitPhrases(lang string) error {
for index, phraseName := range phraseNames {
phrase, ok := langPack.TmplPhrases[phraseName]
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)
}

View File

@ -16,10 +16,19 @@ import (
"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 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 {
Name 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)
}
var Template_forum_guest_handle = Template_forum_handle
var Template_forum_member_handle = Template_forum_handle
// nolint
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)
}
var Template_profile_guest_handle = Template_profile_handle
var Template_profile_member_handle = Template_profile_handle
// nolint
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)
header.Title = "User 526"
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 {
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)
header.Title = "General Forum"
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 {
return err
}
@ -495,7 +508,21 @@ func writeTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string)
getterstr += "\treturn " + templateName + "_frags\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)
if err != nil {
log.Fatal(err)
@ -573,6 +600,17 @@ func InitTemplates() error {
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{} {
level, ok := levelInt.(int)
if !ok {
@ -581,6 +619,15 @@ func InitTemplates() error {
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{} {
return ""
}
@ -606,6 +653,10 @@ func InitTemplates() error {
for index, path := range templateFiles {
path = strings.Replace(path, "\\", "/", -1)
log.Print("templateFile: ", path)
if skipCTmpl(path) {
log.Print("skipping")
continue
}
templateFileMap[path] = index
}
@ -616,6 +667,10 @@ func InitTemplates() error {
for _, path := range overrideFiles {
path = strings.Replace(path, "\\", "/", -1)
log.Print("overrideFile: ", path)
if skipCTmpl(path) {
log.Print("skipping")
continue
}
index, ok := templateFileMap["templates/"+strings.TrimPrefix(path, "templates/overrides/")]
if !ok {
log.Print("not ok: templates/" + strings.TrimPrefix(path, "templates/overrides/"))

View File

@ -49,7 +49,7 @@ type CTemplateSet struct {
TemplateFragmentCount map[string]int
FragOnce map[string]bool
fragmentCursor map[string]int
FragOut string
FragOut []OutFrag
fragBuf []Fragment
varList map[string]VarItem
localVars map[string]map[string]VarItemReflect
@ -88,9 +88,11 @@ func NewCTemplateSet() *CTemplateSet {
"dock": true,
"elapsed": true,
"lang": true,
"level": true,
"scope": true,
"dyntmpl": true,
//"langf":true,
"level": true,
"abstime": true,
"scope": true,
"dyntmpl": true,
},
}
}
@ -124,6 +126,12 @@ type Skipper struct {
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) {
c.importMap = map[string]string{}
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"
var writeFrag = func(tmplName string, index int, body string) {
fragmentPrefix := tmplName + "_frags[" + strconv.Itoa(index) + "]" + " = []byte(`" + body + "`)\n"
c.detail("writing ", fragmentPrefix)
c.FragOut += fragmentPrefix
//c.detail("writing ", fragmentPrefix)
c.FragOut = append(c.FragOut, OutFrag{tmplName, index, body})
}
for _, frag := range c.fragBuf {
@ -925,6 +932,7 @@ ArgLoop:
notident = true
con.PushPhrase(len(c.langIndexToName) - 1)
break ArgLoop
// TODO: Implement langf
case "level":
// TODO: Implement level literals
leftOperand := node.Args[pos+1].String()
@ -936,6 +944,17 @@ ArgLoop:
litString("phrases.GetLevelPhrase("+leftParam+")", false)
c.importMap[langPkg] = langPkg
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":
literal = true
break ArgLoop
@ -1255,6 +1274,19 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
case reflect.Int64:
c.importMap["strconv"] = "strconv"
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:
if !val.IsValid() {
panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")

View File

@ -12,6 +12,7 @@ import (
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"text/template"
@ -50,6 +51,7 @@ type Theme struct {
RunOnDock func(string) string //(dock string) (sbody string)
// 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
}
@ -95,6 +97,16 @@ func (theme *Theme) LoadStaticFiles() error {
}
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)
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?
err = theme.ResourceTemplates.ExecuteTemplate(&b, filename, CSSData{Phrases: phraseMap})
if err != nil {
log.Print("Failed in adding static file '" + path + "' for default theme '" + theme.Name + "'")
return err
}
data = b.Bytes()
@ -267,16 +280,16 @@ func (theme *Theme) MapTemplates() {
func (theme *Theme) setActive(active bool) error {
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 {
return err
}
hasTheme := err != sql.ErrNoRows
if hasTheme {
_, err = themeStmts.updateTheme.Exec(active, theme.Name)
_, err = themeStmts.update.Exec(active, theme.Name)
} else {
_, err = themeStmts.addTheme.Exec(theme.Name, active)
_, err = themeStmts.add.Exec(theme.Name, active)
}
if err != nil {
return err
@ -331,3 +344,120 @@ func (theme Theme) BuildDock(dock string) (sbody string) {
}
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"
"net/http"
"os"
"reflect"
"sync"
"sync/atomic"
@ -29,22 +28,21 @@ var fallbackTheme = "cosora"
var overridenTemplates = make(map[string]bool) // ? What is this used for?
type ThemeStmts struct {
getThemes *sql.Stmt
isThemeDefault *sql.Stmt
updateTheme *sql.Stmt
addTheme *sql.Stmt
getAll *sql.Stmt
isDefault *sql.Stmt
update *sql.Stmt
add *sql.Stmt
}
var themeStmts ThemeStmts
func init() {
DefaultThemeBox.Store(fallbackTheme)
DbInits.Add(func(acc *qgen.Accumulator) error {
themeStmts = ThemeStmts{
getThemes: acc.Select("themes").Columns("uname, default").Prepare(),
isThemeDefault: acc.Select("themes").Columns("default").Where("uname = ?").Prepare(),
updateTheme: acc.Update("themes").Set("default = ?").Where("uname = ?").Prepare(),
addTheme: acc.Insert("themes").Columns("uname, default").Fields("?,?").Prepare(),
getAll: acc.Select("themes").Columns("uname, default").Prepare(),
isDefault: acc.Select("themes").Columns("default").Where("uname = ?").Prepare(),
update: acc.Update("themes").Set("default = ?").Where("uname = ?").Prepare(),
add: acc.Insert("themes").Columns("uname, default").Fields("?,?").Prepare(),
}
return acc.FirstError()
})
@ -57,7 +55,11 @@ func NewThemeList() (themes ThemeList, err error) {
if err != nil {
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 {
if !themeFile.IsDir() {
continue
@ -77,6 +79,14 @@ func NewThemeList() (themes ThemeList, err error) {
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
if theme.Path != "" {
log.Print("Resolving redirect to " + theme.Path)
@ -130,6 +140,12 @@ func NewThemeList() (themes ThemeList, err error) {
themes[theme.Name] = theme
}
if defaultTheme == "" {
defaultTheme = lastTheme
}
DefaultThemeBox.Store(defaultTheme)
return themes, nil
}
@ -139,7 +155,7 @@ func (themes ThemeList) LoadActiveStatus() error {
ChangeDefaultThemeMutex.Lock()
defer ChangeDefaultThemeMutex.Unlock()
rows, err := themeStmts.getThemes.Query()
rows, err := themeStmts.getAll.Query()
if err != nil {
return err
}
@ -290,123 +306,6 @@ func ResetTemplateOverrides() {
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
func CreateThemeTemplate(theme string, name string) {
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))
}
// 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
func ConvertByteUnit(bytes float64) (float64, string) {
switch {

View File

@ -98,7 +98,6 @@
"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.",
"alerts_no_new_alerts":"No new alerts",
"alerts_no_actor":"Unable to find the actor",
"alerts_no_target_user":"Unable to find the target user",
"alerts_no_linked_topic":"Unable to find the linked topic",
@ -356,7 +355,7 @@
"forum_locked":"Locked",
"topics_moderate":"Moderate",
"topics_replies_suffix":" replies",
"forums_topics_suffix":" topics",
"forums.topics_suffix":" topics",
"topics_gap_likes_suffix":" likes",
"topics_likes_suffix":"likes",
"topics_last":"Last",

View File

@ -45,7 +45,7 @@
<img src="{{.CurrentUser.MicroAvatar}}" />
<div class="option_box">
<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>
{{end}}

View File

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

View File

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

View File

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

View File

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

View File

@ -95,7 +95,7 @@
</div>
<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 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}}
</div>
</div>

View File

@ -28,7 +28,7 @@
</div>
<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 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}}
</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}}
{{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}}
<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>
<span>
<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>
</div>
</div>

View File

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

View File

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

View File

@ -777,27 +777,13 @@ input[type=checkbox]:checked + label .sel {
.button_container .open_edit:after, .edit_item:after {
content: "{{lang "topic.edit_button_text" . }}";
}
.delete_item:after {
content: "{{lang "topic.delete_button_text" . }}";
}
.ip_item_button:after {
content: "{{lang "topic.ip_button_text" . }}";
}
.lock_item:after {
content: "{{lang "topic.lock_button_text" . }}";
}
.unlock_item:after {
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" . }}";
}
}{{$p := .}}
{{range (toArr "delete" "lock" "unlock" "pin" "unpin" "report")}}
.{{.}}_item:after {
content: "{{lang (concat "topic." . "_button_text") ($p) }}";
}{{end}}
.like_count:after {
content: "{{lang "topic.like_count_suffix" . }}";
}

View File

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

View File

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

View File

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

View File

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