Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c41ee27069 | |||
| e5b395ffa5 | |||
| b55f76c855 |
@@ -1 +1 @@
|
|||||||
version=0.1.3
|
version=0.1.4
|
||||||
|
|||||||
@@ -891,14 +891,53 @@ public class AudiobookRecorder extends JFrame {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
JMenuObject edit = new JMenuObject("Open in external editor", s, new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
try {
|
||||||
|
JMenuObject o = (JMenuObject)e.getSource();
|
||||||
|
Sentence s = (Sentence)o.getObject();
|
||||||
|
s.openInExternalEditor();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JMenu external = new JMenu("Run external processor");
|
||||||
|
|
||||||
|
for (int i = 0; i < 999; i++) {
|
||||||
|
String name = Options.get("editor.processor." + i + ".name");
|
||||||
|
if (name == null) break;
|
||||||
|
if (name.equals("")) break;
|
||||||
|
JMenuObject ob = new JMenuObject(name, s, new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
JMenuObject o = (JMenuObject)e.getSource();
|
||||||
|
Sentence s = (Sentence)o.getObject();
|
||||||
|
s.runExternalProcessor(Utils.s2i(o.getActionCommand()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ob.setActionCommand(Integer.toString(i));
|
||||||
|
external.add(ob);
|
||||||
|
}
|
||||||
|
|
||||||
|
JMenuObject undo = new JMenuObject("Undo", s, new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
JMenuObject o = (JMenuObject)e.getSource();
|
||||||
|
Sentence s = (Sentence)o.getObject();
|
||||||
|
s.undo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.add(undo);
|
||||||
|
menu.addSeparator();
|
||||||
menu.add(rec);
|
menu.add(rec);
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
menu.add(moveUp);
|
menu.add(moveUp);
|
||||||
menu.add(moveDown);
|
menu.add(moveDown);
|
||||||
menu.add(moveMenu);
|
menu.add(moveMenu);
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
menu.add(edit);
|
||||||
|
menu.add(external);
|
||||||
menu.add(ins);
|
menu.add(ins);
|
||||||
menu.add(del);
|
menu.add(del);
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
@@ -912,6 +951,17 @@ public class AudiobookRecorder extends JFrame {
|
|||||||
|
|
||||||
JPopupMenu menu = new JPopupMenu();
|
JPopupMenu menu = new JPopupMenu();
|
||||||
|
|
||||||
|
JMenuObject undo = new JMenuObject("Undo all", c, new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
JMenuObject o = (JMenuObject)e.getSource();
|
||||||
|
Chapter c = (Chapter)o.getObject();
|
||||||
|
for (Enumeration s = c.children(); s.hasMoreElements();) {
|
||||||
|
Sentence snt = (Sentence)s.nextElement();
|
||||||
|
snt.undo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
JMenuObject peak = new JMenuObject("Auto-trim all (Peak)", c, new ActionListener() {
|
JMenuObject peak = new JMenuObject("Auto-trim all (Peak)", c, new ActionListener() {
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
JMenuObject o = (JMenuObject)e.getSource();
|
JMenuObject o = (JMenuObject)e.getSource();
|
||||||
@@ -1083,6 +1133,29 @@ public class AudiobookRecorder extends JFrame {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
JMenu external = new JMenu("Run external processor");
|
||||||
|
|
||||||
|
for (int i = 0; i < 999; i++) {
|
||||||
|
String name = Options.get("editor.processor." + i + ".name");
|
||||||
|
if (name == null) break;
|
||||||
|
if (name.equals("")) break;
|
||||||
|
JMenuObject ob = new JMenuObject(name, c, new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
JMenuObject o = (JMenuObject)e.getSource();
|
||||||
|
Chapter c = (Chapter)o.getObject();
|
||||||
|
for (Enumeration s = c.children(); s.hasMoreElements();) {
|
||||||
|
Sentence snt = (Sentence)s.nextElement();
|
||||||
|
if (!snt.isLocked()) {
|
||||||
|
snt.runExternalProcessor(Utils.s2i(o.getActionCommand()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ob.setActionCommand(Integer.toString(i));
|
||||||
|
external.add(ob);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
menu.add(convertAll);
|
menu.add(convertAll);
|
||||||
menu.add(normalizeAll);
|
menu.add(normalizeAll);
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
@@ -1100,6 +1173,8 @@ public class AudiobookRecorder extends JFrame {
|
|||||||
menu.add(exportChapter);
|
menu.add(exportChapter);
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
menu.add(deleteChapter);
|
menu.add(deleteChapter);
|
||||||
|
menu.addSeparator();
|
||||||
|
menu.add(external);
|
||||||
|
|
||||||
menu.show(bookTree, e.getX(), e.getY());
|
menu.show(bookTree, e.getX(), e.getY());
|
||||||
} else if (node instanceof Book) {
|
} else if (node instanceof Book) {
|
||||||
@@ -1201,6 +1276,13 @@ public class AudiobookRecorder extends JFrame {
|
|||||||
|
|
||||||
if (s.isLocked()) return;
|
if (s.isLocked()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
s.backup();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (s.startRecording()) {
|
if (s.startRecording()) {
|
||||||
recording = (Sentence)selectedNode;
|
recording = (Sentence)selectedNode;
|
||||||
centralPanel.setFlash(true);
|
centralPanel.setFlash(true);
|
||||||
@@ -1545,9 +1627,9 @@ public class AudiobookRecorder extends JFrame {
|
|||||||
for (int e = 0; e < book.equaliser.length; e++) {
|
for (int e = 0; e < book.equaliser.length; e++) {
|
||||||
for (int i = 0; i < 31; i++) {
|
for (int i = 0; i < 31; i++) {
|
||||||
if (prefs.getProperty(String.format("audio.eq.profiles.%d.%d", e, i)) == null) {
|
if (prefs.getProperty(String.format("audio.eq.profiles.%d.%d", e, i)) == null) {
|
||||||
book.equaliser[e].setChannel(i, Options.getFloat(String.format("audio.eq.profiles.%d.%d", e, i)));
|
book.equaliser[e].setChannel(i, Options.getFloat("audio.eq." + i));
|
||||||
} else {
|
} else {
|
||||||
book.equaliser[e].setChannel(i, Utils.s2f(prefs.getProperty("audio.eq." + i)));
|
book.equaliser[e].setChannel(i, Utils.s2f(prefs.getProperty(String.format("audio.eq.profiles.%d.%d", e, i))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2434,4 +2516,10 @@ public class AudiobookRecorder extends JFrame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateWaveform() {
|
||||||
|
if (selectedSentence != null) {
|
||||||
|
sampleWaveform.setData(selectedSentence.getAudioData());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,14 @@ public class Options extends JDialog {
|
|||||||
|
|
||||||
JTextField havenApiKey;
|
JTextField havenApiKey;
|
||||||
|
|
||||||
|
JTextField externalEditor;
|
||||||
|
|
||||||
Equaliser equaliser;
|
Equaliser equaliser;
|
||||||
|
|
||||||
JTextArea startupScript;
|
JTextArea startupScript;
|
||||||
|
|
||||||
|
ArrayList<JTextField[]> processorList;
|
||||||
|
|
||||||
static HashMap<String, String> defaultPrefs;
|
static HashMap<String, String> defaultPrefs;
|
||||||
static Preferences prefs = null;
|
static Preferences prefs = null;
|
||||||
|
|
||||||
@@ -82,6 +86,24 @@ public class Options extends JDialog {
|
|||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addTwoLabel(JPanel panel, String label1, String label2) {
|
||||||
|
JLabel l1 = new JLabel(label1);
|
||||||
|
constraint.gridx = 0;
|
||||||
|
constraint.gridwidth = 1;
|
||||||
|
constraint.gridheight = 1;
|
||||||
|
constraint.anchor = GridBagConstraints.LINE_START;
|
||||||
|
panel.add(l1, constraint);
|
||||||
|
|
||||||
|
JLabel l2 = new JLabel(label2);
|
||||||
|
constraint.gridx = 1;
|
||||||
|
constraint.gridwidth = 1;
|
||||||
|
constraint.gridheight = 1;
|
||||||
|
constraint.anchor = GridBagConstraints.LINE_START;
|
||||||
|
panel.add(l2, constraint);
|
||||||
|
|
||||||
|
constraint.gridy++;
|
||||||
|
}
|
||||||
|
|
||||||
JTextField addTextField(JPanel panel, String label, String def) {
|
JTextField addTextField(JPanel panel, String label, String def) {
|
||||||
JLabel l = new JLabel(label);
|
JLabel l = new JLabel(label);
|
||||||
constraint.gridx = 0;
|
constraint.gridx = 0;
|
||||||
@@ -102,6 +124,22 @@ public class Options extends JDialog {
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JTextField[] addTwoField(JPanel panel, String def1, String def2) {
|
||||||
|
JTextField a = new JTextField(def1);
|
||||||
|
constraint.gridx = 0;
|
||||||
|
constraint.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
panel.add(a, constraint);
|
||||||
|
|
||||||
|
JTextField b = new JTextField(def2);
|
||||||
|
constraint.gridx = 1;
|
||||||
|
panel.add(b, constraint);
|
||||||
|
constraint.fill = GridBagConstraints.NONE;
|
||||||
|
|
||||||
|
constraint.gridy++;
|
||||||
|
return new JTextField[] { a, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
JTextField addFilePath(JPanel panel, String label, String path, boolean dironly) {
|
JTextField addFilePath(JPanel panel, String label, String path, boolean dironly) {
|
||||||
JLabel l = new JLabel(label);
|
JLabel l = new JLabel(label);
|
||||||
@@ -286,6 +324,10 @@ public class Options extends JDialog {
|
|||||||
|
|
||||||
addSeparator(optionsPanel);
|
addSeparator(optionsPanel);
|
||||||
|
|
||||||
|
externalEditor = addTextField(optionsPanel, "External Editor Command", get("editor.external"));
|
||||||
|
|
||||||
|
addSeparator(optionsPanel);
|
||||||
|
|
||||||
cacheSize = addSpinner(optionsPanel, "Cache size:", 0, 5000, 1, getInteger("cache.size"), "");
|
cacheSize = addSpinner(optionsPanel, "Cache size:", 0, 5000, 1, getInteger("cache.size"), "");
|
||||||
|
|
||||||
addSeparator(optionsPanel);
|
addSeparator(optionsPanel);
|
||||||
@@ -318,6 +360,39 @@ public class Options extends JDialog {
|
|||||||
tabs.add("Effects", effects);
|
tabs.add("Effects", effects);
|
||||||
|
|
||||||
|
|
||||||
|
JPanel processors = new JPanel();
|
||||||
|
processors.setLayout(new BorderLayout());
|
||||||
|
JPanel processorListPanel = new JPanel();
|
||||||
|
|
||||||
|
JScrollPane psp = new JScrollPane(processorListPanel);
|
||||||
|
|
||||||
|
processors.add(psp, BorderLayout.CENTER);
|
||||||
|
processorListPanel.setLayout(new GridBagLayout());
|
||||||
|
|
||||||
|
constraint.gridx = 0;
|
||||||
|
constraint.gridy = 0;
|
||||||
|
constraint.gridwidth = 1;
|
||||||
|
constraint.gridheight = 1;
|
||||||
|
|
||||||
|
addTwoLabel(processorListPanel, "Name", "Command");
|
||||||
|
|
||||||
|
processorList = new ArrayList<JTextField[]>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 999; i++) {
|
||||||
|
String name = get("editor.processor." + i + ".name");
|
||||||
|
String command = get("editor.processor." + i + ".command");
|
||||||
|
if (name == null || command == null) break;
|
||||||
|
if (name.equals("") || command.equals("")) break;
|
||||||
|
JTextField[] f = addTwoField(processorListPanel, name, command);
|
||||||
|
processorList.add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
JTextField[] f = addTwoField(processorListPanel, "", "");
|
||||||
|
processorList.add(f);
|
||||||
|
|
||||||
|
tabs.add("Processors", processors);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -491,6 +566,8 @@ public class Options extends JDialog {
|
|||||||
defaultPrefs.put("process.sphinx", "false");
|
defaultPrefs.put("process.sphinx", "false");
|
||||||
defaultPrefs.put("process.haven.apikey", "");
|
defaultPrefs.put("process.haven.apikey", "");
|
||||||
|
|
||||||
|
defaultPrefs.put("editor.external", "");
|
||||||
|
|
||||||
defaultPrefs.put("cache.size", "100");
|
defaultPrefs.put("cache.size", "100");
|
||||||
|
|
||||||
defaultPrefs.put("audio.eq.0", "0.00");
|
defaultPrefs.put("audio.eq.0", "0.00");
|
||||||
@@ -635,6 +712,7 @@ public class Options extends JDialog {
|
|||||||
set("audio.export.samplerate", ((KVPair)exportRate.getSelectedItem()).key);
|
set("audio.export.samplerate", ((KVPair)exportRate.getSelectedItem()).key);
|
||||||
set("process.sphinx", enableParsing.isSelected());
|
set("process.sphinx", enableParsing.isSelected());
|
||||||
set("process.haven.apikey", havenApiKey.getText());
|
set("process.haven.apikey", havenApiKey.getText());
|
||||||
|
set("editor.external", externalEditor.getText());
|
||||||
set("cache.size", cacheSize.getValue());
|
set("cache.size", cacheSize.getValue());
|
||||||
|
|
||||||
set("effects.ethereal.offset", etherealOffset.getValue());
|
set("effects.ethereal.offset", etherealOffset.getValue());
|
||||||
@@ -647,6 +725,18 @@ public class Options extends JDialog {
|
|||||||
|
|
||||||
set("scripts.startup", startupScript.getText());
|
set("scripts.startup", startupScript.getText());
|
||||||
|
|
||||||
|
int procNo = 0;
|
||||||
|
for (JTextField[] proc : processorList) {
|
||||||
|
String name = proc[0].getText();
|
||||||
|
String command = proc[1].getText();
|
||||||
|
if (name.equals("") || command.equals("")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
set("editor.processor." + procNo + ".name", name);
|
||||||
|
set("editor.processor." + procNo + ".command", command);
|
||||||
|
procNo++;
|
||||||
|
}
|
||||||
|
|
||||||
savePreferences();
|
savePreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public class ProgressDialog extends JDialog implements EncoderProgressListener {
|
|||||||
pack();
|
pack();
|
||||||
|
|
||||||
setSize(new Dimension(300, 100));
|
setSize(new Dimension(300, 100));
|
||||||
|
setResizable(false);
|
||||||
|
|
||||||
// setVisible(true);
|
// setVisible(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1053,4 +1053,170 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
|
|||||||
eqProfile = e;
|
eqProfile = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ExternalEditor implements Runnable {
|
||||||
|
Sentence sentence;
|
||||||
|
ExternalEditor(Sentence s) {
|
||||||
|
sentence = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
String command = Options.get("editor.external");
|
||||||
|
if (command == null) return;
|
||||||
|
if (command.equals("")) return;
|
||||||
|
|
||||||
|
String[] parts = command.split("::");
|
||||||
|
|
||||||
|
ArrayList<String> args = new ArrayList<String>();
|
||||||
|
|
||||||
|
for (String part : parts) {
|
||||||
|
if (part.equals("%f")) {
|
||||||
|
args.add(getFile().getAbsolutePath());
|
||||||
|
} else {
|
||||||
|
args.add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ProcessBuilder process = new ProcessBuilder(args);
|
||||||
|
Process proc = process.start();
|
||||||
|
proc.waitFor();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
clearCache();
|
||||||
|
AudiobookRecorder.window.updateWaveform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openInExternalEditor() {
|
||||||
|
ExternalEditor ed = new ExternalEditor(this);
|
||||||
|
Thread t = new Thread(ed);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void backup() throws IOException {
|
||||||
|
File whereto = getFile().getParentFile();
|
||||||
|
String name = getFile().getName();
|
||||||
|
|
||||||
|
int backupNumber = -1;
|
||||||
|
for (int i = 1; i < 999999; i++) {
|
||||||
|
|
||||||
|
String fn = String.format("backup-%08d-%s", i, name);
|
||||||
|
File testFile = new File(whereto, fn);
|
||||||
|
if (!testFile.exists()) {
|
||||||
|
backupNumber = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (backupNumber == -1) {
|
||||||
|
System.err.println("Out of backup space!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String fn = String.format("backup-%08d-%s", backupNumber, getFile().getName());
|
||||||
|
File bak = new File(getFile().getParentFile(), fn);
|
||||||
|
Files.copy(getFile().toPath(), bak.toPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExternalProcessor implements Runnable {
|
||||||
|
Sentence sentence;
|
||||||
|
int number;
|
||||||
|
|
||||||
|
public ExternalProcessor(Sentence s, int num) {
|
||||||
|
sentence = s;
|
||||||
|
number = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
String command = Options.get("editor.processor." + number + ".command");
|
||||||
|
if (command == null) return;
|
||||||
|
if (command.equals("")) return;
|
||||||
|
|
||||||
|
String[] parts = command.split("::");
|
||||||
|
|
||||||
|
ArrayList<String> args = new ArrayList<String>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
backup();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File out = new File(getFile().getParentFile(), "proc-" + getFile().getName());
|
||||||
|
|
||||||
|
for (String part : parts) {
|
||||||
|
if (part.equals("%f")) {
|
||||||
|
args.add(getFile().getAbsolutePath());
|
||||||
|
} else if (part.equals("%o")) {
|
||||||
|
args.add(out.getAbsolutePath());
|
||||||
|
} else {
|
||||||
|
args.add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ProcessBuilder process = new ProcessBuilder(args);
|
||||||
|
Process proc = process.start();
|
||||||
|
proc.waitFor();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out.exists()) {
|
||||||
|
try {
|
||||||
|
File in = getFile();
|
||||||
|
in.delete();
|
||||||
|
Files.copy(out.toPath(), in.toPath());
|
||||||
|
out.delete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache();
|
||||||
|
AudiobookRecorder.window.updateWaveform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void runExternalProcessor(int num) {
|
||||||
|
if (isLocked()) return;
|
||||||
|
ExternalProcessor ed = new ExternalProcessor(this, num);
|
||||||
|
Thread t = new Thread(ed);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void undo() {
|
||||||
|
File whereto = getFile().getParentFile();
|
||||||
|
String name = getFile().getName();
|
||||||
|
|
||||||
|
int backupNumber = -1;
|
||||||
|
for (int i = 1; i < 999999; i++) {
|
||||||
|
String fn = String.format("backup-%08d-%s", i, name);
|
||||||
|
File testFile = new File(whereto, fn);
|
||||||
|
if (testFile.exists()) {
|
||||||
|
backupNumber = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupNumber == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String fn = String.format("backup-%08d-%s", backupNumber, getFile().getName());
|
||||||
|
File bak = new File(getFile().getParentFile(), fn);
|
||||||
|
|
||||||
|
try {
|
||||||
|
File in = getFile();
|
||||||
|
in.delete();
|
||||||
|
Files.copy(bak.toPath(), in.toPath());
|
||||||
|
bak.delete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache();
|
||||||
|
AudiobookRecorder.window.updateWaveform();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user