Finish Grid system
This commit is contained in:
parent
4a60506c99
commit
89f47edd59
33
README.md
33
README.md
@ -7,9 +7,31 @@ __Demo:__
|
|||||||
|
|
||||||
<img src="./example/screencast.gif" alt="demo" width="600">
|
<img src="./example/screencast.gif" alt="demo" width="600">
|
||||||
|
|
||||||
__Grid layout:(incomplete)__
|
__Grid layout:__
|
||||||
|
|
||||||
<img src="./example/grid.gif" alt="grid" width="400">
|
Expressive syntax, using [12 columns grid system](http://www.w3schools.com/bootstrap/bootstrap_grid_system.asp)
|
||||||
|
```go
|
||||||
|
import ui "github.com/gizak/termui"
|
||||||
|
// init and create widgets...
|
||||||
|
|
||||||
|
// build
|
||||||
|
ui.Body.AddRows(
|
||||||
|
ui.NewRow(
|
||||||
|
ui.NewCol(6, 0, widget0),
|
||||||
|
ui.NewCol(6, 0, widget1)),
|
||||||
|
ui.NewRow(
|
||||||
|
ui.NewCol(3, 0, widget2),
|
||||||
|
ui.NewCol(3, 0, widget30, widget31, widget32),
|
||||||
|
ui.NewCol(6, 0, widget4)))
|
||||||
|
|
||||||
|
// calculate layout
|
||||||
|
ui.Body.Align()
|
||||||
|
|
||||||
|
ui.Render(ui.Body)
|
||||||
|
```
|
||||||
|
[demo code:](https://github.com/gizak/termui/blob/master/example/grid.go)
|
||||||
|
|
||||||
|
<img src="./example/grid.gif" alt="grid" width="500">
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -58,7 +80,7 @@ Note that components can be overlapped (I'd rather call this a feature...), `Ren
|
|||||||
|
|
||||||
## Themes
|
## Themes
|
||||||
|
|
||||||
_All_ colors in _all_ components _can_ be changed at _any_ time, while there provides some predefined color schemes:
|
_All_ colors in _all_ components can be changed at _any_ time, while there provides some predefined color schemes:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// for now there are only two themes: default and helloworld
|
// for now there are only two themes: default and helloworld
|
||||||
@ -114,8 +136,11 @@ The `helloworld` color scheme drops in some colors!
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] Grid layout
|
- [x] Grid layout
|
||||||
- [ ] Event system
|
- [ ] Event system
|
||||||
|
- [ ] Canvas widget
|
||||||
|
- [ ] Refine APIs
|
||||||
|
- [ ] Focusable widgets
|
||||||
|
|
||||||
## License
|
## License
|
||||||
This library is under the [MIT License](http://opensource.org/licenses/MIT)
|
This library is under the [MIT License](http://opensource.org/licenses/MIT)
|
||||||
|
BIN
example/grid.gif
BIN
example/grid.gif
Binary file not shown.
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 782 KiB |
@ -1,6 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
/*
|
|
||||||
import ui "github.com/gizak/termui"
|
import ui "github.com/gizak/termui"
|
||||||
import tm "github.com/nsf/termbox-go"
|
import tm "github.com/nsf/termbox-go"
|
||||||
import "math"
|
import "math"
|
||||||
@ -50,10 +49,43 @@ func main() {
|
|||||||
lc.AxesColor = ui.ColorWhite
|
lc.AxesColor = ui.ColorWhite
|
||||||
lc.LineColor = ui.ColorYellow | ui.AttrBold
|
lc.LineColor = ui.ColorYellow | ui.AttrBold
|
||||||
|
|
||||||
ui.Body.Rows = []ui.Row{
|
gs := make([]*ui.Gauge, 3)
|
||||||
|
for i := range gs {
|
||||||
|
gs[i] = ui.NewGauge()
|
||||||
|
gs[i].Height = 2
|
||||||
|
gs[i].HasBorder = false
|
||||||
|
gs[i].Percent = i * 10
|
||||||
|
gs[i].PaddingBottom = 1
|
||||||
|
gs[i].BarColor = ui.ColorRed
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := ui.NewList()
|
||||||
|
ls.HasBorder = false
|
||||||
|
ls.Items = []string{
|
||||||
|
"[1] Downloading File 1",
|
||||||
|
"", // == \newline
|
||||||
|
"[2] Downloading File 2",
|
||||||
|
"",
|
||||||
|
"[3] Uploading File 3",
|
||||||
|
}
|
||||||
|
ls.Height = 5
|
||||||
|
|
||||||
|
par := ui.NewPar("<> This row has 3 columns\n<- Widgets can be stacked up like left side\n<- Stacked widgets are treated as a single widget")
|
||||||
|
par.Height = 5
|
||||||
|
par.Border.Label = "Demonstration"
|
||||||
|
|
||||||
|
// build layout
|
||||||
|
ui.Body.AddRows(
|
||||||
ui.NewRow(
|
ui.NewRow(
|
||||||
ui.NewCol(sp, 6, 0, true),
|
ui.NewCol(6, 0, sp),
|
||||||
ui.NewCol(lc, 6, 0, true))}
|
ui.NewCol(6, 0, lc)),
|
||||||
|
ui.NewRow(
|
||||||
|
ui.NewCol(3, 0, ls),
|
||||||
|
ui.NewCol(3, 0, gs[0], gs[1], gs[2]),
|
||||||
|
ui.NewCol(6, 0, par)))
|
||||||
|
|
||||||
|
// calculate layout
|
||||||
|
ui.Body.Align()
|
||||||
|
|
||||||
draw := func(t int) {
|
draw := func(t int) {
|
||||||
sp.Lines[0].Data = spdata[t:]
|
sp.Lines[0].Data = spdata[t:]
|
||||||
@ -75,7 +107,15 @@ func main() {
|
|||||||
if e.Type == tm.EventKey && e.Ch == 'q' {
|
if e.Type == tm.EventKey && e.Ch == 'q' {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if e.Type == tm.EventResize {
|
||||||
|
ui.Body.Width = ui.TermWidth()
|
||||||
|
ui.Body.Align()
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
|
for _, g := range gs {
|
||||||
|
g.Percent = (g.Percent + 3) % 100
|
||||||
|
}
|
||||||
|
|
||||||
draw(i)
|
draw(i)
|
||||||
i++
|
i++
|
||||||
if i == 102 {
|
if i == 102 {
|
||||||
@ -85,4 +125,3 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
251
grid.go
251
grid.go
@ -1,125 +1,6 @@
|
|||||||
package termui
|
package termui
|
||||||
|
|
||||||
import tm "github.com/nsf/termbox-go"
|
// Bufferers that can be manipulated by Grid
|
||||||
|
|
||||||
/*
|
|
||||||
type container struct {
|
|
||||||
height int
|
|
||||||
width int
|
|
||||||
BgColor Attribute
|
|
||||||
Rows []Row
|
|
||||||
}
|
|
||||||
|
|
||||||
type Row []Col
|
|
||||||
|
|
||||||
type Col struct {
|
|
||||||
Blocks []ColumnBufferer
|
|
||||||
Offset int // 0 ~ 11
|
|
||||||
Span int // 1 ~ 12
|
|
||||||
}
|
|
||||||
|
|
||||||
type ColumnBufferer interface {
|
|
||||||
Bufferer
|
|
||||||
GetHeight() int
|
|
||||||
SetWidth(int)
|
|
||||||
SetX(int)
|
|
||||||
SetY(int)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRow(cols ...Col) Row {
|
|
||||||
return cols
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCol(span, offset int, blocks ...ColumnBufferer) Col {
|
|
||||||
return Col{Blocks: blocks, Span: span, Offset: offset}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highest col is the height of a Row
|
|
||||||
func (r Row) GetHeight() int {
|
|
||||||
h := 0
|
|
||||||
for _, v := range r {
|
|
||||||
if nh := v.GetHeight(); nh > h {
|
|
||||||
h = nh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set width according to its span
|
|
||||||
func (r Row) SetWidth(w int) {
|
|
||||||
for _, c := range r {
|
|
||||||
c.SetWidth(int(float64(w*c.Span) / 12.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set x y
|
|
||||||
func (r Row) SetX(x int) {
|
|
||||||
for i := range r {
|
|
||||||
r[i].SetX(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Row) SetY(y int) {
|
|
||||||
for i := range r {
|
|
||||||
r[i].SetY(y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeight recursively retrieves height of each children, then add them up.
|
|
||||||
func (c Col) GetHeight() int {
|
|
||||||
h := 0
|
|
||||||
for _, v := range c.Blocks {
|
|
||||||
h += c.GetHeight()
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Col) GetWidth() int {
|
|
||||||
w := 0
|
|
||||||
for _, v := range c.Blocks {
|
|
||||||
if nw := v.GetWidth(); nw > w {
|
|
||||||
w = nw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Col) SetWidth(w int) {
|
|
||||||
for i := range c.Blocks {
|
|
||||||
c.SetWidth(w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c container) Buffer() []Point {
|
|
||||||
ps := []Point{}
|
|
||||||
maxw, _ := tm.Size()
|
|
||||||
|
|
||||||
y := 0
|
|
||||||
for _, row := range c.Rows {
|
|
||||||
x := 0
|
|
||||||
maxHeight := 0
|
|
||||||
|
|
||||||
for _, col := range row {
|
|
||||||
if h := col.GetHeight(); h > maxHeight {
|
|
||||||
maxHeight = h
|
|
||||||
}
|
|
||||||
|
|
||||||
w := int(float64(maxw*(col.Span+col.Offset)) / 12.0)
|
|
||||||
if col.GetWidth() > w {
|
|
||||||
col.SetWidth(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
col.SetY(y)
|
|
||||||
col.SetX(x)
|
|
||||||
ps = append(ps, col.Buffer()...)
|
|
||||||
x += w + int(float64(maxw*col.Offset)/12)
|
|
||||||
}
|
|
||||||
y += maxHeight
|
|
||||||
}
|
|
||||||
return ps
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
type LayoutBufferer interface {
|
type LayoutBufferer interface {
|
||||||
Bufferer
|
Bufferer
|
||||||
GetHeight() int
|
GetHeight() int
|
||||||
@ -130,8 +11,8 @@ type LayoutBufferer interface {
|
|||||||
|
|
||||||
// build a layout tree
|
// build a layout tree
|
||||||
type row struct {
|
type row struct {
|
||||||
Cols []*row
|
Cols []*row //children
|
||||||
Widget LayoutBufferer // only leaves hold this
|
Widget LayoutBufferer // root
|
||||||
X int
|
X int
|
||||||
Y int
|
Y int
|
||||||
Width int
|
Width int
|
||||||
@ -140,15 +21,9 @@ type row struct {
|
|||||||
Offset int
|
Offset int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContainer() *row {
|
func (r *row) calcLayout() {
|
||||||
w, _ := tm.Size()
|
|
||||||
r := &row{Width: w, Span: 12, X: 0, Y: 0, Cols: []*row{}}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *row) layout() {
|
|
||||||
r.assignWidth(r.Width)
|
r.assignWidth(r.Width)
|
||||||
r.solveHeight()
|
r.Height = r.solveHeight()
|
||||||
r.assignX(r.X)
|
r.assignX(r.X)
|
||||||
r.assignY(r.Y)
|
r.assignY(r.Y)
|
||||||
}
|
}
|
||||||
@ -163,14 +38,14 @@ func (r *row) isRenderableLeaf() bool {
|
|||||||
|
|
||||||
func (r *row) assignWidth(w int) {
|
func (r *row) assignWidth(w int) {
|
||||||
cw := int(float64(w*r.Span) / 12)
|
cw := int(float64(w*r.Span) / 12)
|
||||||
r.Width = cw
|
r.SetWidth(cw)
|
||||||
|
|
||||||
for i, _ := range r.Cols {
|
for i, _ := range r.Cols {
|
||||||
r.Cols[i].assignWidth(cw)
|
r.Cols[i].assignWidth(cw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bottom up
|
// bottom up, return r's total height
|
||||||
func (r *row) solveHeight() int {
|
func (r *row) solveHeight() int {
|
||||||
if r.isRenderableLeaf() {
|
if r.isRenderableLeaf() {
|
||||||
r.Height = r.Widget.GetHeight()
|
r.Height = r.Widget.GetHeight()
|
||||||
@ -196,9 +71,7 @@ func (r *row) solveHeight() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *row) assignX(x int) {
|
func (r *row) assignX(x int) {
|
||||||
if r.isRenderableLeaf() {
|
r.SetX(x)
|
||||||
r.Widget.SetX(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !r.isLeaf() {
|
if !r.isLeaf() {
|
||||||
acc := 0
|
acc := 0
|
||||||
@ -210,14 +83,12 @@ func (r *row) assignX(x int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.X = x
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *row) assignY(y int) {
|
func (r *row) assignY(y int) {
|
||||||
r.Y = y
|
r.SetY(y)
|
||||||
|
|
||||||
if r.isRenderableLeaf() {
|
if r.isLeaf() {
|
||||||
r.Widget.SetY(y)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,6 +102,31 @@ func (r *row) assignY(y int) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r row) GetHeight() int {
|
||||||
|
return r.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *row) SetX(x int) {
|
||||||
|
r.X = x
|
||||||
|
if r.Widget != nil {
|
||||||
|
r.Widget.SetX(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *row) SetY(y int) {
|
||||||
|
r.Y = y
|
||||||
|
if r.Widget != nil {
|
||||||
|
r.Widget.SetY(y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *row) SetWidth(w int) {
|
||||||
|
r.Width = w
|
||||||
|
if r.Widget != nil {
|
||||||
|
r.Widget.SetWidth(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// recursively merge all widgets buffer
|
// recursively merge all widgets buffer
|
||||||
func (r *row) Buffer() []Point {
|
func (r *row) Buffer() []Point {
|
||||||
merged := []Point{}
|
merged := []Point{}
|
||||||
@ -239,6 +135,12 @@ func (r *row) Buffer() []Point {
|
|||||||
return r.Widget.Buffer()
|
return r.Widget.Buffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for those are not leaves but have a renderable widget
|
||||||
|
if r.Widget != nil {
|
||||||
|
merged = append(merged, r.Widget.Buffer()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect buffer from children
|
||||||
if !r.isLeaf() {
|
if !r.isLeaf() {
|
||||||
for _, c := range r.Cols {
|
for _, c := range r.Cols {
|
||||||
merged = append(merged, c.Buffer()...)
|
merged = append(merged, c.Buffer()...)
|
||||||
@ -248,4 +150,73 @@ func (r *row) Buffer() []Point {
|
|||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
//var Body container
|
type Grid struct {
|
||||||
|
Rows []*row
|
||||||
|
Width int
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
BgColor Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGrid(rows ...*row) *Grid {
|
||||||
|
return &Grid{Rows: rows}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Grid) AddRows(rs ...*row) {
|
||||||
|
g.Rows = append(g.Rows, rs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRow(cols ...*row) *row {
|
||||||
|
rs := &row{Span: 12, Cols: cols}
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCol accepts: widgets are LayoutBufferer or
|
||||||
|
// widgets is A NewRow
|
||||||
|
// Note that if multiple widgets are provided, they will stack up in the col
|
||||||
|
func NewCol(span, offset int, widgets ...LayoutBufferer) *row {
|
||||||
|
r := &row{Span: span, Offset: offset}
|
||||||
|
|
||||||
|
if widgets != nil && len(widgets) == 1 {
|
||||||
|
wgt := widgets[0]
|
||||||
|
nw, isRow := wgt.(*row)
|
||||||
|
if isRow {
|
||||||
|
r.Cols = nw.Cols
|
||||||
|
} else {
|
||||||
|
r.Widget = wgt
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Cols = []*row{}
|
||||||
|
ir := r
|
||||||
|
for _, w := range widgets {
|
||||||
|
nr := &row{Span: 12, Widget: w}
|
||||||
|
ir.Cols = []*row{nr}
|
||||||
|
ir = nr
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate each rows' layout
|
||||||
|
func (g *Grid) Align() {
|
||||||
|
h := 0
|
||||||
|
for _, r := range g.Rows {
|
||||||
|
r.SetWidth(g.Width)
|
||||||
|
r.SetX(g.X)
|
||||||
|
r.SetY(g.Y + h)
|
||||||
|
r.calcLayout()
|
||||||
|
h += r.GetHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Grid) Buffer() []Point {
|
||||||
|
ps := []Point{}
|
||||||
|
for _, r := range g.Rows {
|
||||||
|
ps = append(ps, r.Buffer()...)
|
||||||
|
}
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
var Body *Grid
|
||||||
|
43
grid_test.go
43
grid_test.go
@ -30,22 +30,32 @@ func TestRowWidth(t *testing.T) {
|
|||||||
/
|
/
|
||||||
1100:w
|
1100:w
|
||||||
*/
|
*/
|
||||||
r = &row{
|
/*
|
||||||
Span: 12,
|
r = &row{
|
||||||
Cols: []*row{
|
Span: 12,
|
||||||
&row{Widget: p0, Span: 6},
|
Cols: []*row{
|
||||||
&row{
|
&row{Widget: p0, Span: 6},
|
||||||
Span: 6,
|
&row{
|
||||||
Cols: []*row{
|
Span: 6,
|
||||||
&row{Widget: p1, Span: 6},
|
Cols: []*row{
|
||||||
&row{
|
&row{Widget: p1, Span: 6},
|
||||||
Span: 6,
|
&row{
|
||||||
Cols: []*row{
|
Span: 6,
|
||||||
&row{
|
Cols: []*row{
|
||||||
Span: 12,
|
&row{
|
||||||
Widget: p2,
|
Span: 12,
|
||||||
Cols: []*row{
|
Widget: p2,
|
||||||
&row{Span: 12, Widget: p3}}}}}}}}}
|
Cols: []*row{
|
||||||
|
&row{Span: 12, Widget: p3}}}}}}}}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
r = NewRow(
|
||||||
|
NewCol(6, 0, p0),
|
||||||
|
NewCol(6, 0,
|
||||||
|
NewRow(
|
||||||
|
NewCol(6, 0, p1),
|
||||||
|
NewCol(6, 0, p2, p3))))
|
||||||
|
|
||||||
r.assignWidth(100)
|
r.assignWidth(100)
|
||||||
if r.Width != 100 ||
|
if r.Width != 100 ||
|
||||||
(r.Cols[0].Width) != 50 ||
|
(r.Cols[0].Width) != 50 ||
|
||||||
@ -60,6 +70,7 @@ func TestRowWidth(t *testing.T) {
|
|||||||
|
|
||||||
func TestRowHeight(t *testing.T) {
|
func TestRowHeight(t *testing.T) {
|
||||||
spew.Dump()
|
spew.Dump()
|
||||||
|
|
||||||
if (r.solveHeight()) != 2 ||
|
if (r.solveHeight()) != 2 ||
|
||||||
(r.Cols[1].Cols[1].Height) != 2 ||
|
(r.Cols[1].Cols[1].Height) != 2 ||
|
||||||
(r.Cols[1].Cols[1].Cols[0].Height) != 2 ||
|
(r.Cols[1].Cols[1].Cols[0].Height) != 2 ||
|
||||||
|
22
render.go
22
render.go
@ -8,10 +8,14 @@ type Bufferer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Init() error {
|
func Init() error {
|
||||||
// Body = container{
|
Body = NewGrid()
|
||||||
// BgColor: theme.BodyBg,
|
Body.X = 0
|
||||||
// Rows: []Row{},
|
Body.Y = 0
|
||||||
// }
|
Body.BgColor = theme.BodyBg
|
||||||
|
defer (func() {
|
||||||
|
w, _ := tm.Size()
|
||||||
|
Body.Width = w
|
||||||
|
})()
|
||||||
return tm.Init()
|
return tm.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,6 +23,16 @@ func Close() {
|
|||||||
tm.Close()
|
tm.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TermWidth() int {
|
||||||
|
w, _ := tm.Size()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func TermHeight() int {
|
||||||
|
_, h := tm.Size()
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
// render all from left to right, right could overlap on left ones
|
// render all from left to right, right could overlap on left ones
|
||||||
func Render(rs ...Bufferer) {
|
func Render(rs ...Bufferer) {
|
||||||
tm.Clear(tm.ColorDefault, toTmAttr(theme.BodyBg))
|
tm.Clear(tm.ColorDefault, toTmAttr(theme.BodyBg))
|
||||||
|
Loading…
Reference in New Issue
Block a user