mirror of
https://codeberg.org/scip/kageviewer.git
synced 2025-12-18 21:11:05 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b384795e53 | |||
| b41d23a2fd | |||
| dacbb5567c | |||
| 6cb30560d0 | |||
| e42df9080f | |||
| 8cd2d74a8b | |||
| 490afb1d76 |
BIN
.github/assets/logo.png
vendored
Normal file
BIN
.github/assets/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
.github/assets/screenshot.png
vendored
Normal file
BIN
.github/assets/screenshot.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
releases
|
||||
kage-viewer
|
||||
|
||||
6
Makefile
6
Makefile
@@ -40,6 +40,12 @@ install: buildlocal
|
||||
clean:
|
||||
rm -rf $(tool) coverage.out testdata t/out
|
||||
|
||||
shader-destruct: buildlocal
|
||||
./$(tool) -g 32x32 -i example/wall.png -i example/damage.png --map-ticks Time -s example/destruct.kage
|
||||
|
||||
shader-ebiten: buildlocal
|
||||
./$(tool) -g 640x480 --map-ticks Time --map-mouse Cursor -s example/ebiten.kage
|
||||
|
||||
test: clean
|
||||
mkdir -p t/out
|
||||
go test ./... $(ARGS)
|
||||
|
||||
48
README.md
48
README.md
@@ -1,9 +1,21 @@
|
||||
# kage-viewer - Viewer for shaders written in Kage, similar to glslviewer
|
||||
|
||||

