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:
parent
5db5bc0c7e
commit
8f2f47e8aa
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* Gosora Common Resources
|
||||
* Copyright Azareal 2018 - 2019
|
||||
* Copyright Azareal 2018 - 2020
|
||||
*
|
||||
*/
|
||||
package common // import "github.com/Azareal/Gosora/common"
|
||||
|
@ -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)
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* Gosora Forum Store
|
||||
* Copyright Azareal 2017 - 2018
|
||||
* Copyright Azareal 2017 - 2019
|
||||
*
|
||||
*/
|
||||
package common
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* OttoJS Plugin Module
|
||||
* Copyright Azareal 2016 - 2018
|
||||
* Copyright Azareal 2016 - 2019
|
||||
*
|
||||
*/
|
||||
package common
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* Reply Resources File
|
||||
* Copyright Azareal 2016 - 2018
|
||||
* Copyright Azareal 2016 - 2019
|
||||
*
|
||||
*/
|
||||
package common
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* Gosora Task System
|
||||
* Copyright Azareal 2017 - 2018
|
||||
* Copyright Azareal 2017 - 2019
|
||||
*
|
||||
*/
|
||||
package common
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -94,6 +94,7 @@ func NewCTemplateSet() *CTemplateSet {
|
||||
"reltime": true,
|
||||
"scope": true,
|
||||
"dyntmpl": true,
|
||||
"index": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* Gosora Topic File
|
||||
* Copyright Azareal 2017 - 2018
|
||||
* Copyright Azareal 2017 - 2019
|
||||
*
|
||||
*/
|
||||
package common
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* Utility Functions And Stuff
|
||||
* Copyright Azareal 2017 - 2018
|
||||
* Copyright Azareal 2017 - 2019
|
||||
*
|
||||
*/
|
||||
package common
|
||||
|
@ -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
148
common/widget.go
Normal 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
|
||||
}
|
41
common/widget_search_and_filter.go
Normal file
41
common/widget_search_and_filter.go
Normal 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
39
common/widget_store.go
Normal 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
55
common/widget_wol.go
Normal 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
|
||||
}
|
15
common/widget_wol_context.go
Normal file
15
common/widget_wol_context.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
561
gen_router.go
561
gen_router.go
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* Gosora MySQL Interface
|
||||
* Copyright Azareal 2017 - 2018
|
||||
* Copyright Azareal 2017 - 2019
|
||||
*
|
||||
*/
|
||||
package install
|
||||
|
@ -2,7 +2,7 @@
|
||||
*
|
||||
* Gosora PostgreSQL Interface
|
||||
* Under heavy development
|
||||
* Copyright Azareal 2017 - 2018
|
||||
* Copyright Azareal 2017 - 2019
|
||||
*
|
||||
*/
|
||||
package install
|
||||
|
@ -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",
|
||||
|
2
main.go
2
main.go
@ -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)
|
||||
|
60
misc_test.go
60
misc_test.go
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
2
pgsql.go
2
pgsql.go
@ -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
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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
66
public/widgets.js
Normal 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);
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -2,7 +2,7 @@
|
||||
*
|
||||
* Query Generator Library
|
||||
* WIP Under Construction
|
||||
* Copyright Azareal 2017 - 2018
|
||||
* Copyright Azareal 2017 - 2019
|
||||
*
|
||||
*/
|
||||
package qgen
|
||||
|
@ -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"),
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* Gosora Route Handlers
|
||||
* Copyright Azareal 2016 - 2018
|
||||
* Copyright Azareal 2016 - 2019
|
||||
*
|
||||
*/
|
||||
package main
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
);
|
@ -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`)
|
||||
);
|
@ -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`)
|
||||
);
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -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}}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
8
templates/panel_analytics_time_range.html
Normal file
8
templates/panel_analytics_time_range.html
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}}
|
||||
|
@ -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>
|
||||
|
62
templates/panel_themes_widgets.html
Normal file
62
templates/panel_themes_widgets.html
Normal 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" . }}
|
55
templates/panel_themes_widgets_widget.html
Normal file
55
templates/panel_themes_widgets_widget.html
Normal 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>
|
@ -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}}&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>
|
||||
|
@ -13,15 +13,15 @@
|
||||
<span class="controls{{if .LikeCount}} has_likes{{end}}">
|
||||
|
||||
<a href="{{.UserLink}}" class="username real_username" rel="author">{{.CreatedByName}}</a>
|
||||
{{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}}&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>
|
||||
|
||||
|
@ -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>
|
||||
|
12
templates/widget_online.html
Normal file
12
templates/widget_online.html
Normal 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>
|
7
templates/widget_search_and_filter.html
Normal file
7
templates/widget_search_and_filter.html
Normal 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>
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -42,6 +42,9 @@
|
||||
.to_right {
|
||||
margin-left: auto;
|
||||
}
|
||||
.bg_red {
|
||||
background-color: rgb(88,68,68) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.colstack {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user