Add MenuScene with New Level UI
* Debug mode: no longer enables the DebugOverlay (F3) by default, but does now insert the current FPS counter into the window title bar. * ui.Frame: set a default "mostly transparent" BG color so the frame background doesn't render as white. * Add the MenuScene which will house the game's main menus. * The "New Level" menu is first to be added. * UI lets you pick Page Type and Wallpaper using radio buttons. * Page Type: Unbounded, Bounded (default), No Negative Space, Bordered * Fix bugs in uix.Canvas to fully support all these page types.
This commit is contained in:
parent
1150d6d3e9
commit
4dd1bebc5f
|
@ -22,6 +22,7 @@ type Engine interface {
|
|||
|
||||
// Clear the full canvas and set this color.
|
||||
Clear(Color)
|
||||
SetTitle(string)
|
||||
DrawPoint(Color, Point)
|
||||
DrawLine(Color, Point, Point)
|
||||
DrawRect(Color, Rect)
|
||||
|
|
|
@ -84,6 +84,12 @@ func (r *Renderer) Setup() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SetTitle sets the SDL window title.
|
||||
func (r *Renderer) SetTitle(title string) {
|
||||
r.title = title
|
||||
r.window.SetTitle(title)
|
||||
}
|
||||
|
||||
// GetTicks gets SDL's current tick count.
|
||||
func (r *Renderer) GetTicks() uint32 {
|
||||
return sdl.GetTicks()
|
||||
|
|
|
@ -21,6 +21,7 @@ func NewFrame(name string) *Frame {
|
|||
packs: map[Anchor][]packedWidget{},
|
||||
widgets: []Widget{},
|
||||
}
|
||||
w.SetBackground(render.RGBA(1, 0, 0, 0)) // invisible default BG
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("Frame<%s>",
|
||||
name,
|
||||
|
|
|
@ -37,6 +37,21 @@ var (
|
|||
Color: render.Black,
|
||||
}
|
||||
|
||||
// UIFont is the main font for UI labels.
|
||||
UIFont = render.Text{
|
||||
Size: 12,
|
||||
Padding: 4,
|
||||
Color: render.Black,
|
||||
}
|
||||
|
||||
// LabelFont is the font for strong labels in UI.
|
||||
LabelFont = render.Text{
|
||||
Size: 12,
|
||||
FontFilename: "./fonts/DejaVuSans-Bold.ttf",
|
||||
Padding: 4,
|
||||
Color: render.Black,
|
||||
}
|
||||
|
||||
// Color for draggable doodad.
|
||||
DragColor = render.MustHexColor("#0099FF")
|
||||
)
|
||||
|
|
|
@ -93,8 +93,7 @@ func (c Command) Run(d *Doodle) error {
|
|||
|
||||
// New opens a new map in the editor mode.
|
||||
func (c Command) New(d *Doodle) error {
|
||||
d.Flash("Starting a new map")
|
||||
d.NewMap()
|
||||
d.GotoNewMenu()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/lib/events"
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"github.com/kirsle/golog"
|
||||
|
@ -57,12 +58,17 @@ func New(debug bool, engine render.Engine) *Doodle {
|
|||
|
||||
if debug {
|
||||
log.Logger.Config.Level = golog.DebugLevel
|
||||
DebugOverlay = true // on by default in debug mode, F3 to disable
|
||||
// DebugOverlay = true // on by default in debug mode, F3 to disable
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Title returns the game's preferred window title.
|
||||
func (d *Doodle) Title() string {
|
||||
return fmt.Sprintf("%s v%s", branding.AppName, branding.Version)
|
||||
}
|
||||
|
||||
// SetupEngine sets up the rendering engine.
|
||||
func (d *Doodle) SetupEngine() error {
|
||||
if err := d.Engine.Setup(); err != nil {
|
||||
|
@ -83,8 +89,8 @@ func (d *Doodle) Run() error {
|
|||
// Set up the default scene.
|
||||
if d.Scene == nil {
|
||||
// d.Goto(&GUITestScene{})
|
||||
d.NewMap()
|
||||
// d.Goto(&MainScene{})
|
||||
// d.NewMap()
|
||||
d.Goto(&MainScene{})
|
||||
}
|
||||
|
||||
log.Info("Enter Main Loop")
|
||||
|
|
|
@ -363,7 +363,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
|
|||
menuButton{
|
||||
Text: "New Level",
|
||||
Click: func(render.Point) {
|
||||
d.NewMap()
|
||||
d.GotoNewMenu()
|
||||
},
|
||||
},
|
||||
menuButton{
|
||||
|
|
|
@ -153,6 +153,8 @@ func (d *Doodle) DrawCollisionBox(actor doodads.Actor) {
|
|||
}
|
||||
|
||||
// TrackFPS shows the current FPS once per second.
|
||||
//
|
||||
// In debug mode, changes the window title to include the FPS counter.
|
||||
func (d *Doodle) TrackFPS(skipped uint32) {
|
||||
fpsFrames++
|
||||
fpsCurrentTicks = d.Engine.GetTicks()
|
||||
|
@ -168,4 +170,8 @@ func (d *Doodle) TrackFPS(skipped uint32) {
|
|||
fpsFrames = 0
|
||||
fpsSkipped = skipped
|
||||
}
|
||||
|
||||
if d.Debug {
|
||||
d.Engine.SetTitle(fmt.Sprintf("%s (%d FPS)", d.Title(), fpsCurrent))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// PageType configures the bounds and wallpaper behavior of a Level.
|
||||
type PageType int
|
||||
|
||||
|
@ -29,4 +31,32 @@ const (
|
|||
// - The wallpaper vert mirrors Top along the Y=Width plane
|
||||
// - The wallpaper 180 rotates the Corner for opposite corners
|
||||
Bordered
|
||||
|
||||
// If you add new PageType, also update the two functions below.
|
||||
)
|
||||
|
||||
// String converts the PageType to a string label.
|
||||
func (p PageType) String() string {
|
||||
switch p {
|
||||
case Unbounded:
|
||||
return "Unbounded"
|
||||
case NoNegativeSpace:
|
||||
return "NoNegativeSpace"
|
||||
case Bounded:
|
||||
return "Bounded"
|
||||
case Bordered:
|
||||
return "Bordered"
|
||||
}
|
||||
return fmt.Sprintf("PageType<%d>", p)
|
||||
}
|
||||
|
||||
// PageTypeFromString returns a PageType from its string version.
|
||||
func PageTypeFromString(name string) (PageType, bool) {
|
||||
// The min and max PageType value.
|
||||
for i := Unbounded; i <= Bordered; i++ {
|
||||
if name == i.String() {
|
||||
return PageType(i), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
|
|
@ -6,12 +6,18 @@ import (
|
|||
"git.kirsle.net/apps/doodle/lib/ui"
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/uix"
|
||||
)
|
||||
|
||||
// MainScene implements the main menu of Doodle.
|
||||
type MainScene struct {
|
||||
Supervisor *ui.Supervisor
|
||||
frame *ui.Frame
|
||||
|
||||
// Background wallpaper canvas.
|
||||
canvas *uix.Canvas
|
||||
}
|
||||
|
||||
// Name of the scene.
|
||||
|
@ -23,6 +29,20 @@ func (s *MainScene) Name() string {
|
|||
func (s *MainScene) Setup(d *Doodle) error {
|
||||
s.Supervisor = ui.NewSupervisor()
|
||||
|
||||
// Set up the background wallpaper canvas.
|
||||
s.canvas = uix.NewCanvas(100, false)
|
||||
s.canvas.Resize(render.Rect{
|
||||
W: int32(d.width),
|
||||
H: int32(d.height),
|
||||
})
|
||||
s.canvas.LoadLevel(d.Engine, &level.Level{
|
||||
Chunker: level.NewChunker(100),
|
||||
Palette: level.NewPalette(),
|
||||
PageType: level.Bounded,
|
||||
Wallpaper: "notebook.png",
|
||||
})
|
||||
|
||||
// Main UI button frame.
|
||||
frame := ui.NewFrame("frame")
|
||||
s.frame = frame
|
||||
|
||||
|
@ -31,14 +51,13 @@ func (s *MainScene) Setup(d *Doodle) error {
|
|||
Font: balance.StatusFont,
|
||||
}))
|
||||
button1.Handle(ui.Click, func(p render.Point) {
|
||||
d.NewMap()
|
||||
d.GotoNewMenu()
|
||||
})
|
||||
|
||||
button2 := ui.NewButton("Button2", ui.NewLabel(ui.Label{
|
||||
Text: "New Map",
|
||||
Text: "Load Map",
|
||||
Font: balance.StatusFont,
|
||||
}))
|
||||
button2.SetText("Load Map")
|
||||
|
||||
frame.Pack(button1, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
|
@ -59,6 +78,18 @@ func (s *MainScene) Setup(d *Doodle) error {
|
|||
// Loop the editor scene.
|
||||
func (s *MainScene) Loop(d *Doodle, ev *events.State) error {
|
||||
s.Supervisor.Loop(ev)
|
||||
|
||||
if resized := ev.Resized.Read(); resized {
|
||||
w, h := d.Engine.WindowSize()
|
||||
d.width = w
|
||||
d.height = h
|
||||
log.Info("Resized to %dx%d", d.width, d.height)
|
||||
s.canvas.Resize(render.Rect{
|
||||
W: int32(d.width),
|
||||
H: int32(d.height),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -67,6 +98,8 @@ func (s *MainScene) Draw(d *Doodle) error {
|
|||
// Clear the canvas and fill it with white.
|
||||
d.Engine.Clear(render.White)
|
||||
|
||||
s.canvas.Present(d.Engine, render.Origin)
|
||||
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: branding.AppName,
|
||||
Font: render.Text{
|
||||
|
|
282
pkg/menu_scene.go
Normal file
282
pkg/menu_scene.go
Normal file
|
@ -0,0 +1,282 @@
|
|||
package doodle
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/lib/events"
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/lib/ui"
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
)
|
||||
|
||||
/*
|
||||
MenuScene holds the main dialog menu UIs for:
|
||||
|
||||
* New Level
|
||||
* Open Level
|
||||
* Settings
|
||||
*/
|
||||
type MenuScene struct {
|
||||
// Configuration.
|
||||
StartupMenu string
|
||||
|
||||
Supervisor *ui.Supervisor
|
||||
|
||||
// Private widgets.
|
||||
window *ui.Window
|
||||
|
||||
// Values for the New menu
|
||||
newPageType string
|
||||
newWallpaper string
|
||||
}
|
||||
|
||||
// Name of the scene.
|
||||
func (s *MenuScene) Name() string {
|
||||
return "Menu"
|
||||
}
|
||||
|
||||
// GotoNewMenu loads the MenuScene and shows the "New" window.
|
||||
func (d *Doodle) GotoNewMenu() {
|
||||
log.Info("Loading the MenuScene to the New window")
|
||||
scene := &MenuScene{
|
||||
StartupMenu: "new",
|
||||
}
|
||||
d.Goto(scene)
|
||||
}
|
||||
|
||||
// GotoLoadMenu loads the MenuScene and shows the "Load" window.
|
||||
func (d *Doodle) GotoLoadMenu() {
|
||||
log.Info("Loading the MenuScene to the Load window")
|
||||
scene := &MenuScene{
|
||||
StartupMenu: "load",
|
||||
}
|
||||
d.Goto(scene)
|
||||
}
|
||||
|
||||
// Setup the scene.
|
||||
func (s *MenuScene) Setup(d *Doodle) error {
|
||||
s.Supervisor = ui.NewSupervisor()
|
||||
|
||||
switch s.StartupMenu {
|
||||
case "new":
|
||||
if err := s.setupNewWindow(d); err != nil {
|
||||
return err
|
||||
}
|
||||
case "load":
|
||||
if err := s.setupLoadWindow(d); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
d.Flash("No Valid StartupMenu Given to MenuScene")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupNewWindow sets up the UI for the "New" window.
|
||||
func (s *MenuScene) setupNewWindow(d *Doodle) error {
|
||||
// Default scene options.
|
||||
s.newPageType = level.Bounded.String()
|
||||
s.newWallpaper = "notebook.png"
|
||||
|
||||
window := ui.NewWindow("New Drawing")
|
||||
window.Configure(ui.Config{
|
||||
Width: int32(float64(d.width) * 0.8),
|
||||
Height: int32(float64(d.height) * 0.8),
|
||||
Background: render.Grey,
|
||||
})
|
||||
window.Compute(d.Engine)
|
||||
|
||||
{
|
||||
frame := ui.NewFrame("New Level Frame")
|
||||
window.Pack(frame, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
Fill: true,
|
||||
Expand: true,
|
||||
})
|
||||
|
||||
/******************
|
||||
* Frame for selecting Page Type
|
||||
******************/
|
||||
|
||||
label1 := ui.NewLabel(ui.Label{
|
||||
Text: "Page Type",
|
||||
Font: balance.LabelFont,
|
||||
})
|
||||
frame.Pack(label1, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
FillX: true,
|
||||
})
|
||||
|
||||
typeFrame := ui.NewFrame("Page Type Options Frame")
|
||||
frame.Pack(typeFrame, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
FillX: true,
|
||||
})
|
||||
|
||||
var types = []struct {
|
||||
Name string
|
||||
Value level.PageType
|
||||
}{
|
||||
{"Unbounded", level.Unbounded},
|
||||
{"Bounded", level.Bounded},
|
||||
{"No Negative Space", level.NoNegativeSpace},
|
||||
{"Bordered", level.Bordered},
|
||||
}
|
||||
for _, t := range types {
|
||||
// Hide some options for the free version of the game.
|
||||
if balance.FreeVersion {
|
||||
if t.Value != level.Bounded {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
radio := ui.NewRadioButton(t.Name,
|
||||
&s.newPageType,
|
||||
t.Value.String(),
|
||||
ui.NewLabel(ui.Label{
|
||||
Text: t.Name,
|
||||
Font: balance.MenuFont,
|
||||
}),
|
||||
)
|
||||
s.Supervisor.Add(radio)
|
||||
typeFrame.Pack(radio, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
PadX: 4,
|
||||
})
|
||||
}
|
||||
|
||||
/******************
|
||||
* Frame for selecting Level Wallpaper
|
||||
******************/
|
||||
|
||||
label2 := ui.NewLabel(ui.Label{
|
||||
Text: "Wallpaper",
|
||||
Font: balance.LabelFont,
|
||||
})
|
||||
frame.Pack(label2, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
FillX: true,
|
||||
})
|
||||
|
||||
wpFrame := ui.NewFrame("Wallpaper Frame")
|
||||
frame.Pack(wpFrame, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
FillX: true,
|
||||
})
|
||||
|
||||
var wallpapers = []struct {
|
||||
Name string
|
||||
Value string
|
||||
}{
|
||||
{"Notebook", "notebook.png"},
|
||||
{"Blueprint", "blueprint.png"},
|
||||
{"Legal Pad", "legal.png"},
|
||||
{"Placemat", "placemat.png"},
|
||||
}
|
||||
for _, t := range wallpapers {
|
||||
radio := ui.NewRadioButton(t.Name, &s.newWallpaper, t.Value, ui.NewLabel(ui.Label{
|
||||
Text: t.Name,
|
||||
Font: balance.MenuFont,
|
||||
}))
|
||||
s.Supervisor.Add(radio)
|
||||
wpFrame.Pack(radio, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
PadX: 4,
|
||||
})
|
||||
}
|
||||
|
||||
/******************
|
||||
* Confirm/cancel buttons.
|
||||
******************/
|
||||
|
||||
bottomFrame := ui.NewFrame("Button Frame")
|
||||
// bottomFrame.Configure(ui.Config{
|
||||
// BorderSize: 1,
|
||||
// BorderStyle: ui.BorderSunken,
|
||||
// BorderColor: render.Black,
|
||||
// })
|
||||
// bottomFrame.SetBackground(render.Grey)
|
||||
frame.Pack(bottomFrame, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
FillX: true,
|
||||
PadY: 8,
|
||||
})
|
||||
|
||||
var buttons = []struct {
|
||||
Label string
|
||||
F func(render.Point)
|
||||
}{
|
||||
{"Continue", func(p render.Point) {
|
||||
d.Flash("Create new map with %s page type and %s wallpaper", s.newPageType, s.newWallpaper)
|
||||
pageType, ok := level.PageTypeFromString(s.newPageType)
|
||||
if !ok {
|
||||
d.Flash("Invalid Page Type '%s'", s.newPageType)
|
||||
return
|
||||
}
|
||||
|
||||
lvl := level.New()
|
||||
lvl.Palette = level.DefaultPalette()
|
||||
lvl.Wallpaper = s.newWallpaper
|
||||
lvl.PageType = pageType
|
||||
|
||||
d.Goto(&EditorScene{
|
||||
DrawingType: enum.LevelDrawing,
|
||||
Level: lvl,
|
||||
})
|
||||
}},
|
||||
|
||||
{"Cancel", func(p render.Point) {
|
||||
d.Goto(&MainScene{})
|
||||
}},
|
||||
}
|
||||
for _, t := range buttons {
|
||||
btn := ui.NewButton(t.Label, ui.NewLabel(ui.Label{
|
||||
Text: t.Label,
|
||||
Font: balance.MenuFont,
|
||||
}))
|
||||
btn.Handle(ui.Click, t.F)
|
||||
s.Supervisor.Add(btn)
|
||||
bottomFrame.Pack(btn, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
PadX: 4,
|
||||
PadY: 8,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
s.window = window
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupLoadWindow sets up the UI for the "New" window.
|
||||
func (s *MenuScene) setupLoadWindow(d *Doodle) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Loop the editor scene.
|
||||
func (s *MenuScene) Loop(d *Doodle, ev *events.State) error {
|
||||
s.Supervisor.Loop(ev)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Draw the pixels on this frame.
|
||||
func (s *MenuScene) Draw(d *Doodle) error {
|
||||
// Clear the canvas and fill it with white.
|
||||
d.Engine.Clear(render.White)
|
||||
|
||||
s.window.Compute(d.Engine)
|
||||
s.window.MoveTo(render.Point{
|
||||
X: (int32(d.width) / 2) - (s.window.Size().W / 2),
|
||||
Y: 60,
|
||||
})
|
||||
s.window.Present(d.Engine, s.window.Point())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy the scene.
|
||||
func (s *MenuScene) Destroy() error {
|
||||
return nil
|
||||
}
|
|
@ -123,7 +123,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
|
|||
*s.debScroll = s.drawing.Scroll.String()
|
||||
|
||||
// Has the window been resized?
|
||||
if resized := ev.Resized.Read(); resized {
|
||||
if resized := ev.Resized.Now; resized {
|
||||
w, h := d.Engine.WindowSize()
|
||||
if w != d.width || h != d.height {
|
||||
d.width = w
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/collision"
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/scripting"
|
||||
"github.com/kirsle/blog/src/log"
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
|
|
|
@ -69,7 +69,8 @@ func (w *Canvas) loopConstrainScroll() error {
|
|||
}
|
||||
|
||||
// Constrain the bottom and right for limited world sizes.
|
||||
if w.wallpaper.maxWidth+w.wallpaper.maxHeight > 0 {
|
||||
if w.wallpaper.pageType >= level.Bounded &&
|
||||
w.wallpaper.maxWidth+w.wallpaper.maxHeight > 0 {
|
||||
var (
|
||||
// TODO: downcast from int64!
|
||||
mw = int32(w.wallpaper.maxWidth)
|
||||
|
|
|
@ -46,16 +46,18 @@ func (w *Canvas) loopContainActorsInsideLevel(a *Actor) {
|
|||
}
|
||||
|
||||
// Bound it on the right bottom edges. XXX: downcast from int64!
|
||||
if w.wallpaper.maxWidth > 0 {
|
||||
if int64(orig.X+size.W) > w.wallpaper.maxWidth {
|
||||
var delta = int32(w.wallpaper.maxWidth - int64(orig.X+size.W))
|
||||
moveBy.X = delta
|
||||
if w.wallpaper.pageType >= level.Bounded {
|
||||
if w.wallpaper.maxWidth > 0 {
|
||||
if int64(orig.X+size.W) > w.wallpaper.maxWidth {
|
||||
var delta = int32(w.wallpaper.maxWidth - int64(orig.X+size.W))
|
||||
moveBy.X = delta
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.wallpaper.maxHeight > 0 {
|
||||
if int64(orig.Y+size.H) > w.wallpaper.maxHeight {
|
||||
var delta = int32(w.wallpaper.maxHeight - int64(orig.Y+size.H))
|
||||
moveBy.Y = delta
|
||||
if w.wallpaper.maxHeight > 0 {
|
||||
if int64(orig.Y+size.H) > w.wallpaper.maxHeight {
|
||||
var delta = int32(w.wallpaper.maxHeight - int64(orig.Y+size.H))
|
||||
moveBy.Y = delta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user