- unify extraction of resources for project extensions (e.g. desktop deployment icons)
- add icon to exe for windows desktop deployment by default git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@8748 75d07b2b-3a1a-0410-a2c5-0572b91ccdca
This commit is contained in:
parent
08cb3182bd
commit
b5dccedb0b
@ -32,14 +32,21 @@
|
||||
package com.jme3.gde.core.j2seproject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.LineNumberReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.netbeans.api.project.Project;
|
||||
import org.netbeans.api.project.ProjectManager;
|
||||
import org.netbeans.api.project.ant.AntBuildExtender;
|
||||
@ -50,6 +57,7 @@ import org.netbeans.modules.java.j2seproject.api.J2SEProjectConfigurations;
|
||||
import org.netbeans.spi.project.support.ant.EditableProperties;
|
||||
import org.openide.filesystems.FileLock;
|
||||
import org.openide.filesystems.FileObject;
|
||||
import org.openide.filesystems.FileUtil;
|
||||
import org.openide.util.Exceptions;
|
||||
|
||||
/**
|
||||
@ -63,6 +71,7 @@ public class ProjectExtensionManager {
|
||||
private String extensionTargets;
|
||||
private String[] extensionDependencies;
|
||||
private String antTaskLibrary;
|
||||
private URL zipFile;
|
||||
|
||||
/**
|
||||
* Allows extending ant based projects
|
||||
@ -140,62 +149,17 @@ public class ProjectExtensionManager {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
addAntTaskLibrary(proj, antTaskLibrary);
|
||||
try {
|
||||
addZipContents(proj.getProjectDirectory());
|
||||
} catch (IOException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.WARNING, "Trying to include assets build snippet in project type that doesn't support AntBuildExtender API contract.");
|
||||
}
|
||||
}
|
||||
|
||||
private FileObject getImplFile(FileObject projDir, boolean create) {
|
||||
FileObject assetsImpl = projDir.getFileObject("nbproject/" + extensionName + "-impl.xml");
|
||||
if (assetsImpl == null) {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "No {0}-impl.xml found", extensionName);
|
||||
if (create) {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Creating {0}-impl.xml", extensionName);
|
||||
assetsImpl = createImplFile(projDir);
|
||||
}
|
||||
} else {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Found {0}-impl.xml", extensionName);
|
||||
try {
|
||||
if (create && !assetsImpl.asLines().get(1).startsWith("<!--" + extensionName + "-impl.xml " + extensionVersion + "-->")) {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Updating {0}-impl.xml", extensionName);
|
||||
assetsImpl.delete();
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Deleted {0}-impl.xml", extensionName);
|
||||
assetsImpl = createImplFile(projDir);
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Recreated {0}-impl.xml", extensionName);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
}
|
||||
return assetsImpl;
|
||||
}
|
||||
|
||||
private FileObject createImplFile(FileObject projDir) {
|
||||
FileLock lock = null;
|
||||
FileObject file = null;
|
||||
try {
|
||||
file = projDir.getFileObject("nbproject").createData(extensionName + "-impl.xml");
|
||||
lock = file.lock();
|
||||
OutputStreamWriter out = new OutputStreamWriter(file.getOutputStream(lock));
|
||||
out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||
out.write("<!--" + extensionName + "-impl.xml " + extensionVersion + "-->\n");
|
||||
out.write("<project name=\"" + extensionName + "-impl\" basedir=\"..\">\n");
|
||||
if (extensionTargets != null) {
|
||||
out.write(extensionTargets);
|
||||
}
|
||||
out.write("</project>\n");
|
||||
out.close();
|
||||
} catch (IOException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
} finally {
|
||||
if (lock != null) {
|
||||
lock.releaseLock();
|
||||
}
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the extension including all files and libraries
|
||||
* @param proj
|
||||
@ -229,43 +193,23 @@ public class ProjectExtensionManager {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
removeAntTaskLibrary(proj, antTaskLibrary);
|
||||
try {
|
||||
removeZipContents(proj.getProjectDirectory());
|
||||
} catch (IOException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.WARNING, "Trying to include assets build snippet in project type that doesn't support AntBuildExtender API contract.");
|
||||
}
|
||||
}
|
||||
|
||||
private void addAntTaskLibrary(Project proj, String name) {
|
||||
if (name == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AntBuildExtender extender = proj.getLookup().lookup(AntBuildExtender.class);
|
||||
if (extender != null) {
|
||||
LibraryManager.getDefault();
|
||||
extender.addLibrary(LibraryManager.getDefault().getLibrary(name));
|
||||
ProjectManager.getDefault().saveProject(proj);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Exceptions.printStackTrace(e);
|
||||
}
|
||||
public void setExtensionDependencies(String[] extensionDependencies) {
|
||||
this.extensionDependencies = extensionDependencies;
|
||||
}
|
||||
|
||||
private void removeAntTaskLibrary(Project proj, String name) {
|
||||
if (name == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AntBuildExtender extender = proj.getLookup().lookup(AntBuildExtender.class);
|
||||
if (extender != null) {
|
||||
LibraryManager.getDefault();
|
||||
extender.removeLibrary(LibraryManager.getDefault().getLibrary(name));
|
||||
ProjectManager.getDefault().saveProject(proj);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
public void setExtensionTargets(String extensionTargets) {
|
||||
this.extensionTargets = extensionTargets;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -277,6 +221,18 @@ public class ProjectExtensionManager {
|
||||
this.antTaskLibrary = antTaskLibrary;
|
||||
}
|
||||
|
||||
public void setDataZip(String url) {
|
||||
try {
|
||||
this.zipFile = new URL(url);
|
||||
} catch (MalformedURLException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDataZipUrl(URL url) {
|
||||
this.zipFile = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads ant targets from a file in the classpath
|
||||
* @param path
|
||||
@ -380,11 +336,146 @@ public class ProjectExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void setExtensionDependencies(String[] extensionDependencies) {
|
||||
this.extensionDependencies = extensionDependencies;
|
||||
private FileObject getImplFile(FileObject projDir, boolean create) {
|
||||
FileObject assetsImpl = projDir.getFileObject("nbproject/" + extensionName + "-impl.xml");
|
||||
if (assetsImpl == null) {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "No extension file {0}-impl.xml found", extensionName);
|
||||
if (create) {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Creating extension file {0}-impl.xml", extensionName);
|
||||
assetsImpl = createImplFile(projDir);
|
||||
}
|
||||
} else {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Found extension file {0}-impl.xml", extensionName);
|
||||
try {
|
||||
if (create && !assetsImpl.asLines().get(1).startsWith("<!--" + extensionName + "-impl.xml " + extensionVersion + "-->")) {
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Updating extension file {0}-impl.xml", extensionName);
|
||||
assetsImpl.delete();
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Deleted extension file {0}-impl.xml", extensionName);
|
||||
assetsImpl = createImplFile(projDir);
|
||||
Logger.getLogger(ProjectExtensionManager.class.getName()).log(Level.INFO, "Recreated extension file {0}-impl.xml", extensionName);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
}
|
||||
return assetsImpl;
|
||||
}
|
||||
|
||||
public void setExtensionTargets(String extensionTargets) {
|
||||
this.extensionTargets = extensionTargets;
|
||||
private FileObject createImplFile(FileObject projDir) {
|
||||
FileLock lock = null;
|
||||
FileObject file = null;
|
||||
try {
|
||||
file = projDir.getFileObject("nbproject").createData(extensionName + "-impl.xml");
|
||||
lock = file.lock();
|
||||
OutputStreamWriter out = new OutputStreamWriter(file.getOutputStream(lock));
|
||||
out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||
out.write("<!--" + extensionName + "-impl.xml " + extensionVersion + "-->\n");
|
||||
out.write("<project name=\"" + extensionName + "-impl\" basedir=\"..\">\n");
|
||||
if (extensionTargets != null) {
|
||||
out.write(extensionTargets);
|
||||
}
|
||||
out.write("</project>\n");
|
||||
out.close();
|
||||
} catch (IOException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
} finally {
|
||||
if (lock != null) {
|
||||
lock.releaseLock();
|
||||
}
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private void addAntTaskLibrary(Project proj, String name) {
|
||||
if (name == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AntBuildExtender extender = proj.getLookup().lookup(AntBuildExtender.class);
|
||||
if (extender != null) {
|
||||
LibraryManager.getDefault();
|
||||
extender.addLibrary(LibraryManager.getDefault().getLibrary(name));
|
||||
ProjectManager.getDefault().saveProject(proj);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Exceptions.printStackTrace(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeAntTaskLibrary(Project proj, String name) {
|
||||
if (name == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AntBuildExtender extender = proj.getLookup().lookup(AntBuildExtender.class);
|
||||
if (extender != null) {
|
||||
LibraryManager.getDefault();
|
||||
extender.removeLibrary(LibraryManager.getDefault().getLibrary(name));
|
||||
ProjectManager.getDefault().saveProject(proj);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void addZipContents(FileObject projectRoot) throws IOException {
|
||||
if (zipFile == null) {
|
||||
return;
|
||||
}
|
||||
InputStream in = zipFile.openStream();
|
||||
try {
|
||||
ZipInputStream str = new ZipInputStream(in);
|
||||
ZipEntry entry;
|
||||
while ((entry = str.getNextEntry()) != null) {
|
||||
if (entry.isDirectory()) {
|
||||
FileObject fo = projectRoot.getFileObject(entry.getName());
|
||||
if (fo == null) {
|
||||
FileUtil.createFolder(projectRoot, entry.getName());
|
||||
}
|
||||
} else {
|
||||
FileObject fo = projectRoot.getFileObject(entry.getName());
|
||||
if (fo != null && !fo.equals(projectRoot)) {
|
||||
fo.delete();
|
||||
}
|
||||
fo = FileUtil.createData(projectRoot, entry.getName());
|
||||
writeFile(str, fo);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeZipContents(FileObject projectRoot) throws IOException {
|
||||
if (zipFile == null) {
|
||||
return;
|
||||
}
|
||||
InputStream in = zipFile.openStream();
|
||||
try {
|
||||
ZipInputStream str = new ZipInputStream(in);
|
||||
ZipEntry entry;
|
||||
while ((entry = str.getNextEntry()) != null) {
|
||||
FileObject obj = projectRoot.getFileObject(entry.getName());
|
||||
if (obj != null && !obj.equals(projectRoot)) {
|
||||
Logger.getLogger(this.getClass().getName()).log(Level.INFO, "Deleting file " + obj.getNameExt());
|
||||
if (entry.getSize() != -1 && entry.getSize() == obj.getSize()) {
|
||||
obj.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeFile(ZipInputStream str, FileObject fo) throws IOException {
|
||||
OutputStream out = fo.getOutputStream();
|
||||
try {
|
||||
Logger.getLogger(this.getClass().getName()).log(Level.INFO, "Creating file " + fo.getNameExt());
|
||||
FileUtil.copy(str, out);
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,14 @@
|
||||
<project name="com.jme3.gde.desktop.executables" default="netbeans" basedir=".">
|
||||
<description>Builds, tests, and runs the project com.jme3.gde.desktop.executables.</description>
|
||||
<import file="nbproject/build-impl.xml"/>
|
||||
<target name="zip-osxdata" description="Zips mac stubs">
|
||||
<target name="init" depends="-zip-data,basic-init,files-init,build-init,-javac-init"/>
|
||||
<target name="-zip-data" description="Zips mac stubs">
|
||||
<zip destfile="src/com/jme3/gde/desktop/executables/macapp-data.zip">
|
||||
<zipfileset dir="macapp-data" prefix=""/>
|
||||
</zip>
|
||||
<zip destfile="src/com/jme3/gde/desktop/executables/winapp-data.zip">
|
||||
<zipfileset dir="winapp-data" prefix=""/>
|
||||
</zip>
|
||||
</target>
|
||||
|
||||
</project>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
|
||||
<plist version="0.9">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>${application.title}</string>
|
||||
|
Binary file not shown.
@ -9,18 +9,10 @@ import com.jme3.gde.core.j2seproject.ProjectExtensionProperties;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import javax.swing.JComponent;
|
||||
|
||||
import org.netbeans.api.project.Project;
|
||||
import org.netbeans.spi.project.ui.support.ProjectCustomizer;
|
||||
import org.openide.filesystems.FileObject;
|
||||
import org.openide.filesystems.FileUtil;
|
||||
|
||||
import org.openide.util.Exceptions;
|
||||
import org.openide.util.Lookup;
|
||||
@ -70,9 +62,11 @@ public class DesktopExeCompositeProvider implements ProjectCustomizer.CompositeC
|
||||
public SavePropsListener(ProjectExtensionProperties props, Project project) {
|
||||
this.properties = props;
|
||||
this.project = project;
|
||||
launch4j = new ProjectExtensionManager("launch4j", "v1.1", new String[]{"jar", "-launch4j-exe"});
|
||||
launch4j = new ProjectExtensionManager("launch4j", "v1.2", new String[]{"jar", "-launch4j-exe"});
|
||||
launch4j.setAntTaskLibrary("launch4j");
|
||||
launch4j.setDataZip("nbres:/com/jme3/gde/desktop/executables/winapp-data.zip");
|
||||
macapp = new ProjectExtensionManager("macapp", "v1.1", new String[]{"jar", "-mac-app"});
|
||||
macapp.setDataZip("nbres:/com/jme3/gde/desktop/executables/macapp-data.zip");
|
||||
linux = new ProjectExtensionManager("linuxlauncher", "v1.1", new String[]{"jar", "-linux-launcher"});
|
||||
}
|
||||
|
||||
@ -92,13 +86,6 @@ public class DesktopExeCompositeProvider implements ProjectCustomizer.CompositeC
|
||||
if ("true".equals(properties.getProperty("mac.app.enabled"))) {
|
||||
macapp.loadTargets("nbres:/com/jme3/gde/desktop/executables/macapp-targets.xml");
|
||||
macapp.checkExtension(project);
|
||||
if (project.getProjectDirectory().getFileObject("osx-stub") == null) {
|
||||
try {
|
||||
unZipFile(new URL("nbres:/com/jme3/gde/desktop/executables/macapp-data.zip").openStream(), project.getProjectDirectory());
|
||||
} catch (Exception ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
macapp.removeExtension(project);
|
||||
}
|
||||
@ -110,30 +97,5 @@ public class DesktopExeCompositeProvider implements ProjectCustomizer.CompositeC
|
||||
}
|
||||
}
|
||||
|
||||
private void unZipFile(InputStream source, FileObject projectRoot) throws IOException {
|
||||
try {
|
||||
ZipInputStream str = new ZipInputStream(source);
|
||||
ZipEntry entry;
|
||||
while ((entry = str.getNextEntry()) != null) {
|
||||
if (entry.isDirectory()) {
|
||||
FileUtil.createFolder(projectRoot, entry.getName());
|
||||
} else {
|
||||
FileObject fo = FileUtil.createData(projectRoot, entry.getName());
|
||||
writeFile(str, fo);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
source.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeFile(ZipInputStream str, FileObject fo) throws IOException {
|
||||
OutputStream out = fo.getOutputStream();
|
||||
try {
|
||||
FileUtil.copy(str, out);
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<echo>Windows EXE Creation</echo>
|
||||
<taskdef classname="net.sf.launch4j.ant.Launch4jTask" classpath="${libs.launch4j.classpath}" name="launch4j"/>
|
||||
<launch4j>
|
||||
<config jar="${dist.jar}" outfile="${dist.dir}/${application.title}.exe" errTitle="${application.title}" headertype="gui" chdir="." customProcName="true">
|
||||
<config jar="${dist.jar}" outfile="${dist.dir}/${application.title}.exe" errTitle="${application.title}" icon="win-icon.ico" headertype="gui" chdir="." customProcName="true">
|
||||
<singleInstance mutexName="${main.class}"/>
|
||||
<jre minVersion="1.5.0" maxheapsize="512"/>
|
||||
</config>
|
||||
|
Binary file not shown.
Binary file not shown.
BIN
sdk/jme3-desktop-executables/winapp-data/win-icon.ico
Normal file
BIN
sdk/jme3-desktop-executables/winapp-data/win-icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
@ -11,6 +11,7 @@ import java.awt.event.ActionListener;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
@ -71,19 +72,13 @@ public class LwjglAppletCompositeProvider implements ProjectCustomizer.Composite
|
||||
this.properties = props;
|
||||
this.project = project;
|
||||
manager.setAntTaskLibrary("lwjgl-applet");
|
||||
manager.setDataZip("nbres:/com/jme3/gde/lwjgl/applet/applet-data.zip");
|
||||
}
|
||||
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if ("true".equals(properties.getProperty("lwjgl.applet.enabled"))) {
|
||||
manager.loadTargets("nbres:/com/jme3/gde/lwjgl/applet/lwjgl-applet-targets.xml");
|
||||
manager.checkExtension(project);
|
||||
if (project.getProjectDirectory().getFileObject("appletlogo.png") == null) {
|
||||
try {
|
||||
unZipFile(new URL("nbres:/com/jme3/gde/lwjgl/applet/applet-data.zip").openStream(), project.getProjectDirectory());
|
||||
} catch (Exception ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
manager.removeExtension(project);
|
||||
}
|
||||
@ -94,30 +89,5 @@ public class LwjglAppletCompositeProvider implements ProjectCustomizer.Composite
|
||||
}
|
||||
}
|
||||
|
||||
private void unZipFile(InputStream source, FileObject projectRoot) throws IOException {
|
||||
try {
|
||||
ZipInputStream str = new ZipInputStream(source);
|
||||
ZipEntry entry;
|
||||
while ((entry = str.getNextEntry()) != null) {
|
||||
if (entry.isDirectory()) {
|
||||
FileUtil.createFolder(projectRoot, entry.getName());
|
||||
} else {
|
||||
FileObject fo = FileUtil.createData(projectRoot, entry.getName());
|
||||
writeFile(str, fo);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
source.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeFile(ZipInputStream str, FileObject fo) throws IOException {
|
||||
OutputStream out = fo.getOutputStream();
|
||||
try {
|
||||
FileUtil.copy(str, out);
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user