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:
parent
7abd3220de
commit
bdf7fa40d5
@ -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
|
||||
|
||||
|
@ -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, ""},
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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/"))
|
||||
|
@ -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?")
|
||||
|
136
common/theme.go
136
common/theme.go
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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}}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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" . }}";
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user