Refactoring
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,3 +27,5 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
unreal-pm
|
||||||
|
|
||||||
|
|||||||
40
log.go
Normal file
40
log.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
logText *widget.Entry
|
||||||
|
logBuffer []string
|
||||||
|
logMaxLines int
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalLogger *Logger;
|
||||||
|
|
||||||
|
func InitLogger(text *widget.Entry, maxLines int) {
|
||||||
|
globalLogger = &Logger{
|
||||||
|
logText: text,
|
||||||
|
logMaxLines: logMaxLines,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Log(line string) {
|
||||||
|
if globalLogger == nil {
|
||||||
|
fmt.Println("Global Logger is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
globalLogger.log(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) log(line string) {
|
||||||
|
l.logBuffer = append(l.logBuffer, line)
|
||||||
|
if len(l.logBuffer) > l.logMaxLines {
|
||||||
|
l.logBuffer = l.logBuffer[1:]
|
||||||
|
}
|
||||||
|
l.logText.SetText(strings.Join(l.logBuffer, "\n"))
|
||||||
|
}
|
||||||
243
main.go
243
main.go
@@ -1,266 +1,39 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
"fyne.io/fyne/v2/canvas"
|
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/layout"
|
|
||||||
"fyne.io/fyne/v2/theme"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Project struct {
|
|
||||||
EnginePath string `json:"enginePath"`
|
|
||||||
ProjectPath string `json:"projectPath"`
|
|
||||||
ProjectName string `json:"projectName"`
|
|
||||||
Version string `json:"engineVersion"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectsFilePath = "projects.json"
|
const projectsFilePath = "projects.json"
|
||||||
var projectList *fyne.Container
|
|
||||||
var logText *widget.Entry
|
|
||||||
var scrollContainer *container.Scroll
|
|
||||||
var logBuffer []string
|
|
||||||
var logMaxLines = 7
|
var logMaxLines = 7
|
||||||
|
|
||||||
type hoverableButton struct {
|
|
||||||
widget.Button
|
|
||||||
onMouseIn func()
|
|
||||||
onMouseOut func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hoverableButton) MouseIn() {
|
|
||||||
if h.onMouseIn != nil {
|
|
||||||
h.onMouseIn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hoverableButton) MouseOut() {
|
|
||||||
if h.onMouseOut != nil {
|
|
||||||
h.onMouseOut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadProjects() ([]Project, error) {
|
|
||||||
file, err := os.ReadFile(projectsFilePath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var projects []Project
|
|
||||||
if err := json.Unmarshal(file, &projects); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return projects, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeDir(path string) {
|
|
||||||
err := os.RemoveAll(path)
|
|
||||||
if err != nil {
|
|
||||||
log("Can't remove content at " + path)
|
|
||||||
} else {
|
|
||||||
log("Removed " + path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanUnrealProject(project Project) {
|
|
||||||
dirs := []string {
|
|
||||||
"Binaries",
|
|
||||||
"DerivedDataCache",
|
|
||||||
"Intermediate",
|
|
||||||
"Saved",
|
|
||||||
"Script",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dir := range dirs {
|
|
||||||
fullPath := filepath.Join(project.ProjectPath, dir)
|
|
||||||
removeDir(fullPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginsPath := filepath.Join(project.ProjectPath, "Plugins")
|
|
||||||
entries, err := os.ReadDir(pluginsPath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginDir := filepath.Join(pluginsPath, entry.Name())
|
|
||||||
removeDir(filepath.Join(pluginDir, "Binaries"))
|
|
||||||
removeDir(filepath.Join(pluginDir, "Intermediate"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateUnrealSolution(project Project) {
|
|
||||||
var buildCmd string
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
buildCmd = project.EnginePath + "/Engine/Build/BatchFiles/Linux/GenerateProjectFiles.sh";
|
|
||||||
case "darwin":
|
|
||||||
buildCmd = project.EnginePath + "/Engine/Build/BatchFiles/Mac/GenerateProjectFiles.sh"
|
|
||||||
default:
|
|
||||||
fmt.Println("Generate Project is not yet supported on Windows")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
projectCmd := project.ProjectPath + "/" + project.ProjectName + ".uproject"
|
|
||||||
cmd := exec.Command(buildCmd, projectCmd, "-game")
|
|
||||||
|
|
||||||
logToOutput(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildUnrealSolution(project Project) {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
cmd := exec.Command("make", "-C", project.ProjectPath, project.ProjectName)
|
|
||||||
logToOutput(cmd)
|
|
||||||
default:
|
|
||||||
fmt.Println("Build Project is not yet supported on Windows")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runUnrealProject(project Project) {
|
|
||||||
var buildCmd string
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
buildCmd = project.EnginePath + "/Engine/Binaries/Linux/UnrealEditor"
|
|
||||||
case "darwin":
|
|
||||||
buildCmd = project.EnginePath + "/Engine/Binaries/Mac/UnrealEditor.app/Contents/MacOS/UnrealEditor"
|
|
||||||
default:
|
|
||||||
buildCmd = project.EnginePath + "/Engine/Binaries/Win64/UnrealEditor.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
projectCmd := project.ProjectPath + "/" + project.ProjectName + ".uproject"
|
|
||||||
cmd := exec.Command(buildCmd, projectCmd)
|
|
||||||
|
|
||||||
logToOutput(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logToOutput(cmd *exec.Cmd) {
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error during pipe stdout creation")
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr, err := cmd.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error during pipe stderr creation")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
fmt.Println("Error while loading command")
|
|
||||||
}
|
|
||||||
|
|
||||||
readPipe := func(reader io.ReadCloser) {
|
|
||||||
scanner := bufio.NewScanner(reader)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
log(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go readPipe(stdout)
|
|
||||||
go readPipe(stderr)
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
fmt.Println("Error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func log(line string) {
|
|
||||||
logBuffer = append(logBuffer, line)
|
|
||||||
if len(logBuffer) > logMaxLines {
|
|
||||||
logBuffer = logBuffer[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
logText.SetText(strings.Join(logBuffer, "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func newProjectRow(project Project) fyne.CanvasObject {
|
|
||||||
projectName := project.ProjectName
|
|
||||||
iconPath := filepath.Join(project.ProjectPath, projectName+".png")
|
|
||||||
var projectIcon fyne.Resource
|
|
||||||
|
|
||||||
if _, err := os.Stat(iconPath); err == nil {
|
|
||||||
icon, err := fyne.LoadResourceFromPath(iconPath)
|
|
||||||
if err == nil {
|
|
||||||
projectIcon = icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if projectIcon == nil {
|
|
||||||
projectIcon = theme.FileIcon()
|
|
||||||
}
|
|
||||||
|
|
||||||
icon := canvas.NewImageFromResource(projectIcon)
|
|
||||||
icon.SetMinSize(fyne.NewSize(48, 48))
|
|
||||||
|
|
||||||
nameLabel := widget.NewLabel(projectName + " / Unreal Engine " + project.Version)
|
|
||||||
nameLabel.Alignment = fyne.TextAlignLeading
|
|
||||||
|
|
||||||
// Crée les boutons masqués par défaut
|
|
||||||
clearButton := widget.NewButton("Clean", func() {
|
|
||||||
cleanUnrealProject(project)
|
|
||||||
})
|
|
||||||
generateBtn := widget.NewButton("Generate", func() {
|
|
||||||
generateUnrealSolution(project)
|
|
||||||
})
|
|
||||||
buildBtn := widget.NewButton("Build", func() {
|
|
||||||
buildUnrealSolution(project)
|
|
||||||
})
|
|
||||||
launchBtn := widget.NewButton("Run", func() {
|
|
||||||
runUnrealProject(project)
|
|
||||||
})
|
|
||||||
|
|
||||||
buttons := container.NewHBox(clearButton, generateBtn, buildBtn, launchBtn)
|
|
||||||
|
|
||||||
// Conteneur principal
|
|
||||||
row := container.NewHBox(
|
|
||||||
icon,
|
|
||||||
nameLabel,
|
|
||||||
layout.NewSpacer(),
|
|
||||||
buttons,
|
|
||||||
)
|
|
||||||
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
myApp := app.New()
|
myApp := app.New()
|
||||||
myWindow := myApp.NewWindow("Unreal Project Launcher")
|
myWindow := myApp.NewWindow("Unreal Project Launcher")
|
||||||
myWindow.Resize(fyne.NewSize(730, 460))
|
myWindow.Resize(fyne.NewSize(730, 460))
|
||||||
|
|
||||||
projects, err := loadProjects()
|
projects, err := LoadProjects(projectsFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error while loading projects")
|
fmt.Println("Error while loading projects")
|
||||||
}
|
}
|
||||||
|
|
||||||
logText = widget.NewMultiLineEntry()
|
logText := widget.NewMultiLineEntry()
|
||||||
logText.SetMinRowsVisible(logMaxLines);
|
logText.SetMinRowsVisible(logMaxLines);
|
||||||
|
|
||||||
|
InitLogger(logText, logMaxLines)
|
||||||
|
|
||||||
|
var projectList *fyne.Container
|
||||||
|
|
||||||
reloadUI := func() {
|
reloadUI := func() {
|
||||||
projectList.RemoveAll()
|
projectList.RemoveAll()
|
||||||
|
|
||||||
for _, project := range projects {
|
for _, project := range projects {
|
||||||
row := newProjectRow(project)
|
row := NewProjectRow(project)
|
||||||
projectList.Add(row)
|
projectList.Add(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,7 +41,7 @@ func main() {
|
|||||||
projectList = container.NewVBox()
|
projectList = container.NewVBox()
|
||||||
reloadUI()
|
reloadUI()
|
||||||
|
|
||||||
scrollContainer = container.NewVScroll(projectList)
|
scrollContainer := container.NewVScroll(projectList)
|
||||||
mainContainer := container.NewBorder(nil, logText, nil, nil, scrollContainer)
|
mainContainer := container.NewBorder(nil, logText, nil, nil, scrollContainer)
|
||||||
|
|
||||||
myWindow.SetContent(mainContainer)
|
myWindow.SetContent(mainContainer)
|
||||||
|
|||||||
61
ui.go
Normal file
61
ui.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/canvas"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewProjectRow(project Project) fyne.CanvasObject {
|
||||||
|
projectName := project.ProjectName
|
||||||
|
iconPath := filepath.Join(project.ProjectPath, projectName+".png")
|
||||||
|
var projectIcon fyne.Resource
|
||||||
|
|
||||||
|
if _, err := os.Stat(iconPath); err == nil {
|
||||||
|
icon, err := fyne.LoadResourceFromPath(iconPath)
|
||||||
|
if err == nil {
|
||||||
|
projectIcon = icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if projectIcon == nil {
|
||||||
|
projectIcon = theme.FileIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
icon := canvas.NewImageFromResource(projectIcon)
|
||||||
|
icon.SetMinSize(fyne.NewSize(48, 48))
|
||||||
|
|
||||||
|
nameLabel := widget.NewLabel(projectName + " / Unreal Engine " + project.Version)
|
||||||
|
nameLabel.Alignment = fyne.TextAlignLeading
|
||||||
|
|
||||||
|
// Crée les boutons masqués par défaut
|
||||||
|
clearButton := widget.NewButton("Clean", func() {
|
||||||
|
CleanUnrealProject(project)
|
||||||
|
})
|
||||||
|
generateBtn := widget.NewButton("Generate", func() {
|
||||||
|
GenerateUnrealSolution(project)
|
||||||
|
})
|
||||||
|
buildBtn := widget.NewButton("Build", func() {
|
||||||
|
BuildUnrealSolution(project)
|
||||||
|
})
|
||||||
|
launchBtn := widget.NewButton("Run", func() {
|
||||||
|
RunUnrealProject(project)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttons := container.NewHBox(clearButton, generateBtn, buildBtn, launchBtn)
|
||||||
|
|
||||||
|
// Conteneur principal
|
||||||
|
row := container.NewHBox(
|
||||||
|
icon,
|
||||||
|
nameLabel,
|
||||||
|
layout.NewSpacer(),
|
||||||
|
buttons,
|
||||||
|
)
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
153
utils.go
Normal file
153
utils.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
EnginePath string `json:"enginePath"`
|
||||||
|
ProjectPath string `json:"projectPath"`
|
||||||
|
ProjectName string `json:"projectName"`
|
||||||
|
Version string `json:"engineVersion"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadProjects(projectFilePath string) ([]Project, error) {
|
||||||
|
file, err := os.ReadFile(projectsFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var projects []Project
|
||||||
|
if err := json.Unmarshal(file, &projects); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDir(path string) {
|
||||||
|
err := os.RemoveAll(path)
|
||||||
|
if err != nil {
|
||||||
|
Log("Can't remove content at " + path)
|
||||||
|
} else {
|
||||||
|
Log("Removed " + path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanUnrealProject(project Project) {
|
||||||
|
dirs := []string {
|
||||||
|
"Binaries",
|
||||||
|
"DerivedDataCache",
|
||||||
|
"Intermediate",
|
||||||
|
"Saved",
|
||||||
|
"Script",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
fullPath := filepath.Join(project.ProjectPath, dir)
|
||||||
|
removeDir(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginsPath := filepath.Join(project.ProjectPath, "Plugins")
|
||||||
|
entries, err := os.ReadDir(pluginsPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginDir := filepath.Join(pluginsPath, entry.Name())
|
||||||
|
removeDir(filepath.Join(pluginDir, "Binaries"))
|
||||||
|
removeDir(filepath.Join(pluginDir, "Intermediate"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateUnrealSolution(project Project) {
|
||||||
|
var buildCmd string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
buildCmd = project.EnginePath + "/Engine/Build/BatchFiles/Linux/GenerateProjectFiles.sh";
|
||||||
|
case "darwin":
|
||||||
|
buildCmd = project.EnginePath + "/Engine/Build/BatchFiles/Mac/GenerateProjectFiles.sh"
|
||||||
|
default:
|
||||||
|
fmt.Println("Generate Project is not yet supported on Windows")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectCmd := project.ProjectPath + "/" + project.ProjectName + ".uproject"
|
||||||
|
cmd := exec.Command(buildCmd, projectCmd, "-game")
|
||||||
|
|
||||||
|
logToOutput(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildUnrealSolution(project Project) {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
cmd := exec.Command("make", "-C", project.ProjectPath, project.ProjectName)
|
||||||
|
logToOutput(cmd)
|
||||||
|
default:
|
||||||
|
fmt.Println("Build Project is not yet supported on Windows")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunUnrealProject(project Project) {
|
||||||
|
var buildCmd string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
buildCmd = project.EnginePath + "/Engine/Binaries/Linux/UnrealEditor"
|
||||||
|
case "darwin":
|
||||||
|
buildCmd = project.EnginePath + "/Engine/Binaries/Mac/UnrealEditor.app/Contents/MacOS/UnrealEditor"
|
||||||
|
default:
|
||||||
|
buildCmd = project.EnginePath + "/Engine/Binaries/Win64/UnrealEditor.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
projectCmd := project.ProjectPath + "/" + project.ProjectName + ".uproject"
|
||||||
|
cmd := exec.Command(buildCmd, projectCmd)
|
||||||
|
|
||||||
|
logToOutput(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logToOutput(cmd *exec.Cmd) {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error during pipe stdout creation")
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error during pipe stderr creation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
fmt.Println("Error while loading command")
|
||||||
|
}
|
||||||
|
|
||||||
|
readPipe := func(reader io.ReadCloser) {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
Log(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go readPipe(stdout)
|
||||||
|
go readPipe(stderr)
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
fmt.Println("Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user