huge refactor
This commit is contained in:
parent
322b954149
commit
d335211770
@ -1,13 +1,12 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.tordarus.net/tordarus/buf2d"
|
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
)
|
)
|
||||||
|
|
||||||
func drawBuffer(scr tcell.Screen, buf *buf2d.Buffer) {
|
func drawBuffer(scr tcell.Screen, buf *ViewBuffer) {
|
||||||
buf.Draw(func(x, y int, cn rune) {
|
buf.ForEach(func(x, y int, rn Rune) {
|
||||||
scr.SetContent(x, y, cn, nil, tcell.StyleDefault)
|
scr.SetContent(x, y, rn.Rn, nil, rn.Style)
|
||||||
})
|
})
|
||||||
scr.Show()
|
scr.Show()
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/gdamore/tcell"
|
|
||||||
|
|
||||||
type Events interface {
|
type Events interface {
|
||||||
OnKeyPressed(key tcell.Key)
|
OnKeyPressed(event *KeyEvent) (consumed bool)
|
||||||
}
|
}
|
||||||
|
16
go.mod
Normal file
16
go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module git.tordarus.net/Tordarus/tui
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.tordarus.net/Tordarus/buf2d v1.1.0
|
||||||
|
github.com/gdamore/tcell v1.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gdamore/encoding v1.0.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.7 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect
|
||||||
|
golang.org/x/text v0.3.0 // indirect
|
||||||
|
)
|
14
go.sum
Normal file
14
go.sum
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
git.tordarus.net/Tordarus/buf2d v1.1.0 h1:rIZjD7yeX5XK2D1h75ET5Og0u/NQF3eVonnC5aaqVkQ=
|
||||||
|
git.tordarus.net/Tordarus/buf2d v1.1.0/go.mod h1:XXPpS8nQK0gUI0ki7ftV/qlprsGCRWFVSD4ybvDuUL8=
|
||||||
|
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||||
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
|
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||||
|
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||||
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||||
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
60
rune.go
Normal file
60
rune.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
type Rune struct {
|
||||||
|
Rn rune
|
||||||
|
Style Style
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultRune = Rune{' ', StyleDefault}
|
||||||
|
|
||||||
|
// Text represents a string with rune-based styling
|
||||||
|
type Text struct {
|
||||||
|
str string
|
||||||
|
style []Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Txt returns a Text containing the given str
|
||||||
|
// using the default style
|
||||||
|
func Txt(str string) *Text {
|
||||||
|
styles := make([]Style, 0, len(str))
|
||||||
|
for range str {
|
||||||
|
styles = append(styles, StyleDefault)
|
||||||
|
}
|
||||||
|
return &Text{str: str, style: styles}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the amount of runes in t
|
||||||
|
func (t *Text) Len() int {
|
||||||
|
return len(t.style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendString appends str with default styling to t
|
||||||
|
func (t *Text) AppendString(str string) {
|
||||||
|
t.AppendText(Txt(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrependString prepends str with default styling to t
|
||||||
|
func (t *Text) PrependString(str string) {
|
||||||
|
newTxt := Txt(str)
|
||||||
|
newTxt.AppendText(t)
|
||||||
|
*t = *newTxt
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendText appends txt to t
|
||||||
|
func (t *Text) AppendText(txt *Text) {
|
||||||
|
t.str += txt.str
|
||||||
|
t.style = append(t.style, txt.style...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style applies the given style to the part of t between startIndex (inclusive) and endIndex (exclusive)
|
||||||
|
func (t *Text) Style(style Style, startIndex, endIndex int) {
|
||||||
|
for i := startIndex; i < endIndex; i++ {
|
||||||
|
t.style[i] = style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Text) StyleRegex(style Style, pattern *regexp.Regexp) {
|
||||||
|
// TODO
|
||||||
|
}
|
@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.tordarus.net/tordarus/buf2d"
|
"git.tordarus.net/Tordarus/buf2d"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ func (s *Screen) eventloop() {
|
|||||||
go s.Redraw()
|
go s.Redraw()
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
go func() {
|
go func() {
|
||||||
s.Root.OnKeyPressed(event.Key())
|
s.Root.OnKeyPressed(event)
|
||||||
s.Redraw()
|
s.Redraw()
|
||||||
}()
|
}()
|
||||||
default:
|
default:
|
||||||
@ -53,6 +53,8 @@ func (s *Screen) Start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer s.scr.Fini()
|
||||||
|
|
||||||
s.Redraw()
|
s.Redraw()
|
||||||
return <-s.stopCh
|
return <-s.stopCh
|
||||||
}
|
}
|
||||||
@ -67,7 +69,7 @@ func (s *Screen) StopWithError(err error) {
|
|||||||
|
|
||||||
func (s *Screen) Redraw() {
|
func (s *Screen) Redraw() {
|
||||||
w, h := s.scr.Size()
|
w, h := s.scr.Size()
|
||||||
buf := buf2d.NewBuffer(w, h)
|
buf := buf2d.NewBuffer(w, h, DefaultRune)
|
||||||
s.Root.Draw(buf)
|
s.Root.Draw(buf)
|
||||||
drawBuffer(s.scr, buf)
|
drawBuffer(s.scr, buf)
|
||||||
}
|
}
|
||||||
|
@ -4,22 +4,28 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"tui"
|
|
||||||
"tui/views"
|
|
||||||
|
|
||||||
|
"git.tordarus.net/Tordarus/tui"
|
||||||
|
"git.tordarus.net/Tordarus/tui/views"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestScreen(t *testing.T) {
|
func TestScreen(t *testing.T) {
|
||||||
eventView := views.NewEventView()
|
textView := views.NewTextView("hello world")
|
||||||
|
eventView := views.NewEventView(textView)
|
||||||
screen, err := tui.NewScreen(eventView)
|
screen, err := tui.NewScreen(eventView)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
eventView.KeyPressed = func(key tcell.Key) {
|
eventView.KeyPressed = func(event *tui.KeyEvent) (consumed bool) {
|
||||||
screen.StopWithError(errors.New(fmt.Sprintf("%#v", key)))
|
if event.Key() == tcell.KeyCtrlC {
|
||||||
|
screen.StopWithError(errors.New(fmt.Sprintf("key: %#v | rune: %s", event.Key(), string(event.Rune()))))
|
||||||
|
}
|
||||||
|
|
||||||
|
//textView.Text = event.When().String()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
err = screen.Start()
|
err = screen.Start()
|
||||||
|
13
types.go
Normal file
13
types.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.tordarus.net/Tordarus/buf2d"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ViewBuffer = buf2d.Buffer[Rune]
|
||||||
|
type KeyEvent = tcell.EventKey
|
||||||
|
type Style = tcell.Style
|
||||||
|
type Color = tcell.Color
|
||||||
|
|
||||||
|
var StyleDefault Style = tcell.StyleDefault
|
31
utils.go
Normal file
31
utils.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WriteString writes a whole string to the buffer at position (x,y)
|
||||||
|
// no word wrap is applied at all. If the string does not fit, it will be truncated
|
||||||
|
func WriteString(b *ViewBuffer, str string, style Style, x, y int) {
|
||||||
|
dx := x
|
||||||
|
for _, r := range str {
|
||||||
|
if dx >= b.Width() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.Set(dx, y, Rune{r, style})
|
||||||
|
dx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMultiLineString writes a multi-line string to the buffer at position (x,y)
|
||||||
|
// no word wrap is applied at all. If a line does not fit horizontally, it will be truncated
|
||||||
|
// All lines which do not fit vertically will be truncated as well
|
||||||
|
func WriteMultiLineString(b *ViewBuffer, str string, style Style, x, y int) {
|
||||||
|
lines := strings.Split(str, "\n")
|
||||||
|
for dy, line := range lines {
|
||||||
|
if dy >= b.Height() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteString(b, line, style, x, y+dy)
|
||||||
|
}
|
||||||
|
}
|
42
view.go
42
view.go
@ -1,18 +1,40 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
// View defines the behavior of any element displayable on screen
|
||||||
"git.tordarus.net/tordarus/buf2d"
|
// To define custom Views, it is recommended to add ViewTmpl
|
||||||
"github.com/gdamore/tcell"
|
// as the promoted anonymous field for your custom View struct.
|
||||||
)
|
// It implements the View interface with useful default behavior
|
||||||
|
|
||||||
type View interface {
|
type View interface {
|
||||||
Events
|
Events
|
||||||
|
|
||||||
SetForeground(color tcell.Color)
|
SetForeground(color Color)
|
||||||
Foreground() tcell.Color
|
Foreground() Color
|
||||||
|
|
||||||
SetBackground(color tcell.Color)
|
SetBackground(color Color)
|
||||||
Background() tcell.Color
|
Background() Color
|
||||||
|
|
||||||
Draw(*buf2d.Buffer)
|
Style() Style
|
||||||
|
|
||||||
|
Draw(*ViewBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group defines the behavior of a View which can hold multiple sub views
|
||||||
|
// To define custom Groups, it is recommended to add GroupTmpl
|
||||||
|
// as the promoted anonymous field for your custom Wrapper struct.
|
||||||
|
// It implements the Group interface with useful default behavior
|
||||||
|
type Group interface {
|
||||||
|
View
|
||||||
|
|
||||||
|
Children() []View
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper defines the behavior of a GroupView which can hold exactly one sub view
|
||||||
|
// To define custom Wrappers, it is recommended to add WrapperTmpl
|
||||||
|
// as the promoted anonymous field for your custom Wrapper struct.
|
||||||
|
// It implements the Wrapper interface with useful default behavior
|
||||||
|
type Wrapper interface {
|
||||||
|
Group
|
||||||
|
|
||||||
|
SetView(View)
|
||||||
|
View() View
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
type ViewGroup interface {
|
// TODO GroupTmpl
|
||||||
View
|
|
||||||
Children() []*View
|
|
||||||
}
|
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"tui"
|
"git.tordarus.net/Tordarus/tui"
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventView struct {
|
type EventView struct {
|
||||||
tui.ViewTmpl
|
tui.WrapperTmpl
|
||||||
KeyPressed func(key tcell.Key)
|
|
||||||
|
View tui.View
|
||||||
|
KeyPressed func(event *tui.KeyEvent) (consumed bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *EventView) OnKeyPressed(key tcell.Key) {
|
func NewEventView(view tui.View) *EventView {
|
||||||
|
return &EventView{View: view}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *EventView) OnKeyPressed(event *tui.KeyEvent) (consumed bool) {
|
||||||
if v.KeyPressed != nil {
|
if v.KeyPressed != nil {
|
||||||
v.KeyPressed(key)
|
return v.KeyPressed(event)
|
||||||
}
|
}
|
||||||
|
return v.ViewTmpl.OnKeyPressed(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEventView() *EventView {
|
func (v *EventView) Draw(buf *tui.ViewBuffer) {
|
||||||
return &EventView{}
|
v.View.Draw(buf)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"tui"
|
"git.tordarus.net/Tordarus/tui"
|
||||||
|
|
||||||
"git.tordarus.net/tordarus/buf2d"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TextView struct {
|
type TextView struct {
|
||||||
@ -13,8 +11,8 @@ type TextView struct {
|
|||||||
|
|
||||||
var _ tui.View = &TextView{}
|
var _ tui.View = &TextView{}
|
||||||
|
|
||||||
func (v *TextView) Draw(buf *buf2d.Buffer) {
|
func (v *TextView) Draw(buf *tui.ViewBuffer) {
|
||||||
buf.WriteMultiLineString(v.Text, 0, 0)
|
tui.WriteMultiLineString(buf, v.Text, v.Style(), 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTextView(text string) *TextView {
|
func NewTextView(text string) *TextView {
|
||||||
|
57
viewtmpl.go
57
viewtmpl.go
@ -1,45 +1,44 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import "github.com/gdamore/tcell"
|
||||||
"git.tordarus.net/tordarus/buf2d"
|
|
||||||
"github.com/gdamore/tcell"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ViewTmpl struct {
|
type ViewTmpl struct {
|
||||||
view View
|
foreground *Color
|
||||||
|
background *Color
|
||||||
foreground tcell.Color
|
|
||||||
background tcell.Color
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ View = &ViewTmpl{}
|
var _ View = &ViewTmpl{}
|
||||||
|
|
||||||
func NewViewTmpl(v View) *ViewTmpl {
|
func (v *ViewTmpl) Draw(buf *ViewBuffer) {
|
||||||
return &ViewTmpl{
|
buf.Fill(DefaultRune)
|
||||||
view: v,
|
}
|
||||||
|
|
||||||
|
func (v *ViewTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ViewTmpl) Style() Style {
|
||||||
|
return StyleDefault.Background(v.Background()).Foreground(v.Foreground())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ViewTmpl) Foreground() Color {
|
||||||
|
if v.foreground == nil {
|
||||||
|
return tcell.ColorDefault
|
||||||
}
|
}
|
||||||
|
return *v.foreground
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *ViewTmpl) Draw(buf *buf2d.Buffer) {
|
func (v *ViewTmpl) SetForeground(color Color) {
|
||||||
buf.Fill(' ')
|
v.foreground = &color
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *ViewTmpl) OnKeyPressed(key tcell.Key) {
|
func (v *ViewTmpl) Background() Color {
|
||||||
|
if v.background == nil {
|
||||||
|
return tcell.ColorDefault
|
||||||
|
}
|
||||||
|
return *v.background
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *ViewTmpl) Foreground() tcell.Color {
|
func (v *ViewTmpl) SetBackground(color Color) {
|
||||||
return v.foreground
|
v.background = &color
|
||||||
}
|
|
||||||
|
|
||||||
func (v *ViewTmpl) SetForeground(color tcell.Color) {
|
|
||||||
v.foreground = color
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *ViewTmpl) Background() tcell.Color {
|
|
||||||
return v.background
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *ViewTmpl) SetBackground(color tcell.Color) {
|
|
||||||
v.background = color
|
|
||||||
}
|
}
|
||||||
|
74
wrappertmpl.go
Normal file
74
wrappertmpl.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import "github.com/gdamore/tcell"
|
||||||
|
|
||||||
|
type WrapperTmpl struct {
|
||||||
|
ViewTmpl
|
||||||
|
view View
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Wrapper = &WrapperTmpl{}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) Draw(buf *ViewBuffer) {
|
||||||
|
if v.view != nil {
|
||||||
|
v.view.Draw(buf)
|
||||||
|
} else {
|
||||||
|
v.ViewTmpl.Draw(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
|
||||||
|
if v.view != nil {
|
||||||
|
return v.view.OnKeyPressed(event)
|
||||||
|
}
|
||||||
|
return v.ViewTmpl.OnKeyPressed(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) Style() Style {
|
||||||
|
if v.view != nil {
|
||||||
|
return v.view.Style()
|
||||||
|
}
|
||||||
|
return v.ViewTmpl.Style()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) Foreground() Color {
|
||||||
|
if v.view != nil {
|
||||||
|
return v.view.Foreground()
|
||||||
|
}
|
||||||
|
return v.ViewTmpl.Foreground()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) SetForeground(color Color) {
|
||||||
|
if v.view != nil {
|
||||||
|
v.view.SetForeground(color)
|
||||||
|
} else {
|
||||||
|
v.ViewTmpl.SetForeground(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) Background() Color {
|
||||||
|
if v.background == nil {
|
||||||
|
return tcell.ColorDefault
|
||||||
|
}
|
||||||
|
return *v.background
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) SetBackground(color Color) {
|
||||||
|
if v.view != nil {
|
||||||
|
v.view.SetBackground(color)
|
||||||
|
} else {
|
||||||
|
v.ViewTmpl.SetBackground(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) Children() []View {
|
||||||
|
return []View{v.view}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) View() View {
|
||||||
|
return v.view
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) SetView(view View) {
|
||||||
|
v.view = view
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user