Added the In-Progress Widget Manager UI.

Added the IsoCode field to phrase files.
Rewrote a good portion of the widget system logic.
Added some tests for the widget system.
Added the Online Users widget.
Added a few sealed incomplete widgets like the Search & Filter Widget.
Added the AllUsers method to WsHubImpl for Online Users. Please don't abuse it.

Added the optional *DBTableKey field to AddColumn.
Added the panel_analytics_time_range template to reduce the amount of duplication.
Failed registrations now show up in red in the registration logs for Nox.
Failed logins now show up in red in the login logs for Nox.
Added basic h2 CSS to the other themes.
Added .show_on_block_edit and .hide_on_block_edit to the other themes.
Updated contributing.
Updated a bunch of dates to 2019.
Replaced tblKey{} with nil where possible.
Switched out some &s for &s to reduce the number of possible bugs.
Fixed a bug with selector messages where the inspector would get really jittery due to unnecessary DOM updates.
Moved header.Zone and associated fields to the bottom of ViewTopic to reduce the chances of problems arising.
Added the ZoneData field to *Header.
Added IDs to the items in the forum list template.
Split the fetchPhrases function into the initPhrases and fetchPhrases functions in init.js
Added .colstack_sub_head.
Fixed the CSS in the menu list.
Removed an inline style from the simple topic like and unlike buttons.
Removed an inline style from the simple topic IP button.
Simplified the LoginRequired error handler.
Fixed a typo in the comment prior to DatabaseError()
Reduce the number of false leaves for WebSocket page transitions.
Added the error zone.
De-duped the logic in WsHubImpl.getUsers.
Fixed a potential widget security issue.

Added twenty new phrases.
Added the wid column to the widgets table.

You will need to run the patcher / updater for this commit.
This commit is contained in:
Azareal 2019-01-21 22:27:59 +10:00
parent 5db5bc0c7e
commit 8f2f47e8aa
85 changed files with 1660 additions and 637 deletions

View File

@ -38,7 +38,9 @@ Always use strict mode.
Don't worry about ES5, we're targetting modern browsers. If we decide to backport code to older browsers, then we'll transpile the files.
Please don't use await. It incurs too much of a cognitive overhead as to where and when you can use it.
Please don't use await. It incurs too much of a cognitive overhead as to where and when you can use it. We can't use it everywhere quite yet, which means that we really should be using it nowhere.
Please don't abuse `const` just to shave off a few nanoseconds. Even in the Go server where I care about performance the most, I don't use const everywhere, only in about five spots in thirty thousand lines and I don't use it for performance at all there.
To keep consistency with Go code, variables must be camelCase.

View File

