mirror of
https://github.com/bloeys/nterm.git
synced 2025-12-29 06:28:20 +00:00
Correct and simplify textBuf drawing using the new ViewsFromTo+ new ring funcs
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@ -13,6 +13,13 @@
|
|||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
|
|
||||||
|
# Custom
|
||||||
*.png
|
*.png
|
||||||
*.cpu
|
*.cpu
|
||||||
|
|
||||||
1gb.txt
|
1gb.txt
|
||||||
|
1gb.bin
|
||||||
|
|
||||||
|
test.txt
|
||||||
|
long-line.txt
|
||||||
|
|||||||
@ -295,7 +295,7 @@ func ParseSGRArgs(info *AnsiCodeInfo, args []byte) {
|
|||||||
|
|
||||||
// @TODO Support bold/underline etc
|
// @TODO Support bold/underline etc
|
||||||
// @TODO Support 256 and RGB colors
|
// @TODO Support 256 and RGB colors
|
||||||
panic("Code not supported yet: " + fmt.Sprint(intCode))
|
println("Code not supported yet: " + fmt.Sprint(intCode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
main.go
57
main.go
@ -73,8 +73,10 @@ type program struct {
|
|||||||
lastCmdCharPos *gglm.Vec3
|
lastCmdCharPos *gglm.Vec3
|
||||||
scrollPos int64
|
scrollPos int64
|
||||||
scrollSpd int64
|
scrollSpd int64
|
||||||
maxCharsToShow int64
|
|
||||||
maxLinesToShow int64
|
CellCountX int64
|
||||||
|
CellCountY int64
|
||||||
|
CellCount int64
|
||||||
|
|
||||||
activeCmd *Cmd
|
activeCmd *Cmd
|
||||||
Settings *Settings
|
Settings *Settings
|
||||||
@ -90,13 +92,12 @@ const (
|
|||||||
defaultCmdBufSize = 4 * 1024
|
defaultCmdBufSize = 4 * 1024
|
||||||
defaultTextBufSize = 4 * 1024 * 1024
|
defaultTextBufSize = 4 * 1024 * 1024
|
||||||
|
|
||||||
defaultScrollSpd = 5
|
defaultScrollSpd = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// isDrawingBounds = false
|
|
||||||
drawManyLines = false
|
|
||||||
drawGrid bool
|
drawGrid bool
|
||||||
|
drawManyLines = false
|
||||||
|
|
||||||
textToShow = ""
|
textToShow = ""
|
||||||
|
|
||||||
@ -249,7 +250,7 @@ func (p *program) WriteToTextBuf(text []byte) {
|
|||||||
// This is locked because running cmds are potentially writing to it same time we are
|
// This is locked because running cmds are potentially writing to it same time we are
|
||||||
p.textBufMutex.Lock()
|
p.textBufMutex.Lock()
|
||||||
p.textBuf.Write(text...)
|
p.textBuf.Write(text...)
|
||||||
p.scrollPos = clamp(p.textBuf.Len-p.maxCharsToShow, 0, p.textBuf.Len-1)
|
p.scrollPos = clamp(p.textBuf.Len-p.CellCount, 0, p.textBuf.Len-1)
|
||||||
p.textBufMutex.Unlock()
|
p.textBufMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,13 +297,16 @@ func (p *program) MainUpdate() {
|
|||||||
if mouseWheelYNorm := -int64(input.GetMouseWheelYNorm()); mouseWheelYNorm != 0 {
|
if mouseWheelYNorm := -int64(input.GetMouseWheelYNorm()); mouseWheelYNorm != 0 {
|
||||||
|
|
||||||
var newPosNewLines int64
|
var newPosNewLines int64
|
||||||
w, _ := p.GridSize()
|
|
||||||
if mouseWheelYNorm < 0 {
|
if mouseWheelYNorm < 0 {
|
||||||
newPosNewLines, _ = find_n_lines_index_iterator(p.textBuf.Iterator(), p.scrollPos, p.scrollSpd*mouseWheelYNorm-1, int64(w))
|
newPosNewLines, _ = find_n_lines_index_iterator(p.textBuf.Iterator(), p.scrollPos, p.scrollSpd*mouseWheelYNorm-1, p.CellCountX)
|
||||||
} else {
|
} else {
|
||||||
newPosNewLines, _ = find_n_lines_index_iterator(p.textBuf.Iterator(), p.scrollPos, p.scrollSpd*mouseWheelYNorm, int64(w))
|
newPosNewLines, _ = find_n_lines_index_iterator(p.textBuf.Iterator(), p.scrollPos, p.scrollSpd*mouseWheelYNorm, p.CellCountX)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a := p.textBuf.AbsIndex(uint64(p.scrollPos))
|
||||||
|
b := p.textBuf.AbsIndex(uint64(newPosNewLines))
|
||||||
|
println("was at:", a, "; Now at:", b)
|
||||||
|
// assert.T(p.textBuf.Get(uint64(newPosNewLines)) != '\n', fmt.Sprintf("Original AbsIndex %d; New line at AbsIndex %d\n", a, b))
|
||||||
p.scrollPos = clamp(newPosNewLines, 0, p.textBuf.Len)
|
p.scrollPos = clamp(newPosNewLines, 0, p.textBuf.Len)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,18 +321,19 @@ func (p *program) MainUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw textBuf
|
// Draw textBuf
|
||||||
v1, v2 := p.textBuf.Views()
|
from := p.scrollPos
|
||||||
|
to, _ := find_n_lines_index_iterator(p.textBuf.Iterator(), p.scrollPos, p.CellCountY-2, p.CellCountX)
|
||||||
|
|
||||||
from := clamp(p.scrollPos, 0, int64(len(v1)-1))
|
// to is the first character after the nth line. Passing this index to ViewsFromTo will show 1 more char than we want,
|
||||||
to := clamp(p.scrollPos+p.maxCharsToShow, 0, int64(len(v1)-1))
|
// so we decrement if needed
|
||||||
p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(v1[from:to], *gglm.NewVec3(0, float32(p.GlyphRend.ScreenHeight)-p.GlyphRend.Atlas.LineHeight, 0)).Data
|
if to > 0 {
|
||||||
|
to--
|
||||||
if p.scrollPos >= int64(len(v1)) {
|
|
||||||
|
|
||||||
from := clamp(p.scrollPos-int64(len(v1)), 0, int64(len(v2)-1))
|
|
||||||
to := clamp(p.scrollPos+p.maxCharsToShow, 0, int64(len(v2)-1))
|
|
||||||
p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(v2[from:to], *p.lastCmdCharPos).Data
|
|
||||||
}
|
}
|
||||||
|
assert.T(to >= 0, "'to' was less than zero")
|
||||||
|
v1, v2 := p.textBuf.ViewsFromTo(uint64(from), uint64(to))
|
||||||
|
|
||||||
|
p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(v1, *gglm.NewVec3(0, float32(p.GlyphRend.ScreenHeight)-p.GlyphRend.Atlas.LineHeight, 0)).Data
|
||||||
|
p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(v2, *p.lastCmdCharPos).Data
|
||||||
|
|
||||||
sepLinePos.Data = p.lastCmdCharPos.Data
|
sepLinePos.Data = p.lastCmdCharPos.Data
|
||||||
|
|
||||||
@ -673,8 +678,9 @@ func (p *program) DrawCursor() {
|
|||||||
p.rend.Draw(p.gridMesh, gglm.NewTrMatId().Translate(pos).Scale(gglm.NewVec3(0.1*p.GlyphRend.Atlas.SpaceAdvance, p.GlyphRend.Atlas.LineHeight, 1)), p.gridMat)
|
p.rend.Draw(p.gridMesh, gglm.NewTrMatId().Translate(pos).Scale(gglm.NewVec3(0.1*p.GlyphRend.Atlas.SpaceAdvance, p.GlyphRend.Atlas.LineHeight, 1)), p.gridMat)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *program) GridSize() (w, h int32) {
|
// GridSize returns how many cells horizontally (aka chars per line) and how many cells vertically (aka lines)
|
||||||
return p.GlyphRend.ScreenWidth / int32(p.GlyphRend.Atlas.SpaceAdvance), p.GlyphRend.ScreenHeight / int32(p.GlyphRend.Atlas.LineHeight)
|
func (p *program) GridSize() (w, h int64) {
|
||||||
|
return int64(p.GlyphRend.ScreenWidth) / int64(p.GlyphRend.Atlas.SpaceAdvance), int64(p.GlyphRend.ScreenHeight) / int64(p.GlyphRend.Atlas.LineHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *program) ScreenPosToGridPos(screenPos *gglm.Vec3) {
|
func (p *program) ScreenPosToGridPos(screenPos *gglm.Vec3) {
|
||||||
@ -790,9 +796,8 @@ func (p *program) HandleWindowResize() {
|
|||||||
viewMtx := gglm.LookAt(gglm.NewVec3(0, 0, -10), gglm.NewVec3(0, 0, 0), gglm.NewVec3(0, 1, 0))
|
viewMtx := gglm.LookAt(gglm.NewVec3(0, 0, -10), gglm.NewVec3(0, 0, 0), gglm.NewVec3(0, 1, 0))
|
||||||
p.gridMat.SetUnifMat4("projViewMat", &projMtx.Mul(viewMtx).Mat4)
|
p.gridMat.SetUnifMat4("projViewMat", &projMtx.Mul(viewMtx).Mat4)
|
||||||
|
|
||||||
// We show a bit more than calculated to be safe against showing empty space
|
p.CellCountX, p.CellCountY = p.GridSize()
|
||||||
p.maxLinesToShow = int64(CeilF32(float32(h)/float32(p.GlyphRend.Atlas.LineHeight)) * 1.25)
|
p.CellCount = p.CellCountX * p.CellCountY
|
||||||
p.maxCharsToShow = int64(CeilF32(float32(w)/float32(p.GlyphRend.Atlas.SpaceAdvance))*1.25) * p.maxLinesToShow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FloorF32(x float32) float32 {
|
func FloorF32(x float32) float32 {
|
||||||
@ -874,7 +879,7 @@ func find_n_lines_index_iterator(it ring.Iterator[byte], startIndex, n, charsPer
|
|||||||
buf := make([]byte, 4)
|
buf := make([]byte, 4)
|
||||||
it.GotoIndex(startIndex)
|
it.GotoIndex(startIndex)
|
||||||
|
|
||||||
// @Note we should ignore zero width glyphs
|
// @Todo we should ignore zero width glyphs
|
||||||
// @Note is this better in glyphs package?
|
// @Note is this better in glyphs package?
|
||||||
bytesSeen := int64(0)
|
bytesSeen := int64(0)
|
||||||
charsSeenThisLine := int64(0)
|
charsSeenThisLine := int64(0)
|
||||||
@ -914,6 +919,8 @@ func find_n_lines_index_iterator(it ring.Iterator[byte], startIndex, n, charsPer
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
// @Todo this has wrong behavior when dealing with wrapped lines because we don't know what X position to be in
|
||||||
|
// after going up a line. Are we in the middle of the line?
|
||||||
for !done || bytesToKeep > 0 {
|
for !done || bytesToKeep > 0 {
|
||||||
|
|
||||||
read, done = it.PrevN(buf[bytesToKeep:], 4)
|
read, done = it.PrevN(buf[bytesToKeep:], 4)
|
||||||
|
|||||||
@ -47,7 +47,7 @@ uniform int drawBounds;
|
|||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec4 texColor = texelFetch(diffTex, ivec2(v2fUV0), 0);
|
vec4 texColor = texelFetch(diffTex, ivec2(v2fUV0), 0);
|
||||||
// This commented out part highlights the full region of the char
|
// This part highlights the full region of the char
|
||||||
if (texColor.r == 0 && drawBounds != 0)
|
if (texColor.r == 0 && drawBounds != 0)
|
||||||
{
|
{
|
||||||
fragColor = vec4(0,1,0,0.25);
|
fragColor = vec4(0,1,0,0.25);
|
||||||
|
|||||||
77
ring/ring.go
77
ring/ring.go
@ -91,9 +91,24 @@ func clamp[T constraints.Ordered](x, min, max T) T {
|
|||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get returns the element at the index relative from Buffer.Start
|
||||||
|
// If there are no elements then the default value of T is returned
|
||||||
|
func (b *Buffer[T]) Get(index uint64) (val T) {
|
||||||
|
|
||||||
|
if index >= uint64(b.Len) {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Data[(b.Start+int64(index))%b.Cap]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer[T]) AbsIndex(relIndex uint64) uint64 {
|
||||||
|
return uint64((b.Start + int64(relIndex)) % b.Cap)
|
||||||
|
}
|
||||||
|
|
||||||
// Views returns two slices that have 'Len' elements in total between them.
|
// Views returns two slices that have 'Len' elements in total between them.
|
||||||
// The first slice is from Start till min(Start+Len, Cap). If Start+Len<=Cap then the first slice contains all the data and the second is empty.
|
// The first slice is from Start till min(Start+Len, Cap). If Start+Len<=Cap then the first slice contains all the data and the second is empty.
|
||||||
// If Start+Len>Cap then the first slice contains the data from Start till Cap, and the second slice contains data from Zero till Start+Len-Cap (basically the remaining elements to reach Len in total)
|
// If Start+Len>Cap then the first slice contains the data from Start till Cap, and the second slice contains data from 0 till Start+Len-Cap (basically the remaining elements to reach Len in total)
|
||||||
//
|
//
|
||||||
// This function does NOT copy. Any changes on the returned slices will reflect on the buffer Data
|
// This function does NOT copy. Any changes on the returned slices will reflect on the buffer Data
|
||||||
//
|
//
|
||||||
@ -109,15 +124,49 @@ func (b *Buffer[T]) Views() (v1, v2 []T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffer[T]) Iterator() Iterator[T] {
|
func (b *Buffer[T]) ViewsFromTo(fromIndex, toIndex uint64) (v1, v2 []T) {
|
||||||
|
|
||||||
v1, v2 := b.Views()
|
toIndex++ // We convert the index into a length (e.g. from=0, to=0 is from=0, len=1)
|
||||||
return Iterator[T]{
|
if toIndex <= fromIndex || fromIndex >= uint64(b.Len) {
|
||||||
V1: v1,
|
return []T{}, []T{}
|
||||||
V2: v2,
|
|
||||||
Curr: 0,
|
|
||||||
InV1: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v1, v2 = b.Views()
|
||||||
|
v1Len := uint64(len(v1))
|
||||||
|
v2Len := uint64(len(v2))
|
||||||
|
startInV1 := fromIndex < v1Len
|
||||||
|
|
||||||
|
if startInV1 {
|
||||||
|
|
||||||
|
if toIndex <= v1Len {
|
||||||
|
v1 = v1[fromIndex:toIndex]
|
||||||
|
v2 = v2[:0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toIndex -= v1Len
|
||||||
|
if toIndex > v2Len {
|
||||||
|
toIndex = v2Len
|
||||||
|
}
|
||||||
|
|
||||||
|
v1 = v1[fromIndex:v1Len]
|
||||||
|
v2 = v2[:toIndex]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fromIndex -= v1Len - 1
|
||||||
|
toIndex -= v1Len
|
||||||
|
if toIndex >= v2Len {
|
||||||
|
toIndex = v2Len
|
||||||
|
}
|
||||||
|
|
||||||
|
v1 = v1[:0]
|
||||||
|
v2 = v2[fromIndex:toIndex]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer[T]) Iterator() Iterator[T] {
|
||||||
|
return NewIterator(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBuffer[T any](capacity uint64) *Buffer[T] {
|
func NewBuffer[T any](capacity uint64) *Buffer[T] {
|
||||||
@ -263,7 +312,7 @@ func (it *Iterator[T]) PrevN(buf []T, n int) (read int, done bool) {
|
|||||||
// and the next Prev() call returns done=true
|
// and the next Prev() call returns done=true
|
||||||
func (it *Iterator[T]) GotoStart() {
|
func (it *Iterator[T]) GotoStart() {
|
||||||
it.Curr = 0
|
it.Curr = 0
|
||||||
it.InV1 = true
|
it.InV1 = len(it.V1) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GotoIndex goes to the index n relative to Buffer.Start
|
// GotoIndex goes to the index n relative to Buffer.Start
|
||||||
@ -297,3 +346,13 @@ func (it *Iterator[T]) GotoEnd() {
|
|||||||
it.Curr = int64(len(it.V2))
|
it.Curr = int64(len(it.V2))
|
||||||
it.InV1 = false
|
it.InV1 = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewIterator[T any](b *Buffer[T]) Iterator[T] {
|
||||||
|
v1, v2 := b.Views()
|
||||||
|
return Iterator[T]{
|
||||||
|
V1: v1,
|
||||||
|
V2: v2,
|
||||||
|
Curr: 0,
|
||||||
|
InV1: len(v1) > 0, // If buffer is empty we shouldn't be in V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -87,6 +87,78 @@ func TestRing(t *testing.T) {
|
|||||||
b2.DeleteN(2, 1)
|
b2.DeleteN(2, 1)
|
||||||
Check(t, 3, b2.Len)
|
Check(t, 3, b2.Len)
|
||||||
CheckArr(t, []int{5, 6, 8, 8}, b2.Data)
|
CheckArr(t, []int{5, 6, 8, 8}, b2.Data)
|
||||||
|
|
||||||
|
// ViewsFromTo
|
||||||
|
b2 = ring.NewBuffer[int](4)
|
||||||
|
|
||||||
|
v11, v22 := b2.ViewsFromTo(0, 0)
|
||||||
|
Check(t, 0, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
|
||||||
|
b2.Write(1, 2, 3, 4)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(5, 0)
|
||||||
|
Check(t, 0, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(0, 0)
|
||||||
|
Check(t, 1, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
CheckArr(t, []int{1}, v11)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(0, 1)
|
||||||
|
Check(t, 2, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
CheckArr(t, []int{1, 2}, v11)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(0, 3)
|
||||||
|
Check(t, 4, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
CheckArr(t, []int{1, 2, 3, 4}, v11)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(0, 4)
|
||||||
|
Check(t, 4, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
CheckArr(t, []int{1, 2, 3, 4}, v11)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(0, 40)
|
||||||
|
Check(t, 4, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
CheckArr(t, []int{1, 2, 3, 4}, v11)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(3, 40)
|
||||||
|
Check(t, 1, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
CheckArr(t, []int{4}, v11)
|
||||||
|
|
||||||
|
b2.Write(5, 6)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(3, 40)
|
||||||
|
Check(t, 0, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(1, 2)
|
||||||
|
Check(t, 1, len(v11))
|
||||||
|
Check(t, 1, len(v22))
|
||||||
|
CheckArr(t, []int{4}, v11)
|
||||||
|
CheckArr(t, []int{5}, v22)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(0, 1)
|
||||||
|
Check(t, 2, len(v11))
|
||||||
|
Check(t, 0, len(v22))
|
||||||
|
CheckArr(t, []int{3, 4}, v11)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(0, 2)
|
||||||
|
Check(t, 2, len(v11))
|
||||||
|
Check(t, 1, len(v22))
|
||||||
|
CheckArr(t, []int{3, 4}, v11)
|
||||||
|
CheckArr(t, []int{5}, v22)
|
||||||
|
|
||||||
|
v11, v22 = b2.ViewsFromTo(0, 3)
|
||||||
|
Check(t, 2, len(v11))
|
||||||
|
Check(t, 2, len(v22))
|
||||||
|
CheckArr(t, []int{3, 4}, v11)
|
||||||
|
CheckArr(t, []int{5, 6}, v22)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIterator(t *testing.T) {
|
func TestIterator(t *testing.T) {
|
||||||
@ -198,6 +270,23 @@ func TestIterator(t *testing.T) {
|
|||||||
ans = []int{4, 3}
|
ans = []int{4, 3}
|
||||||
it.PrevN(got, 2)
|
it.PrevN(got, 2)
|
||||||
CheckArr(t, ans, got)
|
CheckArr(t, ans, got)
|
||||||
|
|
||||||
|
// Empty buffer
|
||||||
|
b = ring.NewBuffer[int](4)
|
||||||
|
|
||||||
|
it = b.Iterator()
|
||||||
|
Check(t, 0, len(it.V1))
|
||||||
|
Check(t, 0, len(it.V2))
|
||||||
|
Check(t, false, it.InV1)
|
||||||
|
|
||||||
|
it.GotoStart()
|
||||||
|
Check(t, false, it.InV1)
|
||||||
|
|
||||||
|
it.GotoIndex(1)
|
||||||
|
Check(t, false, it.InV1)
|
||||||
|
|
||||||
|
_, done := it.Next()
|
||||||
|
Check(t, true, done)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Check[T comparable](t *testing.T, expected, got T) {
|
func Check[T comparable](t *testing.T, expected, got T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user