Add theme support
Fix some minor bugs Add default and helloworld themes Update README
This commit is contained in:
parent
affd0d9c07
commit
bc325c986d
26
README.md
26
README.md
@ -13,9 +13,9 @@ __Demo:__
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Each component's layout is a bit like HTML block, which has border and padding.
|
Each component's layout is a bit like HTML block (box model), which has border and padding.
|
||||||
|
|
||||||
The `Border` property can be chosen to hide or display (with its border label), when it comes to display, in this case the space it takes is counted as padding space (i.e. `PaddingTop=PaddingBottom=PaddingLeft=PaddingRight=1`).
|
The `Border` property can be chosen to hide or display (with its border label), when it comes to display, the label takes 1 padding space (i.e. in css: `padding: 1;`, innerHeight and innerWidth therefore shrunk by 1).
|
||||||
|
|
||||||
`````go
|
`````go
|
||||||
import ui "github.com/gizak/termui" // <- ui shortcut, optional
|
import ui "github.com/gizak/termui" // <- ui shortcut, optional
|
||||||
@ -50,11 +50,29 @@ The `Border` property can be chosen to hide or display (with its border label),
|
|||||||
}
|
}
|
||||||
`````
|
`````
|
||||||
|
|
||||||
Note that components can be overlapped (I'd rather call this as a feature...), `Render(rs ...Renderer)` renders its args from left to right (i.e. each component's weight is arising from left to right).
|
Note that components can be overlapped (I'd rather call this a feature...), `Render(rs ...Renderer)` renders its args from left to right (i.e. each component's weight is arising from left to right).
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
|
||||||
|
All colors in all components call be changed at any time, while there provides some predefined color scheme.
|
||||||
|
|
||||||
|
```
|
||||||
|
// for now there are only two themes: default and helloworld
|
||||||
|
termui.UseTheme("helloworld")
|
||||||
|
|
||||||
|
// create components...
|
||||||
|
```
|
||||||
|
The `default ` theme's settings depend on the user's terminal color scheme, which is saying if your terminal default font color is white and background is white, it will be like:
|
||||||
|
|
||||||
|
<img src="./example/themedefault.gif" alt="default" type="image/tiff" width="600">
|
||||||
|
|
||||||
|
The `helloworld` color scheme drops in some colors!
|
||||||
|
|
||||||
|
<img src="./example/themehelloworld.gif" alt="helloworld" type="image/tiff" width="600">
|
||||||
|
|
||||||
## Widgets
|
## Widgets
|
||||||
|
|
||||||
_APIs are subject to change, docs will be added after 2 or 3 commits_
|
_APIs are subject to change, docs will be added after 1 or 2 commits_
|
||||||
|
|
||||||
## GoDoc
|
## GoDoc
|
||||||
|
|
||||||
|
12
bar.go
12
bar.go
@ -20,9 +20,9 @@ type BarChart struct {
|
|||||||
|
|
||||||
func NewBarChart() *BarChart {
|
func NewBarChart() *BarChart {
|
||||||
bc := &BarChart{Block: *NewBlock()}
|
bc := &BarChart{Block: *NewBlock()}
|
||||||
bc.BarColor = ColorCyan
|
bc.BarColor = theme.BarChartBar
|
||||||
bc.NumColor = ColorWhite
|
bc.NumColor = theme.BarChartNum
|
||||||
bc.TextColor = ColorWhite
|
bc.TextColor = theme.BarChartText
|
||||||
bc.BarGap = 1
|
bc.BarGap = 1
|
||||||
bc.BarWidth = 3
|
bc.BarWidth = 3
|
||||||
return bc
|
return bc
|
||||||
@ -62,6 +62,9 @@ func (bc *BarChart) Buffer() []Point {
|
|||||||
p := Point{}
|
p := Point{}
|
||||||
p.Ch = ' '
|
p.Ch = ' '
|
||||||
p.Bg = bc.BarColor
|
p.Bg = bc.BarColor
|
||||||
|
if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent!
|
||||||
|
p.Bg |= AttrReverse
|
||||||
|
}
|
||||||
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j
|
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j
|
||||||
p.Y = bc.innerY + bc.innerHeight - 2 - k
|
p.Y = bc.innerY + bc.innerHeight - 2 - k
|
||||||
ps = append(ps, p)
|
ps = append(ps, p)
|
||||||
@ -83,6 +86,9 @@ func (bc *BarChart) Buffer() []Point {
|
|||||||
p.Ch = bc.dataNum[i][j]
|
p.Ch = bc.dataNum[i][j]
|
||||||
p.Fg = bc.NumColor
|
p.Fg = bc.NumColor
|
||||||
p.Bg = bc.BarColor
|
p.Bg = bc.BarColor
|
||||||
|
if bc.BarColor == ColorDefault { // the same as above
|
||||||
|
p.Bg |= AttrReverse
|
||||||
|
}
|
||||||
if h == 0 {
|
if h == 0 {
|
||||||
p.Bg = bc.BgColor
|
p.Bg = bc.BgColor
|
||||||
}
|
}
|
||||||
|
8
block.go
8
block.go
@ -1,5 +1,6 @@
|
|||||||
package termui
|
package termui
|
||||||
|
|
||||||
|
// basic struct, consider it as css: display:block
|
||||||
type Block struct {
|
type Block struct {
|
||||||
X int
|
X int
|
||||||
Y int
|
Y int
|
||||||
@ -22,7 +23,12 @@ type Block struct {
|
|||||||
func NewBlock() *Block {
|
func NewBlock() *Block {
|
||||||
d := Block{}
|
d := Block{}
|
||||||
d.IsDisplay = true
|
d.IsDisplay = true
|
||||||
d.HasBorder = true
|
d.HasBorder = theme.HasBorder
|
||||||
|
d.Border.BgColor = theme.BorderBg
|
||||||
|
d.Border.FgColor = theme.BorderFg
|
||||||
|
d.Border.LabelBgColor = theme.BorderLabelTextBg
|
||||||
|
d.Border.LabelFgColor = theme.BorderLabelTextFg
|
||||||
|
d.BgColor = theme.BlockBg
|
||||||
d.Width = 2
|
d.Width = 2
|
||||||
d.Height = 2
|
d.Height = 2
|
||||||
return &d
|
return &d
|
||||||
|
2
chart.go
2
chart.go
@ -55,6 +55,8 @@ type LineChart struct {
|
|||||||
|
|
||||||
func NewLineChart() *LineChart {
|
func NewLineChart() *LineChart {
|
||||||
lc := &LineChart{Block: *NewBlock()}
|
lc := &LineChart{Block: *NewBlock()}
|
||||||
|
lc.AxesColor = theme.LineChartAxes
|
||||||
|
lc.LineColor = theme.LineChartLine
|
||||||
lc.Mode = "braille"
|
lc.Mode = "braille"
|
||||||
lc.DotStyle = '•'
|
lc.DotStyle = '•'
|
||||||
lc.axisXLebelGap = 2
|
lc.axisXLebelGap = 2
|
||||||
|
@ -13,7 +13,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
p := ui.NewP(":PRESS q TO QUIT DEMO")
|
p := ui.NewPar(":PRESS q TO QUIT DEMO")
|
||||||
p.Height = 3
|
p.Height = 3
|
||||||
p.Width = 50
|
p.Width = 50
|
||||||
p.TextFgColor = ui.ColorWhite
|
p.TextFgColor = ui.ColorWhite
|
||||||
@ -96,7 +96,7 @@ func main() {
|
|||||||
lc1 := ui.NewLineChart()
|
lc1 := ui.NewLineChart()
|
||||||
lc1.Border.Label = "Line Chart"
|
lc1.Border.Label = "Line Chart"
|
||||||
rndwalk := (func() []float64 {
|
rndwalk := (func() []float64 {
|
||||||
n := 100
|
n := 150
|
||||||
d := make([]float64, n)
|
d := make([]float64, n)
|
||||||
for i := 1; i < n; i++ {
|
for i := 1; i < n; i++ {
|
||||||
if i < 20 {
|
if i < 20 {
|
||||||
@ -116,7 +116,7 @@ func main() {
|
|||||||
lc1.AxesColor = ui.ColorWhite
|
lc1.AxesColor = ui.ColorWhite
|
||||||
lc1.LineColor = ui.ColorYellow | ui.AttrBold
|
lc1.LineColor = ui.ColorYellow | ui.AttrBold
|
||||||
|
|
||||||
p1 := ui.NewP("Hey!\nI am a borderless block!")
|
p1 := ui.NewPar("Hey!\nI am a borderless block!")
|
||||||
p1.HasBorder = false
|
p1.HasBorder = false
|
||||||
p1.Width = 26
|
p1.Width = 26
|
||||||
p1.Height = 2
|
p1.Height = 2
|
||||||
|
144
example/theme.go
Normal file
144
example/theme.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import ui "github.com/gizak/termui"
|
||||||
|
import tm "github.com/nsf/termbox-go"
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := ui.Init()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer ui.Close()
|
||||||
|
|
||||||
|
ui.UseTheme("helloworld")
|
||||||
|
|
||||||
|
p := ui.NewPar(":PRESS q TO QUIT DEMO")
|
||||||
|
p.Height = 3
|
||||||
|
p.Width = 50
|
||||||
|
p.Border.Label = "Text Box"
|
||||||
|
|
||||||
|
strs := []string{"[0] gizak/termui", "[1] editbox.go", "[2] iterrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go"}
|
||||||
|
list := ui.NewList()
|
||||||
|
list.Items = strs
|
||||||
|
list.Border.Label = "List"
|
||||||
|
list.Height = 7
|
||||||
|
list.Width = 25
|
||||||
|
list.Y = 4
|
||||||
|
|
||||||
|
g := ui.NewGauge()
|
||||||
|
g.Percent = 50
|
||||||
|
g.Width = 50
|
||||||
|
g.Height = 3
|
||||||
|
g.Y = 11
|
||||||
|
g.Border.Label = "Gauge"
|
||||||
|
|
||||||
|
spark := ui.NewSparkline()
|
||||||
|
spark.Title = "srv 0:"
|
||||||
|
spdata := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6}
|
||||||
|
spark.Data = spdata
|
||||||
|
|
||||||
|
spark1 := ui.NewSparkline()
|
||||||
|
spark1.Title = "srv 1:"
|
||||||
|
spark1.Data = spdata
|
||||||
|
|
||||||
|
sp := ui.NewSparklines(spark, spark1)
|
||||||
|
sp.Width = 25
|
||||||
|
sp.Height = 7
|
||||||
|
sp.Border.Label = "Sparkline"
|
||||||
|
sp.Y = 4
|
||||||
|
sp.X = 25
|
||||||
|
|
||||||
|
lc := ui.NewLineChart()
|
||||||
|
sinps := (func() []float64 {
|
||||||
|
n := 100
|
||||||
|
ps := make([]float64, n)
|
||||||
|
for i := range ps {
|
||||||
|
ps[i] = 1 + math.Sin(float64(i)/4)
|
||||||
|
}
|
||||||
|
return ps
|
||||||
|
})()
|
||||||
|
|
||||||
|
lc.Border.Label = "Line Chart"
|
||||||
|
lc.Data = sinps
|
||||||
|
lc.Width = 50
|
||||||
|
lc.Height = 11
|
||||||
|
lc.X = 0
|
||||||
|
lc.Y = 14
|
||||||
|
lc.Mode = "dot"
|
||||||
|
|
||||||
|
bc := ui.NewBarChart()
|
||||||
|
bcdata := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
|
||||||
|
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
||||||
|
bc.Border.Label = "Bar Chart"
|
||||||
|
bc.Width = 26
|
||||||
|
bc.Height = 10
|
||||||
|
bc.X = 51
|
||||||
|
bc.Y = 0
|
||||||
|
bc.DataLabels = bclabels
|
||||||
|
|
||||||
|
lc1 := ui.NewLineChart()
|
||||||
|
lc1.Border.Label = "Line Chart"
|
||||||
|
rndwalk := (func() []float64 {
|
||||||
|
n := 150
|
||||||
|
d := make([]float64, n)
|
||||||
|
for i := 1; i < n; i++ {
|
||||||
|
if i < 20 {
|
||||||
|
d[i] = d[i-1] + 0.01
|
||||||
|
}
|
||||||
|
if i > 20 {
|
||||||
|
d[i] = d[i-1] - 0.05
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
})()
|
||||||
|
lc1.Data = rndwalk
|
||||||
|
lc1.Width = 26
|
||||||
|
lc1.Height = 11
|
||||||
|
lc1.X = 51
|
||||||
|
lc1.Y = 14
|
||||||
|
|
||||||
|
p1 := ui.NewPar("Hey!\nI am a borderless block!")
|
||||||
|
p1.HasBorder = false
|
||||||
|
p1.Width = 26
|
||||||
|
p1.Height = 2
|
||||||
|
p1.X = 52
|
||||||
|
p1.Y = 11
|
||||||
|
|
||||||
|
draw := func(t int) {
|
||||||
|
g.Percent = t % 101
|
||||||
|
list.Items = strs[t%9:]
|
||||||
|
sp.Lines[0].Data = spdata[t%10:]
|
||||||
|
sp.Lines[1].Data = spdata[t/2%10:]
|
||||||
|
lc.Data = sinps[t/2:]
|
||||||
|
lc1.Data = rndwalk[t:]
|
||||||
|
bc.Data = bcdata[t/2%10:]
|
||||||
|
ui.Render(p, list, g, sp, lc, bc, lc1, p1)
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := make(chan tm.Event)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
evt <- tm.PollEvent()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e := <-evt:
|
||||||
|
if e.Type == tm.EventKey && e.Ch == 'q' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
draw(i)
|
||||||
|
i++
|
||||||
|
if i == 102 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
example/themedefault.tiff
Normal file
BIN
example/themedefault.tiff
Normal file
Binary file not shown.
BIN
example/themehelloworld.tiff
Normal file
BIN
example/themehelloworld.tiff
Normal file
Binary file not shown.
12
gauge.go
12
gauge.go
@ -10,7 +10,10 @@ type Gauge struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewGauge() *Gauge {
|
func NewGauge() *Gauge {
|
||||||
g := &Gauge{Block: *NewBlock(), PercentColor: ColorWhite, BarColor: ColorGreen}
|
g := &Gauge{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
PercentColor: theme.GaugePercent,
|
||||||
|
BarColor: theme.GaugeBar}
|
||||||
g.Width = 12
|
g.Width = 12
|
||||||
g.Height = 5
|
g.Height = 5
|
||||||
return g
|
return g
|
||||||
@ -34,6 +37,9 @@ func (g *Gauge) Buffer() []Point {
|
|||||||
p.Y = g.innerY + i
|
p.Y = g.innerY + i
|
||||||
p.Ch = ' '
|
p.Ch = ' '
|
||||||
p.Bg = g.BarColor
|
p.Bg = g.BarColor
|
||||||
|
if p.Bg == ColorDefault {
|
||||||
|
p.Bg |= AttrReverse
|
||||||
|
}
|
||||||
ps = append(ps, p)
|
ps = append(ps, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,6 +53,10 @@ func (g *Gauge) Buffer() []Point {
|
|||||||
p.Fg = g.PercentColor
|
p.Fg = g.PercentColor
|
||||||
if w > g.innerWidth/2-1+i {
|
if w > g.innerWidth/2-1+i {
|
||||||
p.Bg = g.BarColor
|
p.Bg = g.BarColor
|
||||||
|
if p.Bg == ColorDefault {
|
||||||
|
p.Bg |= AttrReverse
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
p.Bg = g.Block.BgColor
|
p.Bg = g.Block.BgColor
|
||||||
}
|
}
|
||||||
|
2
list.go
2
list.go
@ -13,6 +13,8 @@ type List struct {
|
|||||||
func NewList() *List {
|
func NewList() *List {
|
||||||
l := &List{Block: *NewBlock()}
|
l := &List{Block: *NewBlock()}
|
||||||
l.Overflow = "hidden"
|
l.Overflow = "hidden"
|
||||||
|
l.ItemFgColor = theme.ListItemFg
|
||||||
|
l.ItemBgColor = theme.ListItemBg
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
p.go
12
p.go
@ -1,17 +1,21 @@
|
|||||||
package termui
|
package termui
|
||||||
|
|
||||||
type P struct {
|
type Par struct {
|
||||||
Block
|
Block
|
||||||
Text string
|
Text string
|
||||||
TextFgColor Attribute
|
TextFgColor Attribute
|
||||||
TextBgColor Attribute
|
TextBgColor Attribute
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewP(s string) *P {
|
func NewPar(s string) *Par {
|
||||||
return &P{Block: *NewBlock(), Text: s}
|
return &Par{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
Text: s,
|
||||||
|
TextFgColor: theme.ParTextFg,
|
||||||
|
TextBgColor: theme.ParTextBg}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *P) Buffer() []Point {
|
func (p *Par) Buffer() []Point {
|
||||||
ps := p.Block.Buffer()
|
ps := p.Block.Buffer()
|
||||||
|
|
||||||
rs := str2runes(p.Text)
|
rs := str2runes(p.Text)
|
||||||
|
@ -2,6 +2,7 @@ package termui
|
|||||||
|
|
||||||
import tm "github.com/nsf/termbox-go"
|
import tm "github.com/nsf/termbox-go"
|
||||||
|
|
||||||
|
// all renderable components should implement this
|
||||||
type Bufferer interface {
|
type Bufferer interface {
|
||||||
Buffer() []Point
|
Buffer() []Point
|
||||||
}
|
}
|
||||||
@ -14,7 +15,9 @@ func Close() {
|
|||||||
tm.Close()
|
tm.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
for _, r := range rs {
|
for _, r := range rs {
|
||||||
buf := r.Buffer()
|
buf := r.Buffer()
|
||||||
for _, v := range buf {
|
for _, v := range buf {
|
||||||
|
@ -26,6 +26,14 @@ func (s *Sparklines) Add(sl Sparkline) {
|
|||||||
s.Lines = append(s.Lines, sl)
|
s.Lines = append(s.Lines, sl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return unrenderable single sparkline, need to add it into Sparklines
|
||||||
|
func NewSparkline() Sparkline {
|
||||||
|
return Sparkline{
|
||||||
|
Height: 1,
|
||||||
|
TitleColor: theme.SparklineTitle,
|
||||||
|
LineColor: theme.SparklineLine}
|
||||||
|
}
|
||||||
|
|
||||||
func NewSparklines(ss ...Sparkline) *Sparklines {
|
func NewSparklines(ss ...Sparkline) *Sparklines {
|
||||||
s := &Sparklines{Block: *NewBlock(), Lines: ss}
|
s := &Sparklines{Block: *NewBlock(), Lines: ss}
|
||||||
return s
|
return s
|
||||||
|
61
theme.go
Normal file
61
theme.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package termui
|
||||||
|
|
||||||
|
type colorScheme struct {
|
||||||
|
BodyBg Attribute
|
||||||
|
BlockBg Attribute
|
||||||
|
HasBorder bool
|
||||||
|
BorderFg Attribute
|
||||||
|
BorderBg Attribute
|
||||||
|
BorderLabelTextFg Attribute
|
||||||
|
BorderLabelTextBg Attribute
|
||||||
|
ParTextFg Attribute
|
||||||
|
ParTextBg Attribute
|
||||||
|
SparklineLine Attribute
|
||||||
|
SparklineTitle Attribute
|
||||||
|
GaugeBar Attribute
|
||||||
|
GaugePercent Attribute
|
||||||
|
LineChartLine Attribute
|
||||||
|
LineChartAxes Attribute
|
||||||
|
ListItemFg Attribute
|
||||||
|
ListItemBg Attribute
|
||||||
|
BarChartBar Attribute
|
||||||
|
BarChartText Attribute
|
||||||
|
BarChartNum Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// default color scheme depends on the user's terminal setting.
|
||||||
|
var themeDefault = colorScheme{HasBorder: true}
|
||||||
|
|
||||||
|
var themeHelloWorld = colorScheme{
|
||||||
|
BodyBg: ColorBlack,
|
||||||
|
BlockBg: ColorBlack,
|
||||||
|
HasBorder: true,
|
||||||
|
BorderFg: ColorWhite,
|
||||||
|
BorderBg: ColorBlack,
|
||||||
|
BorderLabelTextBg: ColorBlack,
|
||||||
|
BorderLabelTextFg: ColorGreen,
|
||||||
|
ParTextBg: ColorBlack,
|
||||||
|
ParTextFg: ColorWhite,
|
||||||
|
SparklineLine: ColorMagenta,
|
||||||
|
SparklineTitle: ColorWhite,
|
||||||
|
GaugeBar: ColorRed,
|
||||||
|
GaugePercent: ColorWhite,
|
||||||
|
LineChartLine: ColorYellow | AttrBold,
|
||||||
|
LineChartAxes: ColorWhite,
|
||||||
|
ListItemBg: ColorBlack,
|
||||||
|
ListItemFg: ColorYellow,
|
||||||
|
BarChartBar: ColorRed,
|
||||||
|
BarChartNum: ColorWhite,
|
||||||
|
BarChartText: ColorCyan,
|
||||||
|
}
|
||||||
|
|
||||||
|
var theme = themeDefault // global dep
|
||||||
|
|
||||||
|
func UseTheme(th string) {
|
||||||
|
switch th {
|
||||||
|
case "helloworld":
|
||||||
|
theme = themeHelloWorld
|
||||||
|
default:
|
||||||
|
theme = themeDefault
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user