@ -114,8 +114,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"issued_by","int",0,false,false,""},
tblColumn{"issued_at","createdAt",0,false,false,""},
tblColumn{"expires_at","datetime",0,false,false,""},
},
[]tblKey{},
}, nil,
)*/
qgen.Install.CreateTable("users_groups_scheduler", "", "",
@ -150,8 +149,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
tblColumn{"validated", "boolean", 0, false, false, "0"},
tblColumn{"token", "varchar", 200, false, false, "''"},
},
[]tblKey{},
}, nil,
)
// TODO: Allow for patterns in domains, if the bots try to shake things up there?
@ -167,6 +165,19 @@ func createTables(adapter qgen.Adapter) error {
)
*/
// TODO: Implement password resets
/*qgen.Install.CreateTable("password_resets", "", "",
[]tblColumn{
tblColumn{"email", "varchar", 200, false, false, ""},
tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
tblColumn{"validated", "varchar", 200, false, false, ""}, // Token given once the one-use token is consumed, used to prevent multiple people consuming the same one-use token
tblColumn{"token", "varchar", 200, false, false, ""},
},
[]tblKey{
tblKey{"email", "unique"},
},
)*/
qgen.Install.CreateTable("forums", mysqlPre, mysqlCol,
[]tblColumn{
tblColumn{"fid", "int", 0, false, true, ""},
@ -305,8 +316,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"pollID", "int", 0, false, false, ""},
tblColumn{"option", "int", 0, false, false, "0"},
tblColumn{"votes", "int", 0, false, false, "0"},
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("polls_votes", mysqlPre, mysqlCol,
@ -316,8 +326,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"option", "int", 0, false, false, "0"},
tblColumn{"castAt", "createdAt", 0, false, false, ""},
tblColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"},
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("users_replies", mysqlPre, mysqlCol,
@ -345,16 +354,14 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"sentBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key
tblColumn{"createdAt", "createdAt", 0, false, false, ""},
tblColumn{"recalc", "tinyint", 0, false, false, "0"},
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("activity_stream_matches", "", "",
[]tblColumn{
tblColumn{"watcher", "int", 0, false, false, ""}, // TODO: Make this a foreign key
tblColumn{"asid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("activity_stream", "", "",
@ -377,8 +384,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"targetID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */
tblColumn{"targetType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */
tblColumn{"level", "int", 0, false, false, "0"}, /* 0: Mentions (aka the global default for any post), 1: Replies To You, 2: All Replies*/
},
[]tblKey{},
}, nil,
)
/* Due to MySQL's design, we have to drop the unique keys for table settings, plugins, and themes down from 200 to 180 or it will error */
@ -428,6 +434,7 @@ func createTables(adapter qgen.Adapter) error {
qgen.Install.CreateTable("widgets", "", "",
[]tblColumn{
tblColumn{"wid", "int", 0, false, true, ""},
tblColumn{"position", "int", 0, false, false, ""},
tblColumn{"side", "varchar", 100, false, false, ""},
tblColumn{"type", "varchar", 100, false, false, ""},
@ -435,7 +442,9 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"location", "varchar", 100, false, false, ""},
tblColumn{"data", "text", 0, false, false, "''"},
},
[]tblKey{},
[]tblKey{
tblKey{"wid", "primary"},
},
)
qgen.Install.CreateTable("menus", "", "",
@ -523,8 +532,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"ipaddress", "varchar", 200, false, false, ""},
tblColumn{"actorID", "int", 0, false, false, ""}, // TODO: Make this a foreign key
tblColumn{"doneAt", "datetime", 0, false, false, ""},
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("administration_logs", "", "",
@ -535,8 +543,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"ipaddress", "varchar", 200, false, false, ""},
tblColumn{"actorID", "int", 0, false, false, ""}, // TODO: Make this a foreign key
tblColumn{"doneAt", "datetime", 0, false, false, ""},
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("viewchunks", "", "",
@ -544,8 +551,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"count", "int", 0, false, false, "0"},
tblColumn{"createdAt", "datetime", 0, false, false, ""},
tblColumn{"route", "varchar", 200, false, false, ""},
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("viewchunks_agents", "", "",
@ -554,8 +560,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"createdAt", "datetime", 0, false, false, ""},
tblColumn{"browser", "varchar", 200, false, false, ""}, // googlebot, firefox, opera, etc.
//tblColumn{"version","varchar",0,false,false,""}, // the version of the browser or bot
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("viewchunks_systems", "", "",
@ -563,8 +568,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"count", "int", 0, false, false, "0"},
tblColumn{"createdAt", "datetime", 0, false, false, ""},
tblColumn{"system", "varchar", 200, false, false, ""}, // windows, android, unknown, etc.
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("viewchunks_langs", "", "",
@ -572,8 +576,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"count", "int", 0, false, false, "0"},
tblColumn{"createdAt", "datetime", 0, false, false, ""},
tblColumn{"lang", "varchar", 200, false, false, ""}, // en, ru, etc.
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("viewchunks_referrers", "", "",
@ -581,8 +584,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"count", "int", 0, false, false, "0"},
tblColumn{"createdAt", "datetime", 0, false, false, ""},
tblColumn{"domain", "varchar", 200, false, false, ""},
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("viewchunks_forums", "", "",
@ -590,8 +592,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"count", "int", 0, false, false, "0"},
tblColumn{"createdAt", "datetime", 0, false, false, ""},
tblColumn{"forum", "int", 0, false, false, ""},
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("topicchunks", "", "",
@ -599,8 +600,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"count", "int", 0, false, false, "0"},
tblColumn{"createdAt", "datetime", 0, false, false, ""},
// TODO: Add a column for the parent forum?
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("postchunks", "", "",
@ -608,22 +608,19 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"count", "int", 0, false, false, "0"},
tblColumn{"createdAt", "datetime", 0, false, false, ""},
// TODO: Add a column for the parent topic / profile?
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("sync", "", "",
[]tblColumn{
tblColumn{"last_update", "datetime", 0, false, false, ""},
},
[]tblKey{},
}, nil,
)
qgen.Install.CreateTable("updates", "", "",
[]tblColumn{
tblColumn{"dbVersion", "int", 0, false, false, "0"},
},
[]tblKey{},
}, nil,
)
return nil

View File

@ -1,7 +1,7 @@
/*
*
* Gosora Common Resources
* Copyright Azareal 2018 - 2019
* Copyright Azareal 2018 - 2020
*
*/
package common // import "github.com/Azareal/Gosora/common"

View File

@ -128,6 +128,7 @@ func LogWarning(err error, extra ...string) {
func errorHeader(w http.ResponseWriter, user User, title string) *Header {
header := DefaultHeader(w, user)
header.Title = title
header.Zone = "error"
return header
}
@ -160,7 +161,7 @@ func InternalErrorJS(err error, w http.ResponseWriter, r *http.Request) RouteErr
return HandledRouteError()
}
// When the task system detects if the database is down, some database errors might lip by this
// When the task system detects if the database is down, some database errors might slip by this
func DatabaseError(w http.ResponseWriter, r *http.Request) RouteError {
w.WriteHeader(500)
pi := ErrorPage{errorHeader(w, GuestUser, phrases.GetErrorPhrase("internal_error_title")), phrases.GetErrorPhrase("internal_error_body")}
@ -285,10 +286,7 @@ func LoginRequiredJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bo
// ? - Where is this used? Should we use it more?
// LoginRequired is an error shown to the end-user when they try to access an area which requires them to login
func LoginRequired(w http.ResponseWriter, r *http.Request, user User) RouteError {
w.WriteHeader(401)
pi := ErrorPage{errorHeader(w, user, phrases.GetErrorPhrase("no_permissions_title")), phrases.GetErrorPhrase("login_required_body")}
handleErrorTemplate(w, r, pi)
return HandledRouteError()
return CustomError(phrases.GetErrorPhrase("login_required_body"), 401, phrases.GetErrorPhrase("no_permissions_title"), w, r, nil, user)
}
// nolint
@ -343,6 +341,7 @@ func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWri
header = DefaultHeader(w, user)
}
header.Title = errtitle
header.Zone = "error"
w.WriteHeader(errcode)
pi := ErrorPage{header, errmsg}
handleErrorTemplate(w, r, pi)

View File

@ -1,7 +1,7 @@
/*
*
* Gosora Forum Store
* Copyright Azareal 2017 - 2018
* Copyright Azareal 2017 - 2019
*
*/
package common

View File

@ -1,7 +1,7 @@
/*
*
* OttoJS Plugin Module
* Copyright Azareal 2016 - 2018
* Copyright Azareal 2016 - 2019
*
*/
package common

View File

@ -28,6 +28,8 @@ type Header struct {
CurrentUser User // TODO: Deprecate CurrentUser on the page structs and use a pointer here
Hooks *HookTable
Zone string
ZoneID int
ZoneData interface{}
Path string
MetaDesc string
StartedAt time.Time
@ -326,6 +328,12 @@ type PanelMenuListPage struct {
ItemList []PanelMenuListItem
}
type PanelWidgetListPage struct {
*BasePanelPage
Docks map[string][]WidgetEdit
BlankWidget WidgetEdit
}
type PanelMenuPage struct {
*BasePanelPage
MenuID int
@ -463,3 +471,6 @@ type AreYouSure struct {
func DefaultHeader(w http.ResponseWriter, user User) *Header {
return &Header{Site: Site, Theme: Themes[fallbackTheme], CurrentUser: user, Writer: w}
}
func SimpleDefaultHeader(w http.ResponseWriter) *Header {
return &Header{Site: Site, Theme: Themes[fallbackTheme], CurrentUser: GuestUser, Writer: w}
}

View File

@ -37,7 +37,9 @@ type LevelPhrases struct {
// ! For the sake of thread safety, you must never modify a *LanguagePack directly, but to create a copy of it and overwrite the entry in the sync.Map
type LanguagePack struct {
Name string
Name string
IsoCode string
// Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent.
Levels LevelPhrases
GlobalPerms map[string]string

View File

@ -1,7 +1,7 @@
/*
*
* Reply Resources File
* Copyright Azareal 2016 - 2018
* Copyright Azareal 2016 - 2019
*
*/
package common

View File

@ -1,7 +1,7 @@
/*
*
* Gosora Task System
* Copyright Azareal 2017 - 2018
* Copyright Azareal 2017 - 2019
*
*/
package common

View File

@ -470,6 +470,11 @@ func CompileJSTemplates() error {
if err != nil {
return err
}
/*widget := &Widget{ID: 0}
panelWidgetsWidgetTmpl, err := c.Compile("panel_themes_widgets_widget.html", "templates/", "*common.Widget", widget, varList)
if err != nil {
return err
}*/
var dirPrefix = "./tmpl_client/"
var wg sync.WaitGroup
@ -492,6 +497,7 @@ func CompileJSTemplates() error {
writeTemplate("topics_topic", topicListItemTmpl)
writeTemplate("topic_posts", topicPostsTmpl)
writeTemplate("topic_alt_posts", topicAltPostsTmpl)
//writeTemplate("panel_themes_widgets_widget", panelWidgetsWidgetTmpl)
writeTemplateList(c, &wg, dirPrefix)
return nil
}

View File

@ -94,6 +94,7 @@ func NewCTemplateSet() *CTemplateSet {
"reltime": true,
"scope": true,
"dyntmpl": true,
"index": true,
},
}
}

View File

@ -1,7 +1,7 @@
/*
*
* Gosora Topic File
* Copyright Azareal 2017 - 2018
* Copyright Azareal 2017 - 2019
*
*/
package common

View File

@ -1,7 +1,7 @@
/*
*
* Utility Functions And Stuff
* Copyright Azareal 2017 - 2018
* Copyright Azareal 2017 - 2019
*
*/
package common

View File

@ -15,13 +15,14 @@ import (
"net/http"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/Azareal/Gosora/common/phrases"
"github.com/Azareal/gopsutil/cpu"
"github.com/Azareal/gopsutil/mem"
"github.com/gorilla/websocket"
"github.com/Azareal/Gosora/common/phrases"
)
// TODO: Disable WebSockets on high load? Add a Control Panel interface for disabling it?
@ -97,6 +98,16 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr
return nil
}
// TODO: Copied from routes package for use in wsPageResponse, find a more elegant solution.
func ParseSEOURL(urlBit string) (slug string, id int, err error) {
halves := strings.Split(urlBit, ".")
if len(halves) < 2 {
halves = append(halves, halves[0])
}
tid, err := strconv.Atoi(halves[1])
return halves[0], tid, err
}
// TODO: Use a map instead of a switch to make this more modular?
func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) {
if page == "/" {
@ -104,14 +115,47 @@ func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) {
}
DebugLog("Entering page " + page)
switch page {
switch {
// Live Topic List is an experimental feature
// TODO: Optimise this to reduce the amount of contention
case "/topics/":
case page == "/topics/":
topicListMutex.Lock()
topicListWatchers[wsUser] = true
topicListMutex.Unlock()
case "/panel/":
case strings.HasPrefix(page, "/topic/"):
//fmt.Println("entering topic prefix websockets zone")
_, tid, err := ParseSEOURL(page)
if err != nil {
return
}
topic, err := Topics.Get(tid)
if err != nil {
return
}
var usercpy *User = BlankUser()
*usercpy = *wsUser.User
usercpy.Init()
if !Forums.Exists(topic.ParentID) {
return
}
/*skip, rerr := header.Hooks.VhookSkippable("ws_topic_check_pre_perms", w, r, usercpy, &fid, &header)
if skip || rerr != nil {
return
}*/
fperms, err := FPStore.Get(topic.ParentID, usercpy.Group)
if err == ErrNoRows {
fperms = BlankForumPerms()
} else if err != nil {
return
}
cascadeForumPerms(fperms, usercpy)
if !usercpy.Perms.ViewTopic {
return
}
case page == "/panel/":
if !wsUser.User.IsSuperMod {
return
}
@ -138,15 +182,19 @@ func wsLeavePage(wsUser *WSUser, conn *websocket.Conn, page string) {
page = Config.DefaultPath
}
DebugLog("Leaving page " + page)
switch page {
case "/topics/":
if page != "" {
DebugLog("Leaving page " + page)
}
switch {
case page == "/topics/":
wsUser.FinalizePage("/topics/", func() {
topicListMutex.Lock()
delete(topicListWatchers, wsUser)
topicListMutex.Unlock()
})
case "/panel/":
case strings.HasPrefix(page, "/topic/"):
//fmt.Println("leaving topic prefix websockets zone")
case page == "/panel/":
adminStatsMutex.Lock()
delete(adminStatsWatchers, conn)
adminStatsMutex.Unlock()

148
common/widget.go Normal file
View File

@ -0,0 +1,148 @@
package common
import (
"database/sql"
"encoding/json"
"strings"
"sync/atomic"
"github.com/Azareal/Gosora/query_gen"
)
type WidgetStmts struct {
//getList *sql.Stmt
getDockList *sql.Stmt
delete *sql.Stmt
create *sql.Stmt
update *sql.Stmt
}
var widgetStmts WidgetStmts
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
widgetStmts = WidgetStmts{
//getList: acc.Select("widgets").Columns("wid, position, side, type, active, location, data").Orderby("position ASC").Prepare(),
getDockList: acc.Select("widgets").Columns("wid, position, type, active, location, data").Where("side = ?").Orderby("position ASC").Prepare(),
delete: acc.Delete("widgets").Where("wid = ?").Prepare(),
create: acc.Insert("widgets").Columns("position, side, type, active, location, data").Fields("?,?,?,?,?,?").Prepare(),
update: acc.Update("widgets").Set("position = ?, side = ?, type = ?, active = ?, location = ?, data = ?").Where("wid = ?").Prepare(),
}
return acc.FirstError()
})
}
// TODO: Shrink this struct for common uses in the templates? Would that really make things go faster?
type Widget struct {
ID int
Enabled bool
Location string // Coming Soon: overview, topics, topic / topic_view, forums, forum, global
Position int
RawBody string
Body string
Side string
Type string
Literal bool
TickMask atomic.Value
InitFunc func(widget *Widget, schedule *WidgetScheduler) error
ShutdownFunc func(widget *Widget) error
BuildFunc func(widget *Widget, hvars interface{}) (string, error)
TickFunc func(widget *Widget) error
}
func (widget *Widget) Delete() error {
_, err := widgetStmts.delete.Exec(widget.ID)
if err != nil {
return err
}
// Reload the dock
// TODO: Better synchronisation
Widgets.delete(widget.ID)
widgets, err := getDockWidgets(widget.Side)
if err != nil {
return err
}
setDock(widget.Side, widgets)
return nil
}
func (widget *Widget) Copy() (owidget *Widget) {
owidget = &Widget{}
*owidget = *widget
return owidget
}
// TODO: Test this
// TODO: Add support for zone:id. Perhaps, carry a ZoneID property around in *Header? It might allow some weirdness like frontend[5] which matches any zone with an ID of 5 but it would be a tad faster than verifying each zone, although it might be problematic if users end up relying on this behaviour for areas which don't pass IDs to the widgets system but *probably* should
func (widget *Widget) Allowed(zone string) bool {
for _, loc := range strings.Split(widget.Location, "|") {
if loc == "global" || loc == zone {
return true
} else if len(loc) > 0 && loc[0] == '!' {
loc = loc[1:]
if loc != "global" && loc != zone {
return true
}
}
}
return false
}
// TODO: Refactor
func (widget *Widget) Build(hvars interface{}) (string, error) {
if widget.Literal {
return widget.Body, nil
}
if widget.BuildFunc != nil {
return widget.BuildFunc(widget, hvars)
}
var header = hvars.(*Header)
err := header.Theme.RunTmpl(widget.Body, hvars, header.Writer)
return "", err
}
type WidgetEdit struct {
*Widget
Data map[string]string
}
func (widget *WidgetEdit) Create() error {
data, err := json.Marshal(widget.Data)
if err != nil {
return err
}
_, err = widgetStmts.create.Exec(widget.Position, widget.Side, widget.Type, widget.Enabled, widget.Location, data)
if err != nil {
return err
}
// Reload the dock
widgets, err := getDockWidgets(widget.Side)
if err != nil {
return err
}
setDock(widget.Side, widgets)
return nil
}
func (widget *WidgetEdit) Commit() error {
data, err := json.Marshal(widget.Data)
if err != nil {
return err
}
_, err = widgetStmts.update.Exec(widget.Position, widget.Side, widget.Type, widget.Enabled, widget.Location, data, widget.ID)
if err != nil {
return err
}
// Reload the dock
widgets, err := getDockWidgets(widget.Side)
if err != nil {
return err
}
setDock(widget.Side, widgets)
return nil
}

View File

@ -0,0 +1,41 @@
package common
import "errors"
// TODO: Move this into it's own package to make neater and tidier
type searchAndFilter struct {
*Header
Forums []*Forum
}
func widgetSearchAndFilter(widget *Widget, hvars interface{}) (out string, err error) {
header := hvars.(*Header)
user := header.CurrentUser
var forums []*Forum
var canSee []int
if user.IsSuperAdmin {
canSee, err = Forums.GetAllVisibleIDs()
if err != nil {
return "", err
}
} else {
group, err := Groups.Get(user.Group)
if err != nil {
// TODO: Revisit this
return "", errors.New("Something weird happened")
}
canSee = group.CanSee
}
for _, fid := range canSee {
forum := Forums.DirtyGet(fid)
if forum.ParentID == 0 && forum.Name != "" && forum.Active {
forums = append(forums, forum)
}
}
saf := &searchAndFilter{header, forums}
err = saf.Header.Theme.RunTmpl("widget_search_and_filter", saf, saf.Header.Writer)
return "", err
}

39
common/widget_store.go Normal file
View File

@ -0,0 +1,39 @@
package common
import (
"database/sql"
"sync"
)
var Widgets *DefaultWidgetStore
type DefaultWidgetStore struct {
widgets map[int]*Widget
sync.RWMutex
}
func NewDefaultWidgetStore() *DefaultWidgetStore {
return &DefaultWidgetStore{widgets: make(map[int]*Widget)}
}
func (widgets *DefaultWidgetStore) Get(id int) (*Widget, error) {
widgets.RLock()
defer widgets.RUnlock()
widget, ok := widgets.widgets[id]
if !ok {
return widget, sql.ErrNoRows
}
return widget, nil
}
func (widgets *DefaultWidgetStore) set(widget *Widget) {
widgets.Lock()
defer widgets.Unlock()
widgets.widgets[widget.ID] = widget
}
func (widgets *DefaultWidgetStore) delete(id int) {
widgets.Lock()
defer widgets.Unlock()
delete(widgets.widgets, id)
}

55
common/widget_wol.go Normal file
View File

@ -0,0 +1,55 @@
package common
import (
"bytes"
"net/http/httptest"
"github.com/Azareal/Gosora/common/phrases"
)
type wolUsers struct {
*Header
Name string
Users []*User
UserCount int
}
func wolInit(widget *Widget, schedule *WidgetScheduler) error {
schedule.Add(widget)
return nil
}
func wolBuild(widget *Widget, hvars interface{}) (string, error) {
ucount := WsHub.UserCount()
// We don't want a ridiculously long list, so we'll show the number if it's too high and only show staff individually
var users []*User
if ucount < 30 {
users = WsHub.AllUsers()
}
wol := &wolUsers{hvars.(*Header), phrases.GetTmplPhrase("widget.online_name"), users, ucount}
err := wol.Header.Theme.RunTmpl("widget_online", wol, wol.Header.Writer)
return "", err
}
func wolRender(widget *Widget, hvars interface{}) (string, error) {
iTickMask := widget.TickMask.Load()
if iTickMask != nil {
tickMask := iTickMask.(*Widget)
if tickMask != nil {
return tickMask.Body, nil
}
}
return wolBuild(widget, hvars)
}
func wolTick(widget *Widget) error {
w := httptest.NewRecorder()
_, err := wolBuild(widget, SimpleDefaultHeader(w))
if err != nil {
return err
}
buf := new(bytes.Buffer)
buf.ReadFrom(w.Result().Body)
widget.TickMask.Store(buf.String())
return nil
}

View File

@ -0,0 +1,15 @@
package common
import "github.com/Azareal/Gosora/common/phrases"
func wolContextRender(widget *Widget, hvars interface{}) (string, error) {
ucount := WsHub.UserCount()
// We don't want a ridiculously long list, so we'll show the number if it's too high and only show staff individually
var users []*User
if ucount < 30 {
users = WsHub.AllUsers()
}
wol := &wolUsers{hvars.(*Header), phrases.GetTmplPhrase("widget.online_name"), users, ucount}
err := wol.Header.Theme.RunTmpl("widget_online", wol, wol.Header.Writer)
return "", err
}

View File

@ -1,37 +1,32 @@
/* Copyright Azareal 2017 - 2018 */
/* Copyright Azareal 2017 - 2019 */
package common
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"html/template"
"strings"
"sync"
"github.com/Azareal/Gosora/query_gen"
"sync/atomic"
)
// TODO: Clean this file up
var Docks WidgetDocks
var widgetUpdateMutex sync.RWMutex
type WidgetDock struct {
Items []*Widget
Scheduler *WidgetScheduler
}
type WidgetDocks struct {
LeftOfNav []*Widget
RightOfNav []*Widget
LeftSidebar []*Widget
RightSidebar []*Widget
LeftSidebar WidgetDock
RightSidebar WidgetDock
//PanelLeft []Menus
Footer []*Widget
}
type Widget struct {
Enabled bool
Location string // Coming Soon: overview, topics, topic / topic_view, forums, forum, global
Position int
Body string
Side string
Type string
Literal bool
Footer WidgetDock
}
type WidgetMenu struct {
@ -50,21 +45,6 @@ type NameTextPair struct {
Text template.HTML
}
type WidgetStmts struct {
getWidgets *sql.Stmt
}
var widgetStmts WidgetStmts
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
widgetStmts = WidgetStmts{
getWidgets: acc.Select("widgets").Columns("position, side, type, active, location, data").Orderby("position ASC").Prepare(),
}
return acc.FirstError()
})
}
func preparseWidget(widget *Widget, wdata string) (err error) {
prebuildWidget := func(name string, data interface{}) (string, error) {
var b bytes.Buffer
@ -73,25 +53,30 @@ func preparseWidget(widget *Widget, wdata string) (err error) {
}
sbytes := []byte(wdata)
widget.Literal = true
// TODO: Split these hard-coded items out of this file and into the files for the individual widget types
switch widget.Type {
case "simple":
case "simple", "about":
var tmp NameTextPair
err = json.Unmarshal(sbytes, &tmp)
if err != nil {
return err
}
widget.Body, err = prebuildWidget("widget_simple", tmp)
case "about":
var tmp NameTextPair
err = json.Unmarshal(sbytes, &tmp)
if err != nil {
return err
}
widget.Body, err = prebuildWidget("widget_about", tmp)
widget.Body, err = prebuildWidget("widget_"+widget.Type, tmp)
case "search_and_filter":
widget.Literal = false
widget.BuildFunc = widgetSearchAndFilter
case "wol":
widget.Literal = false
widget.InitFunc = wolInit
widget.BuildFunc = wolRender
widget.TickFunc = wolTick
case "wol_context":
widget.Literal = false
widget.BuildFunc = wolContextRender
default:
widget.Body = wdata
}
widget.Literal = true
// TODO: Test this
// TODO: Should we toss this through a proper parser rather than crudely replacing it?
@ -115,6 +100,37 @@ func preparseWidget(widget *Widget, wdata string) (err error) {
return err
}
func GetDockList() []string {
return []string{
"leftOfNav",
"rightOfNav",
"rightSidebar",
"footer",
}
}
func GetDock(dock string) []*Widget {
switch dock {
case "leftOfNav":
return Docks.LeftOfNav
case "rightOfNav":
return Docks.RightOfNav
case "rightSidebar":
return Docks.RightSidebar.Items
case "footer":
return Docks.Footer.Items
}
return nil
}
func HasDock(dock string) bool {
switch dock {
case "leftOfNav", "rightOfNav", "rightSidebar", "footer":
return true
}
return false
}
func BuildWidget(dock string, header *Header) (sbody string) {
var widgets []*Widget
if !header.Theme.HasDock(dock) {
@ -143,9 +159,9 @@ func BuildWidget(dock string, header *Header) (sbody string) {
}
return ""
case "rightSidebar":
widgets = Docks.RightSidebar
widgets = Docks.RightSidebar.Items
case "footer":
widgets = Docks.Footer
widgets = Docks.Footer.Items
}
for _, widget := range widgets {
@ -163,93 +179,138 @@ func BuildWidget(dock string, header *Header) (sbody string) {
return sbody
}
// TODO: Test this
// TODO: Add support for zone:id. Perhaps, carry a ZoneID property around in *Header? It might allow some weirdness like frontend[5] which matches any zone with an ID of 5 but it would be a tad faster than verifying each zone, although it might be problematic if users end up relying on this behaviour for areas which don't pass IDs to the widgets system but *probably* should
func (widget *Widget) Allowed(zone string) bool {
for _, loc := range strings.Split(widget.Location, "|") {
if loc == "global" || loc == zone {
return true
} else if len(loc) > 0 && loc[0] == '!' {
loc = loc[1:]
if loc != "global" && loc != zone {
return true
}
func getDockWidgets(dock string) (widgets []*Widget, err error) {
rows, err := widgetStmts.getDockList.Query(dock)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var widget = &Widget{Position: 0, Side: dock}
err = rows.Scan(&widget.ID, &widget.Position, &widget.Type, &widget.Enabled, &widget.Location, &widget.RawBody)
if err != nil {
return nil, err
}
}
return false
}
// TODO: Refactor
func (widget *Widget) Build(hvars interface{}) (string, error) {
if widget.Literal {
return widget.Body, nil
err = preparseWidget(widget, widget.RawBody)
if err != nil {
return nil, err
}
Widgets.set(widget)
widgets = append(widgets, widget)
}
var header = hvars.(*Header)
err := header.Theme.RunTmpl(widget.Body, hvars, header.Writer)
return "", err
return widgets, rows.Err()
}
// TODO: Make a store for this?
func InitWidgets() error {
rows, err := widgetStmts.getWidgets.Query()
leftOfNavWidgets, err := getDockWidgets("leftOfNav")
if err != nil {
return err
}
defer rows.Close()
var data string
var leftOfNavWidgets []*Widget
var rightOfNavWidgets []*Widget
var leftSidebarWidgets []*Widget
var rightSidebarWidgets []*Widget
var footerWidgets []*Widget
for rows.Next() {
var widget = &Widget{Position: 0}
err = rows.Scan(&widget.Position, &widget.Side, &widget.Type, &widget.Enabled, &widget.Location, &data)
if err != nil {
return err
}
err = preparseWidget(widget, data)
if err != nil {
return err
}
switch widget.Side {
case "leftOfNav":
leftOfNavWidgets = append(leftOfNavWidgets, widget)
case "rightOfNav":
rightOfNavWidgets = append(rightOfNavWidgets, widget)
case "left":
leftSidebarWidgets = append(leftSidebarWidgets, widget)
case "right":
rightSidebarWidgets = append(rightSidebarWidgets, widget)
case "footer":
footerWidgets = append(footerWidgets, widget)
}
rightOfNavWidgets, err := getDockWidgets("rightOfNav")
if err != nil {
return err
}
err = rows.Err()
leftSidebarWidgets, err := getDockWidgets("leftSidebar")
if err != nil {
return err
}
rightSidebarWidgets, err := getDockWidgets("rightSidebar")
if err != nil {
return err
}
footerWidgets, err := getDockWidgets("footer")
if err != nil {
return err
}
// TODO: Let themes set default values for widget docks, and let them lock in particular places with their stuff, e.g. leftOfNav and rightOfNav
widgetUpdateMutex.Lock()
Docks.LeftOfNav = leftOfNavWidgets
Docks.RightOfNav = rightOfNavWidgets
Docks.LeftSidebar = leftSidebarWidgets
Docks.RightSidebar = rightSidebarWidgets
Docks.Footer = footerWidgets
widgetUpdateMutex.Unlock()
DebugLog("Docks.LeftOfNav", Docks.LeftOfNav)
DebugLog("Docks.RightOfNav", Docks.RightOfNav)
DebugLog("Docks.LeftSidebar", Docks.LeftSidebar)
DebugLog("Docks.RightSidebar", Docks.RightSidebar)
DebugLog("Docks.Footer", Docks.Footer)
setDock("leftOfNav", leftOfNavWidgets)
setDock("rightOfNav", rightOfNavWidgets)
setDock("leftSidebar", leftSidebarWidgets)
setDock("rightSidebar", rightSidebarWidgets)
setDock("footer", footerWidgets)
AddScheduledSecondTask(Docks.LeftSidebar.Scheduler.Tick)
AddScheduledSecondTask(Docks.RightSidebar.Scheduler.Tick)
AddScheduledSecondTask(Docks.Footer.Scheduler.Tick)
return nil
}
func releaseWidgets(widgets []*Widget) {
for _, widget := range widgets {
if widget.ShutdownFunc != nil {
widget.ShutdownFunc(widget)
}
}
}
// TODO: Use atomics
func setDock(dock string, widgets []*Widget) {
var dockHandle = func(dockWidgets []*Widget) {
widgetUpdateMutex.Lock()
DebugLog(dock, widgets)
releaseWidgets(dockWidgets)
}
var dockHandle2 = func(dockWidgets WidgetDock) WidgetDock {
dockHandle(dockWidgets.Items)
if dockWidgets.Scheduler == nil {
dockWidgets.Scheduler = &WidgetScheduler{}
}
for _, widget := range widgets {
if widget.InitFunc != nil {
widget.InitFunc(widget, dockWidgets.Scheduler)
}
}
dockWidgets.Scheduler.Store()
return WidgetDock{widgets, dockWidgets.Scheduler}
}
switch dock {
case "leftOfNav":
dockHandle(Docks.LeftOfNav)
Docks.LeftOfNav = widgets
case "rightOfNav":
dockHandle(Docks.RightOfNav)
Docks.RightOfNav = widgets
case "leftSidebar":
Docks.LeftSidebar = dockHandle2(Docks.LeftSidebar)
case "rightSidebar":
Docks.RightSidebar = dockHandle2(Docks.RightSidebar)
case "footer":
Docks.Footer = dockHandle2(Docks.Footer)
default:
fmt.Printf("bad dock '%s'\n", dock)
return
}
widgetUpdateMutex.Unlock()
}
type WidgetScheduler struct {
widgets []*Widget
store atomic.Value
}
func (schedule *WidgetScheduler) Add(widget *Widget) {
schedule.widgets = append(schedule.widgets, widget)
}
func (schedule *WidgetScheduler) Store() {
schedule.store.Store(schedule.widgets)
}
func (schedule *WidgetScheduler) Tick() error {
widgets := schedule.store.Load().([]*Widget)
for _, widget := range widgets {
if widget.TickFunc == nil {
continue
}
err := widget.TickFunc(widget.Copy())
if err != nil {
return err
}
}
return nil
}

View File

@ -251,24 +251,36 @@ func (hub *WsHubImpl) getUsers(uids []int) (wsUsers []*WSUser, err error) {
if len(uids) == 0 {
return nil, errWsNouser
}
hub.evenUserLock.RLock()
// We don't want to keep a lock on this for too long, so we'll accept some nil pointers
for _, uid := range uids {
wsUsers = append(wsUsers, hub.evenOnlineUsers[uid])
var appender = func(lock *sync.RWMutex, users map[int]*WSUser) {
lock.RLock()
defer lock.RUnlock()
// We don't want to keep a lock on this for too long, so we'll accept some nil pointers
for _, uid := range uids {
wsUsers = append(wsUsers, users[uid])
}
}
hub.evenUserLock.RUnlock()
hub.oddUserLock.RLock()
// We don't want to keep a lock on this for too long, so we'll accept some nil pointers
for _, uid := range uids {
wsUsers = append(wsUsers, hub.oddOnlineUsers[uid])
}
hub.oddUserLock.RUnlock()
appender(&hub.evenUserLock, hub.evenOnlineUsers)
appender(&hub.oddUserLock, hub.oddOnlineUsers)
if len(wsUsers) == 0 {
return nil, errWsNouser
}
return wsUsers, nil
}
// For Widget WOL, please avoid using this as it might wind up being really long and slow without the right safeguards
func (hub *WsHubImpl) AllUsers() (users []*User) {
var appender = func(lock *sync.RWMutex, userMap map[int]*WSUser) {
lock.RLock()
defer lock.RUnlock()
for _, user := range userMap {
users = append(users, user.User)
}
}
appender(&hub.evenUserLock, hub.evenOnlineUsers)
appender(&hub.oddUserLock, hub.oddOnlineUsers)
return users
}
func (hub *WsHubImpl) removeUser(uid int) {
if uid%2 == 0 {
hub.evenUserLock.Lock()

File diff suppressed because it is too large Load Diff

View File

@ -2,23 +2,24 @@
package main
var dbTablePrimaryKeys = map[string]string{
"users":"uid",
"users_groups":"gid",
"users_groups_scheduler":"uid",
"polls":"pollID",
"registration_logs":"rlid",
"activity_stream":"asid",
"users_avatar_queue":"uid",
"word_filters":"wfid",
"menus":"mid",
"login_logs":"lid",
"users_2fa_keys":"uid",
"polls":"pollID",
"activity_stream":"asid",
"pages":"pid",
"forums":"fid",
"topics":"tid",
"replies":"rid",
"attachments":"attachID",
"revisions":"reviseID",
"users_avatar_queue":"uid",
"forums":"fid",
"users_2fa_keys":"uid",
"users_groups_scheduler":"uid",
"menu_items":"miid",
"registration_logs":"rlid",
"users":"uid",
"users_replies":"rid",
"word_filters":"wfid",
"menus":"mid",
"pages":"pid",
"widgets":"wid",
}

View File

@ -1,7 +1,7 @@
/*
*
* Gosora MySQL Interface
* Copyright Azareal 2017 - 2018
* Copyright Azareal 2017 - 2019
*
*/
package install

View File

@ -2,7 +2,7 @@
*
* Gosora PostgreSQL Interface
* Under heavy development
* Copyright Azareal 2017 - 2018
* Copyright Azareal 2017 - 2019
*
*/
package install

View File

@ -1,5 +1,6 @@
{
"Name": "english",
"IsoCode":"en",
"Levels": {
"Level": "<span class='level_hideable'>Level </span>{0}",
@ -154,6 +155,7 @@
"panel_themes":"Theme Manager",
"panel_themes_menus":"Menu Manager",
"panel_themes_menus_edit":"Menu Editor",
"panel_themes_widgets":"Widget Manager",
"panel_backups":"Backups",
"panel_registration_logs":"Registration Logs",
"panel_mod_logs":"Mod Action Logs",
@ -664,6 +666,10 @@
"footer_made_with_love":"Made with love by Azareal",
"footer_theme_selector_aria":"Change the site's appearance",
"widget.online_name":"Online Users",
"widget.online_none_online":"No one is online.",
"widget.online_some_online":"There are %d users online.",
"option_yes":"Yes",
"option_no":"No",
@ -895,6 +901,23 @@
"panel_themes_menus_edit_update_button":"Update",
"panel_themes_menus_create_button":"Create",
"panel_themes_widgets_head":"Widgets",
"panel_themes_widgets_disabled":"disabled",
"panel_themes_widgets_new":"New Widget",
"panel_themes_widgets_type":"Type",
"panel_themes_widgets_type_about":"About",
"panel_themes_widgets_type_simple":"Simple",
"panel_themes_widgets_type_wol":"Online Users",
"panel_themes_widgets_type_wol_context":"Online User Context",
"panel_themes_widgets_type_search_and_filter":"Search & Filter",
"panel_themes_widgets_enabled":"Enabled",
"panel_themes_widgets_location":"Location",
"panel_themes_widgets_name":"Name",
"panel_themes_widgets_body":"Body",
"panel_themes_widgets_raw_body":"Body",
"panel_themes_widgets_save":"Save",
"panel_themes_widgets_delete":"Delete",
"panel_settings_head":"Settings",
"panel_setting_head":"Edit Setting",
"panel_setting_name":"Setting Name",

View File

@ -4,6 +4,7 @@
* Copyright Azareal 2016 - 2019
*
*/
// Package main contains the main initialisation logic for Gosora
package main // import "github.com/Azareal/Gosora"
import (
@ -78,6 +79,7 @@ func afterDBInit() (err error) {
}
log.Print("Initialising the widgets")
common.Widgets = common.NewDefaultWidgetStore()
err = common.InitWidgets()
if err != nil {
return errors.WithStack(err)

View File

@ -750,6 +750,7 @@ func TestReplyStore(t *testing.T) {
expectNilErr(t, err)
expect(t, topic.PostCount == 3, fmt.Sprintf("TID #1's post count should be three, not %d", topic.PostCount))
// TODO: Expand upon this
rid, err = common.Rstore.Create(topic, "hiii", "::1", 1)
expectNilErr(t, err)
replyTest(rid, topic.ID, 1, "hiii", "::1")
@ -1014,6 +1015,7 @@ func TestWordFilters(t *testing.T) {
// TODO: Add deletion tests
}
// TODO: Expand upon the valid characters which can go in URLs?
func TestSlugs(t *testing.T) {
var res string
var msgList = &MEPairList{nil}
@ -1050,6 +1052,64 @@ func TestSlugs(t *testing.T) {
}
}
func TestWidgets(t *testing.T) {
_, err := common.Widgets.Get(1)
recordMustNotExist(t, err, "There shouldn't be any widgets by default")
widgets := common.Docks.RightSidebar
expect(t, len(widgets) == 0, fmt.Sprintf("RightSidebar should have 0 items, not %d", len(widgets)))
widget := &common.Widget{Position: 0, Side: "rightSidebar", Type: "simple", Enabled: true, Location: "global"}
ewidget := &common.WidgetEdit{widget, map[string]string{"Name": "Test", "Text": "Testing"}}
err = ewidget.Create()
expectNilErr(t, err)
// TODO: Do a test for the widget body
widget2, err := common.Widgets.Get(1)
expectNilErr(t, err)
expect(t, widget2.Position == widget.Position, "wrong position")
expect(t, widget2.Side == widget.Side, "wrong side")
expect(t, widget2.Type == widget.Type, "wrong type")
expect(t, widget2.Enabled, "not enabled")
expect(t, widget2.Location == widget.Location, "wrong location")
widgets = common.Docks.RightSidebar
expect(t, len(widgets) == 1, fmt.Sprintf("RightSidebar should have 1 item, not %d", len(widgets)))
expect(t, widgets[0].Position == widget.Position, "wrong position")
expect(t, widgets[0].Side == widget.Side, "wrong side")
expect(t, widgets[0].Type == widget.Type, "wrong type")
expect(t, widgets[0].Enabled, "not enabled")
expect(t, widgets[0].Location == widget.Location, "wrong location")
widget2.Enabled = false
ewidget = &common.WidgetEdit{widget2, map[string]string{"Name": "Test", "Text": "Testing"}}
err = ewidget.Commit()
expectNilErr(t, err)
widget2, err = common.Widgets.Get(1)
expectNilErr(t, err)
expect(t, widget2.Position == widget.Position, "wrong position")
expect(t, widget2.Side == widget.Side, "wrong side")
expect(t, widget2.Type == widget.Type, "wrong type")
expect(t, !widget2.Enabled, "not enabled")
expect(t, widget2.Location == widget.Location, "wrong location")
widgets = common.Docks.RightSidebar
expect(t, len(widgets) == 1, fmt.Sprintf("RightSidebar should have 1 item, not %d", len(widgets)))
expect(t, widgets[0].Position == widget.Position, "wrong position")
expect(t, widgets[0].Side == widget.Side, "wrong side")
expect(t, widgets[0].Type == widget.Type, "wrong type")
expect(t, !widgets[0].Enabled, "not enabled")
expect(t, widgets[0].Location == widget.Location, "wrong location")
err = widget2.Delete()
expectNilErr(t, err)
_, err = common.Widgets.Get(1)
recordMustNotExist(t, err, "There shouldn't be any widgets anymore")
widgets = common.Docks.RightSidebar
expect(t, len(widgets) == 0, fmt.Sprintf("RightSidebar should have 0 items, not %d", len(widgets)))
}
func TestAuth(t *testing.T) {
// bcrypt likes doing stupid things, so this test will probably fail
realPassword := "Madame Cassandra's Mystic Orb"

View File

@ -24,6 +24,7 @@ func init() {
addPatch(10, patch10)
addPatch(11, patch11)
addPatch(12, patch12)
addPatch(13, patch13)
}
func patch0(scanner *bufio.Scanner) (err error) {
@ -392,11 +393,11 @@ var acc = qgen.NewAcc
var itoa = strconv.Itoa
func patch10(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.AddColumn("topics", tblColumn{"attachCount", "int", 0, false, false, "0"}))
err := execStmt(qgen.Builder.AddColumn("topics", tblColumn{"attachCount", "int", 0, false, false, "0"}, nil))
if err != nil {
return err
}
err = execStmt(qgen.Builder.AddColumn("topics", tblColumn{"lastReplyID", "int", 0, false, false, "0"}))
err = execStmt(qgen.Builder.AddColumn("topics", tblColumn{"lastReplyID", "int", 0, false, false, "0"}, nil))
if err != nil {
return err
}
@ -432,7 +433,7 @@ func patch10(scanner *bufio.Scanner) error {
}
func patch11(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.AddColumn("replies", tblColumn{"attachCount", "int", 0, false, false, "0"}))
err := execStmt(qgen.Builder.AddColumn("replies", tblColumn{"attachCount", "int", 0, false, false, "0"}, nil))
if err != nil {
return err
}
@ -504,3 +505,12 @@ func patch12(scanner *bufio.Scanner) error {
}
return nil
}
func patch13(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.AddColumn("widgets", tblColumn{"wid", "int", 0, false, true, ""}, &tblKey{"wid", "primary"}))
if err != nil {
return err
}
return nil
}

View File

@ -1,6 +1,6 @@
// +build pgsql
/* Copyright Azareal 2016 - 2018 */
/* Copyright Azareal 2016 - 2019 */
/* Super experimental and incomplete. DON'T USE IT YET! */
package main

View File

@ -23,8 +23,7 @@ function ajaxError(xhr,status,errstr) {
console.trace();
}
function postLink(event)
{
function postLink(event) {
event.preventDefault();
let formAction = $(event.target).closest('a').attr("href");
$.ajax({ url: formAction, type: "POST", dataType: "json", error: ajaxError, data: {js: "1"} });
@ -115,9 +114,7 @@ function loadAlerts(menuAlerts) {
}
alertList = [];
alertMapping = {};
for(var i in data.msgs) {
addAlert(data.msgs[i]);
}
for(var i in data.msgs) addAlert(data.msgs[i]);
console.log("data.msgCount:",data.msgCount)
alertCount = data.msgCount;
updateAlertList(menuAlerts)
@ -271,8 +268,13 @@ function runWebSockets() {
let msgblocks = SplitN(message," ",3);
if(msgblocks.length < 3) continue;
if(message.startsWith("set ")) {
let oldInnerHTML = document.querySelector(msgblocks[1]).innerHTML;
if(msgblocks[2]==oldInnerHTML) continue;
document.querySelector(msgblocks[1]).innerHTML = msgblocks[2];
} else if(message.startsWith("set-class ")) {
// Fix to stop the inspector from getting all jittery
let oldClassName = document.querySelector(msgblocks[1]).className;
if(msgblocks[2]==oldClassName) continue;
document.querySelector(msgblocks[1]).className = msgblocks[2];
}
}

View File

@ -94,8 +94,12 @@ function DoNothingButPassBack(item) {
return item;
}
function fetchPhrases() {
fetch("/api/phrases/?query=status,topic_list,alerts")
function initPhrases() {
fetchPhrases("status,topic_list,alerts")
}
function fetchPhrases(plist) {
fetch("/api/phrases/?query="+plist)
.then((resp) => resp.json())
.then((data) => {
console.log("loaded phrase endpoint data");
@ -103,9 +107,7 @@ function fetchPhrases() {
Object.keys(tmplInits).forEach((key) => {
let phrases = [];
let tmplInit = tmplInits[key];
for(let phraseName of tmplInit) {
phrases.push(data[phraseName]);
}
for(let phraseName of tmplInit) phrases.push(data[phraseName]);
console.log("Adding phrases");
console.log("key:",key);
console.log("phrases:",phrases);
@ -115,9 +117,7 @@ function fetchPhrases() {
let prefixes = {};
Object.keys(data).forEach((key) => {
let prefix = key.split(".")[0];
if(prefixes[prefix]===undefined) {
prefixes[prefix] = {};
}
if(prefixes[prefix]===undefined) prefixes[prefix] = {};
prefixes[prefix][key] = data[key];
});
Object.keys(prefixes).forEach((prefix) => {
@ -146,7 +146,7 @@ function fetchPhrases() {
loadScript("template_topics_topic.js", () => {
console.log("Loaded template_topics_topic.js");
toLoad--;
if(toLoad===0) fetchPhrases();
if(toLoad===0) initPhrases();
});
} else {
me = {User:{ID:0,Session:""},Site:{"MaxRequestSize":0}};

66
public/widgets.js Normal file
View File

@ -0,0 +1,66 @@
"use strict";
$(document).ready(() => {
let clickHandle = function(event){
console.log("in clickHandle")
event.preventDefault();
let eparent = $(this).closest(".editable_parent");
eparent.find(".hide_on_block_edit").addClass("edit_opened");
eparent.find(".show_on_block_edit").addClass("edit_opened");
eparent.addClass("in_edit");
eparent.find(".widget_save").click(() => {
eparent.find(".hide_on_block_edit").removeClass("edit_opened");
eparent.find(".show_on_block_edit").removeClass("edit_opened");
eparent.removeClass("in_edit");
});
eparent.find(".widget_delete").click(function(event) {
event.preventDefault();
eparent.remove();
let formData = new URLSearchParams();
formData.append("session",me.User.Session);
let req = new XMLHttpRequest();
let target = this.closest("a").getAttribute("href");
req.open("POST",target,true);
req.send(formData);
});
};
$(".widget_item a").click(clickHandle);
let changeHandle = function(event){
let wtype = this.options[this.selectedIndex].value;
let typeBlock = this.closest(".widget_edit").querySelector(".wtypes");
typeBlock.className = "wtypes wtype_"+wtype;
};
$(".wtype_sel").change(changeHandle);
$(".widget_new a").click(function(event){
console.log("clicked widget_new a")
let widgetList = this.closest(".panel_widgets");
let widgetNew = this.closest(".widget_new");
let widgetTmpl = document.getElementById("widgetTmpl").querySelector(".widget_item");
let node = widgetTmpl.cloneNode(true);
node.querySelector(".wside").value = this.getAttribute("data-dock");
widgetList.insertBefore(node,widgetNew);
$(".widget_item a").unbind("click");
$(".widget_item a").click(clickHandle);
$(".wtype_sel").unbind("change");
$(".wtype_sel").change(changeHandle);
});
$(".widget_save").click(function(event){
console.log("in .widget_save")
event.preventDefault();
event.stopPropagation();
let pform = this.closest("form");
let data = new URLSearchParams();
for (const pair of new FormData(pform)) data.append(pair[0], pair[1]);
data.append("session",me.User.Session);
var req = new XMLHttpRequest();
req.open("POST", pform.getAttribute("action"));
req.send(data);
});
});

View File

@ -108,8 +108,8 @@ func (build *builder) CreateTable(table string, charset string, collation string
return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys))
}
func (build *builder) AddColumn(table string, column DBTableColumn) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.AddColumn("", table, column))
func (build *builder) AddColumn(table string, column DBTableColumn, key *DBTableKey) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.AddColumn("", table, column, key))
}
func (build *builder) AddIndex(table string, iname string, colname string) (stmt *sql.Stmt, err error) {

View File

@ -135,7 +135,8 @@ func (adapter *MssqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum
}
// TODO: Test this, not sure if some things work
func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
// TODO: Add support for keys
func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn,key *DBTableKey) (string, error) {
if table == "" {
return "", errors.New("You need a name for this table")
}

View File

@ -173,13 +173,23 @@ func (adapter *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum
// TODO: Support AFTER column
// TODO: Test to make sure everything works here
func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) {
if table == "" {
return "", errors.New("You need a name for this table")
}
column, size, end := adapter.parseColumn(column)
querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + ";"
querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end
if key != nil {
querystr += " " + key.Type
if key.Type != "unique" {
querystr += " key"
} else if key.Type == "primary" {
querystr += " first"
}
}
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "add-column", querystr)
return querystr, nil

View File

@ -113,7 +113,7 @@ func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset stri
}
// TODO: Implement this
func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn,key *DBTableKey) (string, error) {
if table == "" {
return "", errors.New("You need a name for this table")
}

View File

@ -108,7 +108,7 @@ type Adapter interface {
CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error)
// TODO: Some way to add indices and keys
// TODO: Test this
AddColumn(name string, table string, column DBTableColumn) (string, error)
AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error)
AddIndex(name string, table string, iname string, colname string) (string, error)
SimpleInsert(name string, table string, columns string, fields string) (string, error)
SimpleUpdate(up *updatePrebuilder) (string, error)

View File

@ -2,7 +2,7 @@
*
* Query Generator Library
* WIP Under Construction
* Copyright Azareal 2017 - 2018
* Copyright Azareal 2017 - 2019
*
*/
package qgen

View File

@ -177,6 +177,12 @@ func panelRoutes() *RouteGroup {
Action("panel.ThemesMenuItemDeleteSubmit", "/panel/themes/menus/item/delete/submit/", "extraData"),
Action("panel.ThemesMenuItemOrderSubmit", "/panel/themes/menus/item/order/edit/submit/", "extraData"),
View("panel.ThemesWidgets", "/panel/themes/widgets/"),
//View("panel.ThemesWidgetsEdit", "/panel/themes/widgets/edit/", "extraData"),
Action("panel.ThemesWidgetsEditSubmit", "/panel/themes/widgets/edit/submit/", "extraData"),
Action("panel.ThemesWidgetsCreateSubmit", "/panel/themes/widgets/create/submit/"),
Action("panel.ThemesWidgetsDeleteSubmit", "/panel/themes/widgets/delete/submit/", "extraData"),
View("panel.Plugins", "/panel/plugins/"),
Action("panel.PluginsActivate", "/panel/plugins/activate/", "extraData"),
Action("panel.PluginsDeactivate", "/panel/plugins/deactivate/", "extraData"),

View File

@ -1,7 +1,7 @@
/*
*
* Gosora Route Handlers
* Copyright Azareal 2016 - 2018
* Copyright Azareal 2016 - 2019
*
*/
package main

View File

@ -2,6 +2,9 @@ package panel
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
@ -330,3 +333,155 @@ func ThemesMenuItemOrderSubmit(w http.ResponseWriter, r *http.Request, user comm
return successRedirect("/panel/themes/menus/edit/"+strconv.Itoa(mid), w, r, isJs)
}
func ThemesWidgets(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "themes_widgets", "themes")
if ferr != nil {
return ferr
}
if !user.Perms.ManageThemes {
return common.NoPermissions(w, r, user)
}
basePage.Header.AddScript("widgets.js")
var docks = make(map[string][]common.WidgetEdit)
for _, name := range common.GetDockList() {
var widgets []common.WidgetEdit
for _, widget := range common.GetDock(name) {
var data = make(map[string]string)
err := json.Unmarshal([]byte(widget.RawBody), &data)
if err != nil {
return common.InternalError(err, w, r)
}
widgets = append(widgets, common.WidgetEdit{widget, data})
}
docks[name] = widgets
}
pi := common.PanelWidgetListPage{basePage, docks, common.WidgetEdit{&common.Widget{ID: 0, Type: "simple"}, make(map[string]string)}}
return renderTemplate("panel_themes_widgets", w, r, user, &pi)
}
func widgetsParseInputs(r *http.Request, widget *common.Widget) (*common.WidgetEdit, error) {
var data = make(map[string]string)
widget.Enabled = (r.FormValue("wenabled") == "1")
widget.Location = r.FormValue("wlocation")
if widget.Location == "" {
return nil, errors.New("You need to specify a location for this widget.")
}
widget.Side = r.FormValue("wside")
if !common.HasDock(widget.Side) {
return nil, errors.New("The widget dock you specified doesn't exist.")
}
var wtype = r.FormValue("wtype")
switch wtype {
case "simple", "about":
data["Name"] = r.FormValue("wname")
if data["Name"] == "" {
return nil, errors.New("You need to specify a title for this widget.")
}
data["Text"] = r.FormValue("wtext")
if data["Text"] == "" {
return nil, errors.New("You need to fill in the body for this widget.")
}
widget.Type = wtype // ? - Are we sure we should be directly assigning user provided data even if it's validated?
case "wol", "search_and_filter":
widget.Type = wtype // ? - Are we sure we should be directly assigning user provided data even if it's validated?
default:
return nil, errors.New("Unknown widget type")
}
return &common.WidgetEdit{widget, data}, nil
}
// ThemesWidgetsEditSubmit is an action which is triggered when someone sends an update request for a widget
func ThemesWidgetsEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, swid string) common.RouteError {
fmt.Println("in ThemesWidgetsEditSubmit")
_, ferr := common.SimplePanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
isJs := (r.PostFormValue("js") == "1")
if !user.Perms.ManageThemes {
return common.NoPermissionsJSQ(w, r, user, isJs)
}
wid, err := strconv.Atoi(swid)
if err != nil {
return common.LocalErrorJSQ(phrases.GetErrorPhrase("id_must_be_integer"), w, r, user, isJs)
}
widget, err := common.Widgets.Get(wid)
if err == sql.ErrNoRows {
return common.NotFoundJSQ(w, r, nil, isJs)
} else if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs)
}
ewidget, err := widgetsParseInputs(r, widget.Copy())
if err != nil {
return common.LocalErrorJSQ(err.Error(), w, r, user, isJs)
}
err = ewidget.Commit()
if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs)
}
return successRedirect("/panel/themes/widgets/", w, r, isJs)
}
// ThemesWidgetsCreateSubmit is an action which is triggered when someone sends a create request for a widget
func ThemesWidgetsCreateSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
fmt.Println("in ThemesWidgetsCreateSubmit")
isJs := (r.PostFormValue("js") == "1")
_, ferr := common.SimplePanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
if !user.Perms.ManageThemes {
return common.NoPermissionsJSQ(w, r, user, isJs)
}
ewidget, err := widgetsParseInputs(r, &common.Widget{})
if err != nil {
return common.LocalErrorJSQ(err.Error(), w, r, user, isJs)
}
err = ewidget.Create()
if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs)
}
return successRedirect("/panel/themes/widgets/", w, r, isJs)
}
func ThemesWidgetsDeleteSubmit(w http.ResponseWriter, r *http.Request, user common.User, swid string) common.RouteError {
_, ferr := common.SimplePanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
isJs := (r.PostFormValue("js") == "1")
if !user.Perms.ManageThemes {
return common.NoPermissionsJSQ(w, r, user, isJs)
}
wid, err := strconv.Atoi(swid)
if err != nil {
return common.LocalErrorJSQ(phrases.GetErrorPhrase("id_must_be_integer"), w, r, user, isJs)
}
widget, err := common.Widgets.Get(wid)
if err == sql.ErrNoRows {
return common.NotFound(w, r, nil)
} else if err != nil {
return common.InternalError(err, w, r)
}
err = widget.Delete()
if err != nil {
return common.InternalError(err, w, r)
}
return successRedirect("/panel/themes/widgets/", w, r, isJs)
}

View File

@ -64,7 +64,6 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
return common.NoPermissions(w, r, user)
}
header.Title = topic.Title
header.Zone = "view_topic"
header.Path = common.BuildTopicURL(common.NameToSlug(topic.Title), topic.ID)
// TODO: Cache ContentHTML when possible?
@ -253,6 +252,9 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
}
}
header.Zone = "view_topic"
header.ZoneID = topic.ID
header.ZoneData = topic
rerr := renderTemplate("topic", w, r, header, tpage)
counters.TopicViewCounter.Bump(topic.ID) // TODO: Move this into the router?
counters.ForumViewCounter.Bump(topic.ParentID)

View File

@ -1,8 +1,10 @@
CREATE TABLE [widgets] (
[wid] int not null IDENTITY,
[position] int not null,
[side] nvarchar (100) not null,
[type] nvarchar (100) not null,
[active] bit DEFAULT 0 not null,
[location] nvarchar (100) not null,
[data] nvarchar (MAX) DEFAULT '' not null
[data] nvarchar (MAX) DEFAULT '' not null,
primary key([wid])
);

View File

@ -1,8 +1,10 @@
CREATE TABLE `widgets` (
`wid` int not null AUTO_INCREMENT,
`position` int not null,
`side` varchar(100) not null,
`type` varchar(100) not null,
`active` boolean DEFAULT 0 not null,
`location` varchar(100) not null,
`data` text not null
`data` text not null,
primary key(`wid`)
);

View File

@ -1,8 +1,10 @@
CREATE TABLE "widgets" (
`wid` serial not null,
`position` int not null,
`side` varchar (100) not null,
`type` varchar (100) not null,
`active` boolean DEFAULT 0 not null,
`location` varchar (100) not null,
`data` text DEFAULT '' not null
`data` text DEFAULT '' not null,
primary key(`wid`)
);

View File

@ -4,7 +4,7 @@
<div class="colstack_item rowlist">
<!-- TODO: Do we need this inline CSS? -->
{{range .ItemList}}
<div class="rowitem">
<div class="rowitem{{if not .Success}} bg_red{{end}}">
<span style="float: left;">
<span>{{if .Success}}{{lang "account_logins_success"}}{{else}}{{lang "account_logins_failure"}}"{{end}}</span><br />
<small style="margin-left: 2px;font-size: 12px;" title="{{.IPAddress}}">{{.IPAddress}}</small>

View File

@ -5,7 +5,7 @@
<div class="rowitem"><h1 itemprop="name">{{lang "forums_head"}}</h1></div>
</div>
<div class="rowblock forum_list">
{{range .ItemList}}<div class="rowitem {{if (.Desc) or (.LastTopic.Title)}}datarow {{end}}"itemprop="itemListElement" itemscope
{{range .ItemList}}<div id="forum_{{.ID}}" class="rowitem{{if (.Desc) or (.LastTopic.Title)}} datarow{{end}}" itemprop="itemListElement" itemscope
itemtype="http://schema.org/ListItem">
<span class="forum_left shift_left">
<a href="{{.Link}}" itemprop="item">{{.Name}}</a><br />

View File

@ -4,14 +4,12 @@
<title>{{.Title}} | {{.Header.Site.Name}}</title>
<link href="/static/{{.Header.Theme.Name}}/main.css" rel="stylesheet" type="text/css">
{{range .Header.Stylesheets}}
<link href="/static/{{.}}" rel="stylesheet" type="text/css">
{{end}}
<link href="/static/{{.}}" rel="stylesheet" type="text/css">{{end}}
<meta property="x-loggedin" content="{{.CurrentUser.Loggedin}}" />
<script type="text/javascript" src="/static/init.js"></script>
<script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script>
{{range .Header.Scripts}}
<script type="text/javascript" src="/static/{{.}}"></script>
{{end}}
<script type="text/javascript" src="/static/{{.}}"></script>{{end}}
<script type="text/javascript" src="/static/global.js"></script>
<meta name="viewport" content="width=device-width,initial-scale = 1.0, maximum-scale=1.0,user-scalable=no" />
{{if .Header.MetaDesc}}<meta name="description" content="{{.Header.MetaDesc}}" />{{end}}

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{.FriendlyAgent}}{{lang "panel_statistics_views_head_suffix"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{lang "panel_statistics_user_agents_head"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{.FriendlyAgent}}{{lang "panel_statistics_views_head_suffix"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{lang "panel_statistics_forums_head"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{.FriendlyAgent}}{{lang "panel_statistics_views_head_suffix"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{lang "panel_statistics_languages_head"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{lang "panel_statistics_post_counts_head"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{.Agent}}{{lang "panel_statistics_views_head_suffix"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{lang "panel_statistics_referrers_head"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{.Route}}{{lang "panel_statistics_views_head_suffix"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{lang "panel_statistics_routes_head"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{.FriendlyAgent}}{{lang "panel_statistics_views_head_suffix"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{lang "panel_statistics_operating_systems_head"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -0,0 +1,8 @@
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{lang "panel_statistics_topic_counts_head"}}</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -6,14 +6,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<h1>{{lang "panel_statistics_requests_head"}}</h1>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>{{lang "panel_statistics_time_range_two_days"}}</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>{{lang "panel_statistics_time_range_one_day"}}</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_twelve_hours"}}</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>{{lang "panel_statistics_time_range_six_hours"}}</option>
</select>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
</form>

View File

@ -26,7 +26,7 @@
</div>
{{if eq .Zone "themes"}}
<div class="rowitem passive submenu"><a href="/panel/themes/menus/">{{lang "panel_menu_menus"}}</a></div>
<div class="rowitem passive submenu"><a href="#">{{lang "panel_menu_widgets"}}</a></div>
<div class="rowitem passive submenu"><a href="/panel/themes/widgets/">{{lang "panel_menu_widgets"}}</a></div>
{{end}}
{{end}}
</div>

View File

@ -7,7 +7,7 @@
</div>
<div id="panel_modlogs" class="colstack_item rowlist">
{{range .Logs}}
<div class="rowitem panel_compactrow">
<div class="rowitem panel_compactrow {{if not .Success}}bg_red{{end}}">
<span{{if not .Success}} class="panel_registration_attempt"{{end}} style="float: left;">
<span>{{if not .Success}}{{lang "panel_logs_registration_attempt"}}: {{end}}{{.Username}} ({{lang "panel_logs_registration_email"}}: {{.Email}}){{if .ParsedReason}} ({{lang "panel_logs_registration_reason"}}: {{.ParsedReason}}){{end}}</span>
{{if $.CurrentUser.Perms.ViewIPs}}<br /><small style="margin-left: 2px;font-size: 12px;" title="{{.IPAddress}}">{{.IPAddress}}</small>{{end}}

View File

@ -5,7 +5,7 @@
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_menus_head"}}</h1></div>
</div>
<div id="panel_settings" class="colstack_item rowlist">
<div id="panel_menus" class="colstack_item rowlist">
{{range .ItemList}}
<div class="rowitem panel_compactrow editable_parent">
<a href="/panel/themes/menus/edit/{{.ID}}" class="editable_block panel_upshift">{{if .Name}}{{.Name}} - {{end}}#{{.ID}}</a>

View File

@ -0,0 +1,62 @@
{{/**
type Widget struct {
Enabled bool
Location string // Coming Soon: overview, topics, topic / topic_view, forums, forum, global
Position int
Body string
Side string
Type string
Literal bool
}
**/}}
{{template "header.html" . }}
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_widgets_head"}}</h1></div>
</div>
{{range $name, $dock := .Docks}}
{{if $dock}}
<div class="colstack_item colstack_head colstack_sub_head">
<div class="rowitem"><h2>{{$name}}</h2></div>
</div>
<div id="panel_widgets_{{$name}}" class="colstack_item rowlist panel_widgets">
{{range $widget := $dock}}
<div id="widget_{{$widget.ID}}" class="rowitem panel_compactrow editable_parent widget_item {{if not .Enabled}}bg_red{{end}}">
<div class="widget_normal editable_block hide_on_block_edit">
<a href="/panel/themes/widgets/edit/{{$widget.ID}}" class="panel_upshift">{{$widget.Type}} <span class="widget_disabled">({{lang "panel_themes_widgets_disabled"}})</span></a>
<a class="panel_compacttext to_right">{{$widget.Location}}</a>
</div>
<div class="widget_edit show_on_block_edit">
<form action="/panel/themes/widgets/edit/submit/{{$widget.ID}}" method="post">
<input class="wside" name="wside" value="{{$name}}" type="hidden" />
{{template "panel_themes_widgets_widget.html" $widget }}
</form>
</div>
</div>
{{end}}
<div class="rowitem panel_compactrow editable_parent widget_new">
<a href="#" data-dock="{{$name}}" class="editable_block panel_upshift">{{lang "panel_themes_widgets_new"}}</a>
</div>
</div>
{{end}}
{{end}}
<div id="widgetTmpl">
<div class="rowitem panel_compactrow editable_parent widget_item blank_widget bg_red">
<div class="widget_normal editable_block hide_on_block_edit">
<a href="#" class="panel_upshift">{{.BlankWidget.Type}} <span class="widget_disabled">({{lang "panel_themes_widgets_disabled"}})</span></a>
<a class="panel_compacttext to_right">{{.BlankWidget.Location}}</a>
</div>
<div class="widget_edit show_on_block_edit">
<form action="/panel/themes/widgets/create/submit/" method="post">
<input name="session" value="{{.CurrentUser.Session}}" type="hidden" />
<input class="wside" name="wside" value="" type="hidden" />
{{template "panel_themes_widgets_widget.html" .BlankWidget }}
</form>
</div>
</div>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -0,0 +1,55 @@
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_themes_widgets_type"}}</a></div>
<div class="formitem">
<select class="wtype_sel" name="wtype">
<option value="about"{{if eq .Type "about"}} selected{{end}}>{{lang "panel_themes_widgets_type_about"}}</option>
<option value="simple"{{if eq .Type "simple"}} selected{{end}}>{{lang "panel_themes_widgets_type_simple"}}</option>
<option value="wol"{{if eq .Type "wol"}} selected{{end}}>{{lang "panel_themes_widgets_type_wol"}}</option>
<!--<option value="wol_context"{{if eq .Type "wol_context"}} selected{{end}}>{{lang "panel_themes_widgets_type_wol_context"}}</option>-->
<!--<option value="search_and_filter"{{if eq .Type "search_and_filter"}} selected{{end}}>{{lang "panel_themes_widgets_type_search_and_filter"}}</option>-->
</select>
</div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_themes_widgets_enabled"}}</a></div>
<div class="formitem">
<select name="wenabled">
<option{{if .Enabled}} selected{{end}} value="1">{{lang "option_yes"}}</option>
<option{{if not .Enabled}} selected{{end}} value="0">{{lang "option_no"}}</option>
</select>
</div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_themes_widgets_location"}}</a></div>
<div class="formitem">
<input name="wlocation" value="{{.Location}}" />
</div>
</div>
<div class="wtypes wtype_{{.Type}}">
<div class="formrow w_simple w_about">
<div class="formitem formlabel"><a>{{lang "panel_themes_widgets_name"}}</a></div>
<div class="formitem">
<input name="wname" value="{{index .Data "Name"}}" />
</div>
</div>
<div class="formrow w_simple w_about">
<div class="formitem formlabel"><a>{{lang "panel_themes_widgets_body"}}</a></div>
<div class="formitem">
<textarea name="wtext">{{index .Data "Text"}}</textarea>
</div>
</div>
<div class="formrow w_default">
<div class="formitem formlabel"><a>{{lang "panel_themes_widgets_raw_body"}}</a></div>
<div class="formitem">
<textarea name="wbody">{{.RawBody}}</textarea>
</div>
</div>
</div>
<div class="formrow form_button_row">
<div class="formitem">
<button name="panel-button" class="formbutton widget_save">{{lang "panel_themes_widgets_save"}}</button>
<a href="/panel/themes/widgets/delete/submit/{{.ID}}">
<button name="panel-button" class="formbutton widget_delete">{{lang "panel_themes_widgets_delete"}}</button>
</a>
</div>
</div>

View File

@ -36,7 +36,7 @@
{{end}}
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button delete_item" aria-label="{{lang "topic.post_delete_aria"}}" data-action="delete"></a>{{end}}
{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item_button hide_on_big" aria-label="{{lang "topic.ip_full_aria"}}" data-action="ip"></a>{{end}}
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="action_button report_item" aria-label="{{lang "topic.report_aria"}}" data-action="report"></a>
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&amp;type=reply" class="action_button report_item" aria-label="{{lang "topic.report_aria"}}" data-action="report"></a>
<a href="#" class="action_button button_menu"></a>
{{end}}
</div>

View File

@ -13,15 +13,15 @@
<span class="controls{{if .LikeCount}} has_likes{{end}}">
<a href="{{.UserLink}}" class="username real_username" rel="author">{{.CreatedByName}}</a>&nbsp;&nbsp;
{{if $.CurrentUser.Perms.LikeItem}}{{if .Liked}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_like_tooltip"}}" aria-label="{{lang "topic.post_like_aria"}}" style="color:#202020;"><button class="username like_label remove_like"></button></a>{{else}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_unlike_tooltip"}}" aria-label="{{lang "topic.post_unlike_aria"}}" style="color:#202020;"><button class="username like_label add_like"></button></a>{{end}}{{end}}
{{if $.CurrentUser.Perms.LikeItem}}{{if .Liked}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_like_tooltip"}}" aria-label="{{lang "topic.post_like_aria"}}"><button class="username like_label remove_like"></button></a>{{else}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_unlike_tooltip"}}" aria-label="{{lang "topic.post_unlike_aria"}}"><button class="username like_label add_like"></button></a>{{end}}{{end}}
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
{{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 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>
{{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.IPAddress}}' 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}}&amp;type=reply" class="mod_button report_item" title="{{lang "topic.post_flag_tooltip"}}" aria-label="{{lang "topic.post_flag_aria"}}" rel="nofollow"><button class="username report_item flag_label"></button></a>
<a class="username hide_on_micro like_count">{{.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="{{lang "topic.post_like_count_tooltip"}}"></a>

View File

@ -1,7 +1,7 @@
<div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}">
<div class="rowitem topic_left passive datarow">
<span class="selector"></span>
<a href="{{.Creator.Link}}"><img src="{{.Creator.MicroAvatar}}" height="64" alt="{{.Creator.Name}}'s Avatar" title="{{.Creator.Name}}'s Avatar" /></a>
<a href="{{.Creator.Link}}"><img src="{{.Creator.MicroAvatar}}" height=64 alt="{{.Creator.Name}}'s Avatar" title="{{.Creator.Name}}'s Avatar" /></a>
<span class="topic_inner_left">
<a class="rowtopic" href="{{.Link}}" itemprop="itemListElement" title="{{.Title}}"><span>{{.Title}}</span></a> {{if .ForumName}}<a class="rowsmall parent_forum" href="{{.ForumLink}}" title="{{.ForumName}}">{{.ForumName}}</a>{{end}}
<br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a>
@ -24,7 +24,7 @@
</div>
<div class="rowitem topic_right passive datarow">
<div class="topic_right_inside">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height=64 alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a>

View File

@ -0,0 +1,12 @@
<div class="rowblock rowhead widget_online">
<div class="rowitem"><h1>{{.Name}}</h1></div>
</div>
<div class="rowblock rowlist bgavatars not_grid widget_online">
{{if lt .UserCount 30}}
{{range .Users}}<div class="rowitem" style="background-image: url('{{.Avatar}}');">
<img src="{{.Avatar}}" class="bgsub" alt="{{.Name}}'s Avatar" />
<a class="rowTitle" href="{{.Link}}">{{.Name}}</a>
</div>
{{else}}<div class="rowitem rowmsg">{{lang "widget.online_none_online"}}</div>{{end}}
{{else}}<div class="rowitem rowmsg">{{langf "widget.online_some_online" .UserCount}}</div>{{end}}
</div>

View File

@ -0,0 +1,7 @@
<div class="search widget_search">
<input name="widget_search" placeholder="Search" />
</div>
<div class="rowblock filter_list widget_filter">
{{range .Forums}} <div class="rowitem filter_item" data-fid="{{.ID}}">{{.Name}}</div>
{{end}}
</div>

View File

@ -236,12 +236,16 @@ ul {
.rowhead:not(:first-child), .opthead:not(:first-child), .colstack_head:not(:first-child) {
margin-top: 8px;
}
.rowhead h1, .opthead h1, .colstack_head h1 {
.rowhead h1, .opthead h1, .colstack_head h1,
.rowhead h2, .opthead h2, .colstack_head h2 {
font-size: 19px;
font-weight: normal;
color: var(--lightened-primary-text-color);
display: inline-block;
}
.rowhead h2, .opthead h2, .colstack_head h2 {
font-size: 17px;
}
.colstack_head a h1 {
color: var(--primary-link-color);
}
@ -251,7 +255,7 @@ ul {
.colstack_head h1 {
font-size: 18px;
}
h1, h3 {
h1, h2, h3 {
-webkit-margin-before: 0;
-webkit-margin-after: 0;
margin-block-start: 0;
@ -534,7 +538,12 @@ input[type=checkbox]:checked + label .sel {
.pollinput:not(:only-child):not(:first-child) {
margin-bottom: 5px;
}
.auto_hide, .show_on_edit:not(.edit_opened), .hide_on_edit.edit_opened, .link_select:not(.link_opened) {
.auto_hide,
.show_on_edit:not(.edit_opened),
.hide_on_edit.edit_opened,
.show_on_block_edit:not(.edit_opened),
.hide_on_block_edit.edit_opened,
.link_select:not(.link_opened) {
display: none;
}

View File

@ -309,6 +309,29 @@
}
*/
.widget_normal {
display: flex;
width: 100%;
}
#widgetTmpl {
display: none;
}
.widget_disabled {
display: none;
}
.bg_red .widget_disabled {
display: inline;
}
.wtypes .formrow {
display: none;
}
.wtype_about .w_about, .wtype_simple .w_simple, .wtype_wol .w_wol, .wtype_default .w_default {
display: block;
}
.panel_widgets {
margin-bottom: 18px;
}
.pageset {
margin-left: 16px;
}

View File

@ -42,6 +42,9 @@
.to_right {
margin-left: auto;
}
.bg_red {
background-color: rgb(88,68,68) !important;
}
@media (max-width: 420px) {
.colstack {

View File

@ -170,11 +170,29 @@ li a {
.sidebar .rowblock:not(.topic_list):not(.rowhead):not(.opthead) .rowitem {
margin-left: 12px;
}
.sidebar .search {
margin-left: 12px;
}
.widget_search:first-child {
margin-top: 36px;
}
.widget_search input {
width: 100%;
height: 30px;
margin-left: 0px;
}
.filter_list {
margin-top: 5px;
}
.colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem {
border-radius: 3px;
background-color: #444444;
padding: 16px;
}
.filter_item {
margin-bottom: 5px;
padding: 4px;
}
.colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem:not(:last-child) {
margin-bottom: 8px;
}
@ -261,6 +279,10 @@ h1, h2, h3, h4, h5 {
.sidebar .rowhead h1 {
font-size: 20px;
}
.sidebar .rowhead:not(:first-child) h1 {
margin-top: 12px;
font-size: 19px;
}
h2 {
font-size: 18px;
@ -335,7 +357,14 @@ h2 {
.more_topic_block_active {
display: block;
}
.hide_ajax_topic, .auto_hide, .show_on_edit:not(.edit_opened), .hide_on_edit.edit_opened, .link_select:not(.link_opened) {
.hide_ajax_topic,
.auto_hide,
.show_on_edit:not(.edit_opened),
.hide_on_edit.edit_opened,
.show_on_block_edit:not(.edit_opened),
.hide_on_block_edit.edit_opened,
.link_select:not(.link_opened) {
display: none !important;
}
@ -866,7 +895,7 @@ input[type=checkbox]:checked + label .sel {
display: flex;
}
.rowlist.bgavatars, .micro_grid {
.rowlist.bgavatars:not(.not_grid), .micro_grid {
display: grid;
/*grid-gap: 16px;
grid-row-gap: 8px;*/
@ -888,6 +917,9 @@ input[type=checkbox]:checked + label .sel {
margin-bottom: 10px;
padding: 16px;
}
.rowlist.not_grid .rowitem {
flex-direction: row;
}
.rowlist.bgavatars .bgsub, .rowlist.bgavatars .rowTitle {
margin-left: auto;
margin-right: auto;
@ -901,6 +933,15 @@ input[type=checkbox]:checked + label .sel {
font-size: 18px;
margin-top: 4px;
}
.rowlist.bgavatars.not_grid .bgsub {
height: 36px;
width: 36px;
margin-left: 8px;
margin-right: 12px;
}
.rowlist.bgavatars.not_grid .rowTitle {
margin-left: 0px;
}
.ip_search_block {
margin-bottom: 12px;

View File

@ -36,12 +36,21 @@
.colstack_right .colstack_head {
margin-bottom: 5px;
}
.colstack_right .colstack_head + .colstack_head:not(:first-child) {
margin-top: 5px;
}
.colstack_right .colstack_head h1 {
font-size: 21px;
}
.colstack_right .colstack_item.the_form, .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem {
background-color: #444444;
}
.colstack_right .colstack_head.colstack_sub_head:not(:first-child) {
margin-top: 6px;
}
.colstack_head + .colstack_head.colstack_sub_head:not(:first-child) {
margin-top: 2px;
}
.rowitem {
display: flex;
}
@ -258,6 +267,39 @@ button, .formbutton, .panel_right_button:not(.has_inner_button), #panel_users .p
margin-bottom: auto;
}
.widget_normal {
display: flex;
width: 100%;
}
.bg_red.in_edit.widget_item {
background-color: #444444 !important;
}
.widget_item .form_button_row .rowitem {
display: flex;
}
.widget_edit .form_button_row .formitem a {
display: inline;
}
.colstack_right .colstack_item.the_form, .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem.widget_new {
padding-top: 12px;
padding-bottom: 12px;
}
#widgetTmpl {
display: none;
}
.widget_disabled {
display: none;
}
.bg_red .widget_disabled {
display: inline;
}
.wtypes .formrow {
display: none;
}
.wtype_about .w_about, .wtype_simple .w_simple, .wtype_wol .w_wol, .wtype_default .w_default {
display: block;
}
#panel_debug .grid_stat:not(.grid_stat_head) {
margin-bottom: 5px;
}

View File

@ -179,16 +179,18 @@ a {
margin-top: 8px;
padding: 12px;
}
.rowitem h1 {
.rowitem h1, .rowitem h2 {
font-size: 16px;
font-weight: normal;
display: inline;
}
h1, h2, h3 {
-webkit-margin-before: 0;
-webkit-margin-after: 0;
margin-block-start: 0;
margin-block-end: 0;
margin-top: 0px;
margin-bottom: 0px;
display: inline;
font-weight: normal;
}
.rowsmall {
font-size: 12px;
@ -208,7 +210,13 @@ a {
float: left;
width: calc(70% - 24px);
}
.colstack_left:empty, .colstack_right:empty, .show_on_edit:not(.edit_opened), .hide_on_edit.edit_opened, .link_select:not(.link_opened) {
.colstack_left:empty,
.colstack_right:empty,
.show_on_edit:not(.edit_opened),
.hide_on_edit.edit_opened,
.show_on_block_edit:not(.edit_opened),
.hide_on_block_edit.edit_opened,
.link_select:not(.link_opened) {
display: none;
}

View File

@ -103,6 +103,22 @@
stroke: hsl(359,98%,43%) !important;
}
#widgetTmpl {
display: none;
}
.widget_disabled {
display: none;
}
.bg_red .widget_disabled {
display: inline;
}
.wtypes .formrow {
display: none;
}
.wtype_about .w_about, .wtype_simple .w_simple, .wtype_wol .w_wol, .wtype_default .w_default {
display: block;
}
.pageset {
margin-left: 0px;
margin-bottom: 0px;

View File

@ -249,7 +249,8 @@ li a {
margin-top: -3px;
margin-bottom: -2px;
}
.rowhead h1, .colstack_head h1 {
.rowhead h1, .colstack_head h1,
.rowhead h2, .colstack_head h2 {
-webkit-margin-before: 0;
-webkit-margin-after: 0;
margin-block-start: 0;
@ -743,7 +744,14 @@ button.username {
.mention {
font-weight: bold;
}
.show_on_edit:not(.edit_opened), .hide_on_edit.edit_opened, .auto_hide, .hide_on_big, .show_on_mobile, .link_select:not(.link_opened) {
.show_on_edit:not(.edit_opened),
.hide_on_edit.edit_opened,
.show_on_block_edit:not(.edit_opened),
.hide_on_block_edit.edit_opened,
.auto_hide,
.hide_on_big,
.show_on_mobile,
.link_select:not(.link_opened) {
display: none;
}

View File

@ -162,4 +162,20 @@
background-color: white;
border: 1px solid hsl(0,0%,85%);
}
#widgetTmpl {
display: none;
}
.widget_disabled {
display: none;
}
.bg_red .widget_disabled {
display: inline;
}
.wtypes .formrow {
display: none;
}
.wtype_about .w_about, .wtype_simple .w_simple, .wtype_wol .w_wol, .wtype_default .w_default {
display: block;
}