|
||||
|
||||
[](https://github.com/tlinden/kage-viewer/blob/master/LICENSE)
|
||||
[](https://goreportcard.com/report/github.com/tlinden/kage-viewer)
|
||||
|
||||
This little tool can be used to test shaders written in [Kage](https://ebitengine.org/en/documents/shader.html), a shader meta language for [Ebitengine](https://github.com/hajimehoshi/ebiten).
|
||||
This little tool can be used to test shaders written in
|
||||
[Kage](https://ebitengine.org/en/documents/shader.html), a shader meta
|
||||
language for
|
||||
[Ebitengine](https://github.com/hajimehoshi/ebiten). kage-viewer
|
||||
reloads changed assets, which allows you to develop your shader and
|
||||
see live, how it responds to your changes. If loading fails, an error
|
||||
will be printed to STDOUT. The same applies for images.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
@@ -61,13 +73,19 @@ Usage: kage-viewer [-vd] [-c <config file>] [-g geom] [-p geom] \
|
||||
-i <image0.png> -i <image1.png> -s <shader.kage>
|
||||
|
||||
Options:
|
||||
-c --config <toml file> Config file to use (optional)
|
||||
-i --image <png file> Image to load (multiple times allowed, up to 4)
|
||||
-s --shader <kage file> Shader to run
|
||||
-g --geometry <WIDTHxHEIGHT> Window size
|
||||
-p --position <XxY> Position of image0
|
||||
-d --debug Show debugging output
|
||||
-v --version Show program version
|
||||
-c --config <toml file> Config file to use (optional)
|
||||
-i --image <png file> Image to load (multiple times allowed, up to 4)
|
||||
-s --shader <kage file> Shader to run
|
||||
-g --geometry <WIDTHxHEIGHT> Window size
|
||||
-p --position <XxY> Position of image0
|
||||
-b --background <png file> Image to load as background
|
||||
-t --tps <ticks/s> At how many ticks per second to run
|
||||
--map-flag <name> Map Flag uniform to <name>
|
||||
--map-ticks <name> Map Flag uniform to <name>
|
||||
--map-slider <name> Map Flag uniform to <name>
|
||||
--map-mouse <name> Map Flag uniform to <name>
|
||||
-d --debug Show debugging output
|
||||
-v --version Show program version
|
||||
```
|
||||
|
||||
Example usage using the provided example:
|
||||
@@ -90,9 +108,19 @@ Uniforms supported so far:
|
||||
`SPACE` or pusing the left mouse button
|
||||
- `var Slider float`: a normalized float value, you can increment it
|
||||
with `UP` or `DOWN`
|
||||
- `var Ticks int`: the time the game runs (ticks, not seconds!)
|
||||
- `var Ticks float`: the time the game runs (ticks, not seconds!)
|
||||
- `var Mouse vec2`: the current mouse position
|
||||
|
||||
If you want to test an existing shader and don't want to rename the
|
||||
uniforms, you can map the ones provided by **kage-viewer** to custom
|
||||
names using the `--map-*` options. For example:
|
||||
|
||||
```shell
|
||||
kage-viewer -g 640x480 --map-ticks Time --map-mouse Cursor examples/shader/default.go
|
||||
```
|
||||
|
||||
This executes the example shader in the ebitenging source repository.
|
||||
|
||||
# Config File
|
||||
|
||||
You can use a config file to store your own codes, once you found one
|
||||
@@ -115,7 +143,7 @@ Possible parameters equal the long command line options.
|
||||
- [X] Implement loading of images and shader files
|
||||
- [X] Implement basic shader rendering and user input
|
||||
- [ ] Add custom uniforms (maybe using lua code?)
|
||||
- [ ] Provide a way to respond live to shader code changes (use lua as
|
||||
- [x] Provide a way to respond live to shader code changes (use lua as
|
||||
well?)
|
||||
|
||||
# Report bugs
|
||||
|
||||
60
config.go
60
config.go
@@ -32,31 +32,43 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
VERSION string = "0.0.1"
|
||||
VERSION string = "0.0.4"
|
||||
Usage string = `This is kage-viewer, a shader viewer.
|
||||
|
||||
Usage: kage-viewer [-vd] [-c <config file>] [-g geom] [-p geom] \
|
||||
-i <image0.png> -i <image1.png> -s <shader.kage>
|
||||
|
||||
Options:
|
||||
-c --config <toml file> Config file to use (optional)
|
||||
-i --image <png file> Image to load (multiple times allowed, up to 4)
|
||||
-s --shader <kage file> Shader to run
|
||||
-g --geometry <WIDTHxHEIGHT> Window size
|
||||
-p --position <XxY> Position of image0
|
||||
-d --debug Show debugging output
|
||||
-v --version Show program version
|
||||
-c --config <toml file> Config file to use (optional)
|
||||
-i --image <png file> Image to load (multiple times allowed, up to 4)
|
||||
-s --shader <kage file> Shader to run
|
||||
-g --geometry <WIDTHxHEIGHT> Window size
|
||||
-p --position <XxY> Position of image0
|
||||
-b --background <png file> Image to load as background
|
||||
-t --tps <ticks/s> At how many ticks per second to run
|
||||
--map-flag <name> Map Flag uniform to <name>
|
||||
--map-ticks <name> Map Flag uniform to <name>
|
||||
--map-slider <name> Map Flag uniform to <name>
|
||||
--map-mouse <name> Map Flag uniform to <name>
|
||||
-d --debug Show debugging output
|
||||
-v --version Show program version
|
||||
`
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Showversion bool `koanf:"version"` // -v
|
||||
Debug bool `koanf:"debug"` // -d
|
||||
Config string `koanf:"config"` // -c
|
||||
Image []string `koanf:"image"` // -i
|
||||
Shader string `koanf:"shader"` // -s
|
||||
Geo string `koanf:"geometry"` // -g
|
||||
Posision string `koanf:"position"` // -p
|
||||
Showversion bool `koanf:"version"` // -v
|
||||
Debug bool `koanf:"debug"` // -d
|
||||
Config string `koanf:"config"` // -c
|
||||
Image []string `koanf:"image"` // -i
|
||||
Shader string `koanf:"shader"` // -s
|
||||
Background string `koanf:"background"` // -b
|
||||
TPS int `koanf:"tps"` // -t
|
||||
Geo string `koanf:"geometry"` // -g
|
||||
Posision string `koanf:"position"` // -p
|
||||
Flag string `koanf:"map-flag"`
|
||||
Ticks string `koanf:"map-ticks"`
|
||||
Mouse string `koanf:"map-mouse"`
|
||||
Slider string `koanf:"map-slider"`
|
||||
|
||||
X, Y, Width, Height int // feed from -g + -p
|
||||
}
|
||||
@@ -64,14 +76,6 @@ type Config struct {
|
||||
func InitConfig() (*Config, error) {
|
||||
var kloader = koanf.New(".")
|
||||
|
||||
// Load default values using the confmap provider.
|
||||
/* not needed yet
|
||||
if err := kloader.Load(confmap.Provider(map[string]interface{}{
|
||||
}, "."), nil); err != nil {
|
||||
return nil, fmt.Errorf("failed to load default values into koanf: %w", err)
|
||||
}
|
||||
*/
|
||||
|
||||
// setup custom usage
|
||||
flagset := flag.NewFlagSet("config", flag.ContinueOnError)
|
||||
flagset.Usage = func() {
|
||||
@@ -87,6 +91,12 @@ func InitConfig() (*Config, error) {
|
||||
flagset.StringP("position", "p", "0x0", "position of shader")
|
||||
flagset.StringArrayP("image", "i", nil, "image file")
|
||||
flagset.StringP("shader", "s", "", "shader file")
|
||||
flagset.StringP("map-flag", "", "Flag", "map flag uniform")
|
||||
flagset.StringP("map-ticks", "", "Ticks", "map ticks uniform")
|
||||
flagset.StringP("map-mouse", "", "Mouse", "map mouse uniform")
|
||||
flagset.StringP("map-slider", "", "Slider", "map slider uniform")
|
||||
flagset.StringP("background", "b", "", "background image")
|
||||
flagset.IntP("tps", "t", 60, "ticks per second")
|
||||
|
||||
if err := flagset.Parse(os.Args[1:]); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse program arguments: %w", err)
|
||||
@@ -140,8 +150,8 @@ func InitConfig() (*Config, error) {
|
||||
}
|
||||
|
||||
func SanitiyCheck(conf *Config) error {
|
||||
if len(conf.Image) < 1 {
|
||||
return fmt.Errorf("at least 1 image must be specified")
|
||||
if len(conf.Image) > 4 {
|
||||
return fmt.Errorf("only 4 images can be specified")
|
||||
}
|
||||
|
||||
if conf.Shader == "" {
|
||||
|
||||
@@ -20,7 +20,7 @@ package main
|
||||
|
||||
var Flag int
|
||||
var Slider float
|
||||
var Time int
|
||||
var Time float
|
||||
var Mouse vec2
|
||||
|
||||
func Fragment(_ vec4, texCoord vec2, _ vec4) vec4 {
|
||||
33
example/ebiten.kage
Normal file
33
example/ebiten.kage
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2020 The Ebiten Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build ignore
|
||||
|
||||
//kage:unit pixels
|
||||
|
||||
package main
|
||||
|
||||
var Time float
|
||||
var Cursor vec2
|
||||
|
||||
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
|
||||
pos := (dstPos.xy - imageDstOrigin()) / imageDstSize()
|
||||
pos += Cursor / imageDstSize() / 4
|
||||
clr := 0.0
|
||||
clr += sin(pos.x*cos(Time/15)*80) + cos(pos.y*cos(Time/15)*10)
|
||||
clr += sin(pos.y*sin(Time/10)*40) + cos(pos.x*sin(Time/25)*40)
|
||||
clr += sin(pos.x*sin(Time/5)*10) + sin(pos.y*sin(Time/35)*80)
|
||||
clr *= sin(Time/10) * 0.5
|
||||
return vec4(clr, clr*0.5, sin(clr+Time/3)*0.75, 1)
|
||||
}
|
||||
83
game.go
83
game.go
@@ -24,17 +24,20 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/Zyko0/Ebiary/asset"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
Conf *Config
|
||||
Images []*ebiten.Image
|
||||
Shader *ebiten.Shader
|
||||
Ticks int
|
||||
Slider float64
|
||||
Flag int
|
||||
Conf *Config
|
||||
Images []*asset.LiveAsset[*ebiten.Image]
|
||||
Shader *asset.LiveAsset[*ebiten.Shader]
|
||||
Cursor []float64
|
||||
Ticks int
|
||||
Slider float64
|
||||
Flag int
|
||||
Background *asset.LiveAsset[*ebiten.Image]
|
||||
}
|
||||
|
||||
func LoadImage(name string) (*ebiten.Image, error) {
|
||||
@@ -55,26 +58,35 @@ func LoadImage(name string) (*ebiten.Image, error) {
|
||||
func (game *Game) Init() error {
|
||||
for _, image := range game.Conf.Image {
|
||||
slog.Debug("Loading images", "image", image)
|
||||
img, err := LoadImage(image)
|
||||
//img, err := LoadImage(image)
|
||||
img, err := asset.NewLiveAsset[*ebiten.Image](image)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to load image %s: %s", image, err)
|
||||
}
|
||||
|
||||
game.Images = append(game.Images, img)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(game.Conf.Shader)
|
||||
shader, err := asset.NewLiveAsset[*ebiten.Shader](game.Conf.Shader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load shader %s: %s", game.Conf.Shader, err)
|
||||
}
|
||||
|
||||
shader, err := ebiten.NewShader(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new shader %s: %s", game.Conf.Shader, err)
|
||||
if game.Conf.Background != "" {
|
||||
slog.Debug("Loading background", "image", game.Conf.Background)
|
||||
|
||||
img, err := asset.NewLiveAsset[*ebiten.Image](game.Conf.Background)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load image %s: %s", game.Conf.Background, err)
|
||||
}
|
||||
|
||||
game.Background = img
|
||||
}
|
||||
|
||||
game.Shader = shader
|
||||
|
||||
ebiten.SetMaxTPS(game.Conf.TPS)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -120,34 +132,61 @@ func (g *Game) Down() {
|
||||
}
|
||||
|
||||
func (game *Game) Update() error {
|
||||
if game.CheckInput() {
|
||||
slog.Debug("Key pressed", "Slider", game.Slider, "Flag", game.Flag)
|
||||
for _, image := range game.Images {
|
||||
if image.Error() != nil {
|
||||
fmt.Println("warn: image reloading error:", image.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if game.Shader.Error() != nil {
|
||||
fmt.Println("warn: shader reloading error:", game.Shader.Error())
|
||||
}
|
||||
|
||||
if game.Background != nil {
|
||||
if game.Background.Error() != nil {
|
||||
fmt.Println("warn: background image reloading error:", game.Background.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if game.CheckInput() {
|
||||
slog.Debug("Key pressed",
|
||||
game.Conf.Flag, game.Flag,
|
||||
game.Conf.Slider, game.Slider,
|
||||
game.Conf.Ticks, fmt.Sprintf("%.02f", float64(game.Ticks)/60),
|
||||
game.Conf.Mouse, fmt.Sprintf("%.02f, %.02f", game.Cursor[0], game.Cursor[1]),
|
||||
)
|
||||
}
|
||||
|
||||
mousex, mousey := ebiten.CursorPosition()
|
||||
game.Cursor = []float64{float64(mousex), float64(mousey)}
|
||||
|
||||
game.Ticks++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (game *Game) Draw(screen *ebiten.Image) {
|
||||
if game.Background != nil {
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
screen.DrawImage(game.Background.Value(), op)
|
||||
}
|
||||
|
||||
op := &ebiten.DrawRectShaderOptions{}
|
||||
|
||||
mousex, mousey := ebiten.CursorPosition()
|
||||
|
||||
op.Uniforms = map[string]any{
|
||||
"Flag": game.Flag,
|
||||
"Slider": game.Slider,
|
||||
"Ticks": game.Ticks,
|
||||
"Mouse": []float64{float64(mousex), float64(mousey)},
|
||||
game.Conf.Flag: game.Flag,
|
||||
game.Conf.Slider: game.Slider,
|
||||
game.Conf.Ticks: float64(game.Ticks) / 60,
|
||||
game.Conf.Mouse: game.Cursor,
|
||||
}
|
||||
|
||||
for idx, image := range game.Images {
|
||||
op.Images[idx] = image
|
||||
op.Images[idx] = image.Value()
|
||||
}
|
||||
|
||||
op.GeoM.Translate(float64(game.Conf.X), float64(game.Conf.Y))
|
||||
|
||||
screen.DrawRectShader(game.Conf.Width, game.Conf.Height, game.Shader, op)
|
||||
screen.DrawRectShader(game.Conf.Width, game.Conf.Height, game.Shader.Value(), op)
|
||||
}
|
||||
|
||||
func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||
|
||||
3
go.mod
3
go.mod
@@ -3,9 +3,10 @@ module github.com/TLINDEN/kage-viewer
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/Zyko0/Ebiary/asset v0.0.0-20240304185439-be56fe8a2a6a // indirect
|
||||
github.com/ebitengine/purego v0.6.0 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
||||
github.com/hajimehoshi/ebiten/v2 v2.6.7 // indirect
|
||||
github.com/jezek/xgb v1.1.0 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,9 +1,13 @@
|
||||
github.com/Zyko0/Ebiary/asset v0.0.0-20240304185439-be56fe8a2a6a h1:kn4fhGvVA6T1lK7qWujIj3m7e9imCZe4MHBuBeflKgU=
|
||||
github.com/Zyko0/Ebiary/asset v0.0.0-20240304185439-be56fe8a2a6a/go.mod h1:4CqqwHRUbvGBpBd5ye4MxDA4k/XtZqrAD1sg9uxmcYI=
|
||||
github.com/ebitengine/purego v0.6.0 h1:Yo9uBc1x+ETQbfEaf6wcBsjrQfCEnh/gaGUg7lguEJY=
|
||||
github.com/ebitengine/purego v0.6.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.6.7 h1:rxlMxu487wZN/JteykmuGdO1qotOolL8vJDU85lPh7A=
|
||||
|
||||
BIN
kage-viewer
BIN
kage-viewer
Binary file not shown.
9
main.go
9
main.go
@@ -21,7 +21,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"log/slog"
|
||||
|
||||
@@ -46,7 +45,6 @@ func main() {
|
||||
if conf.Debug {
|
||||
logLevel := &slog.LevelVar{}
|
||||
// we're using a more verbose logger in debug mode
|
||||
buildInfo, _ := debug.ReadBuildInfo()
|
||||
opts := &yadu.Options{
|
||||
Level: logLevel,
|
||||
AddSource: true,
|
||||
@@ -55,12 +53,7 @@ func main() {
|
||||
logLevel.Set(slog.LevelDebug)
|
||||
|
||||
handler := yadu.NewHandler(os.Stdout, opts)
|
||||
debuglogger := slog.New(handler).With(
|
||||
slog.Group("program_info",
|
||||
slog.Int("pid", os.Getpid()),
|
||||
slog.String("go_version", buildInfo.GoVersion),
|
||||
),
|
||||
)
|
||||
debuglogger := slog.New(handler)
|
||||
slog.SetDefault(debuglogger)
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
f73335dc3e4e65b089624e9580b02d38926597c59127ca507be03148ab229b4f
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
d7726c68b43c550c27a42f7586a418915d042ed9a444f651cb5159e37bdea4ab
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
91b57cfbaa1ec63b797fbb4c4b3892c2e462ec7668207a3c645917af9c7ac3d1
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
197bfab6dda912f8ed8f8d1b7afb1946d1c7640ab84ccb4a48c8d5d7c62fe8ea
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
c69df9ea174b69fb17e5f1e359106bce6105f6185e09531912e04398a4b72eff
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
5c61f307b7b576f10d4a5ae276dbb8070053efd060bbfda93f8535658478b2cf
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
df707023f0bd50fe1ff55e466f212c5668d6c488644b25d5880e64ca21c4f073
|
||||
Reference in New Issue
Block a user