mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
implement custom prompt
This commit is contained in:
18
menu.js
18
menu.js
@ -6,7 +6,7 @@ const is = require("electron-is");
|
|||||||
|
|
||||||
const { getAllPlugins } = require("./plugins/utils");
|
const { getAllPlugins } = require("./plugins/utils");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const prompt = require('electron-prompt');
|
const prompt = require('./prompt');
|
||||||
|
|
||||||
const pluginEnabledMenu = (win, plugin, label = "", hasSubmenu = false) => ({
|
const pluginEnabledMenu = (win, plugin, label = "", hasSubmenu = false) => ({
|
||||||
label: label || plugin,
|
label: label || plugin,
|
||||||
@ -312,7 +312,7 @@ module.exports.setApplicationMenu = (win) => {
|
|||||||
const iconPath = path.join(__dirname, "assets", "youtube-music-tray.png");
|
const iconPath = path.join(__dirname, "assets", "youtube-music-tray.png");
|
||||||
const example = `Example: "socks5://127.0.0.1:9999"`;
|
const example = `Example: "socks5://127.0.0.1:9999"`;
|
||||||
function setProxy(item) {
|
function setProxy(item) {
|
||||||
prompt({
|
let options = {
|
||||||
title: 'Set Proxy',
|
title: 'Set Proxy',
|
||||||
label: 'Enter Proxy Address (leave empty to disable)',
|
label: 'Enter Proxy Address (leave empty to disable)',
|
||||||
value: config.get("options.proxy") || example,
|
value: config.get("options.proxy") || example,
|
||||||
@ -322,8 +322,18 @@ function setProxy(item) {
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
icon: iconPath,
|
icon: iconPath,
|
||||||
customStylesheet: path.join(__dirname, "darkPrompt.css"),
|
customStylesheet: path.join(__dirname, "prompt","darkPrompt.css"),
|
||||||
})
|
};
|
||||||
|
if (config.plugins.isEnabled("in-app-menu")) {
|
||||||
|
Object.assign(options, {
|
||||||
|
frame: false,
|
||||||
|
customScript: path.join(__dirname, "prompt","customTitlebar.js"),
|
||||||
|
enableRemoteModule: true,
|
||||||
|
height: 200,
|
||||||
|
width: 450,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
prompt(options)
|
||||||
.then((input) => {
|
.then((input) => {
|
||||||
if(input !== null && input !== example) {
|
if(input !== null && input !== example) {
|
||||||
config.set("options.proxy", input);
|
config.set("options.proxy", input);
|
||||||
|
|||||||
@ -70,11 +70,11 @@
|
|||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"custom-electron-titlebar": "^3.2.6",
|
"custom-electron-titlebar": "^3.2.6",
|
||||||
"discord-rpc": "^3.2.0",
|
"discord-rpc": "^3.2.0",
|
||||||
|
"doc-ready": "^1.0.4",
|
||||||
"downloads-folder": "^3.0.1",
|
"downloads-folder": "^3.0.1",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
"electron-is": "^3.0.0",
|
"electron-is": "^3.0.0",
|
||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-prompt": "^1.6.2",
|
|
||||||
"electron-store": "^7.0.2",
|
"electron-store": "^7.0.2",
|
||||||
"electron-unhandled": "^3.0.2",
|
"electron-unhandled": "^3.0.2",
|
||||||
"electron-updater": "^4.3.6",
|
"electron-updater": "^4.3.6",
|
||||||
|
|||||||
12
prompt/customTitlebar.js
Normal file
12
prompt/customTitlebar.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const customTitlebar = require("custom-electron-titlebar");
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
const bar = new customTitlebar.Titlebar({
|
||||||
|
backgroundColor: customTitlebar.Color.fromHex("#050505"),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
bar.updateMenu(null);
|
||||||
|
} catch (e) {
|
||||||
|
//will always throw type error - null isn't menu, but it works
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,16 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color:whitesmoke;
|
color:whitesmoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0 !important;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#label {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
#container {
|
#container {
|
||||||
background: rgba( 0, 0, 0, 0.7 );
|
background: rgba( 0, 0, 0, 0.7 );
|
||||||
box-shadow: 0 8px 32px 0 rgba( 31, 38, 135, 0.37 );
|
box-shadow: 0 8px 32px 0 rgba( 31, 38, 135, 0.37 );
|
||||||
@ -11,7 +21,6 @@ body {
|
|||||||
-webkit-backdrop-filter: blur( 10.0px );
|
-webkit-backdrop-filter: blur( 10.0px );
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid rgba(80, 0, 0, 0.4);
|
border: 1px solid rgba(80, 0, 0, 0.4);
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
127
prompt/index.js
Normal file
127
prompt/index.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
const electron = require('electron');
|
||||||
|
|
||||||
|
const BrowserWindow = electron.BrowserWindow || electron.remote.BrowserWindow;
|
||||||
|
const ipcMain = electron.ipcMain || electron.remote.ipcMain;
|
||||||
|
const url = require('url');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const DEFAULT_WIDTH = 370;
|
||||||
|
const DEFAULT_HEIGHT = 160;
|
||||||
|
|
||||||
|
function electronPrompt(options, parentWindow) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = `${Date.now()}-${Math.random()}`;
|
||||||
|
|
||||||
|
const options_ = Object.assign(
|
||||||
|
{
|
||||||
|
width: DEFAULT_WIDTH,
|
||||||
|
height: DEFAULT_HEIGHT,
|
||||||
|
minWidth: DEFAULT_WIDTH,
|
||||||
|
minHeight: DEFAULT_HEIGHT,
|
||||||
|
resizable: false,
|
||||||
|
title: 'Prompt',
|
||||||
|
label: 'Please input a value:',
|
||||||
|
buttonLabels: null,
|
||||||
|
alwaysOnTop: false,
|
||||||
|
value: null,
|
||||||
|
type: 'input',
|
||||||
|
selectOptions: null,
|
||||||
|
icon: null,
|
||||||
|
useHtmlLabel: false,
|
||||||
|
customStylesheet: null,
|
||||||
|
menuBarVisible: false,
|
||||||
|
skipTaskbar: true,
|
||||||
|
frame: true,
|
||||||
|
customScript: null,
|
||||||
|
enableRemoteModule: false
|
||||||
|
},
|
||||||
|
options || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options_.type === 'select' && (options_.selectOptions === null || typeof options_.selectOptions !== 'object')) {
|
||||||
|
reject(new Error('"selectOptions" must be an object'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let promptWindow = new BrowserWindow({
|
||||||
|
frame: options_.frame,
|
||||||
|
width: options_.width,
|
||||||
|
height: options_.height,
|
||||||
|
minWidth: options_.minWidth,
|
||||||
|
minHeight: options_.minHeight,
|
||||||
|
resizable: options_.resizable,
|
||||||
|
minimizable: false,
|
||||||
|
fullscreenable: false,
|
||||||
|
maximizable: false,
|
||||||
|
parent: parentWindow,
|
||||||
|
skipTaskbar: options_.skipTaskbar,
|
||||||
|
alwaysOnTop: options_.alwaysOnTop,
|
||||||
|
useContentSize: options_.resizable,
|
||||||
|
modal: Boolean(parentWindow),
|
||||||
|
title: options_.title,
|
||||||
|
icon: options_.icon || undefined,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
enableRemoteModule: options_.enableRemoteModule
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
promptWindow.setMenu(null);
|
||||||
|
promptWindow.setMenuBarVisibility(options_.menuBarVisible);
|
||||||
|
|
||||||
|
const getOptionsListener = event => {
|
||||||
|
event.returnValue = JSON.stringify(options_);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
ipcMain.removeListener('prompt-get-options:' + id, getOptionsListener);
|
||||||
|
ipcMain.removeListener('prompt-post-data:' + id, postDataListener);
|
||||||
|
ipcMain.removeListener('prompt-error:' + id, errorListener);
|
||||||
|
|
||||||
|
if (promptWindow) {
|
||||||
|
promptWindow.close();
|
||||||
|
promptWindow = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const postDataListener = (event, value) => {
|
||||||
|
resolve(value);
|
||||||
|
event.returnValue = null;
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
const unresponsiveListener = () => {
|
||||||
|
reject(new Error('Window was unresponsive'));
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorListener = (event, message) => {
|
||||||
|
reject(new Error(message));
|
||||||
|
event.returnValue = null;
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcMain.on('prompt-get-options:' + id, getOptionsListener);
|
||||||
|
ipcMain.on('prompt-post-data:' + id, postDataListener);
|
||||||
|
ipcMain.on('prompt-error:' + id, errorListener);
|
||||||
|
promptWindow.on('unresponsive', unresponsiveListener);
|
||||||
|
|
||||||
|
promptWindow.on('closed', () => {
|
||||||
|
promptWindow = null;
|
||||||
|
cleanup();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
const promptUrl = url.format({
|
||||||
|
protocol: 'file',
|
||||||
|
slashes: true,
|
||||||
|
pathname: path.join(__dirname, 'page', 'prompt.html'),
|
||||||
|
hash: id
|
||||||
|
});
|
||||||
|
|
||||||
|
promptWindow.loadURL(promptUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = electronPrompt;
|
||||||
72
prompt/page/prompt.css
Normal file
72
prompt/page/prompt.css
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5em;
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#form {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#label {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
margin-bottom: .8em;
|
||||||
|
padding: 0 .5em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
#data {
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #fff;
|
||||||
|
width: 90%;
|
||||||
|
padding: .4em .5em;
|
||||||
|
border: 1px solid black;
|
||||||
|
min-height: 2em;
|
||||||
|
margin: 0 0 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
select#data {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#data-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buttons {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 .5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buttons > button,
|
||||||
|
#buttons > input[type=submit] {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 0;
|
||||||
|
margin: 0 0 0 .5em;
|
||||||
|
font-size: .8em;
|
||||||
|
line-height: 1em;
|
||||||
|
padding: .6em 1em
|
||||||
|
}
|
||||||
|
|
||||||
|
#ok {
|
||||||
|
background-color: #3879D9;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cancel {
|
||||||
|
background-color: #DDD;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
18
prompt/page/prompt.html
Normal file
18
prompt/page/prompt.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link href="prompt.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<form id="form">
|
||||||
|
<div id="label">...</div>
|
||||||
|
<div id="data-container"></div>
|
||||||
|
<div id="buttons">
|
||||||
|
<button id="cancel">Cancel</button>
|
||||||
|
<button type="submit" id="ok">OK</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script src="prompt.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
170
prompt/page/prompt.js
Normal file
170
prompt/page/prompt.js
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const {ipcRenderer} = require('electron');
|
||||||
|
const docReady = require('doc-ready');
|
||||||
|
|
||||||
|
let promptId = null;
|
||||||
|
let promptOptions = null;
|
||||||
|
|
||||||
|
function promptError(error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
error = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.sendSync('prompt-error:' + promptId, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptCancel() {
|
||||||
|
ipcRenderer.sendSync('prompt-post-data:' + promptId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptSubmit() {
|
||||||
|
const dataElement = document.querySelector('#data');
|
||||||
|
let data = null;
|
||||||
|
|
||||||
|
if (promptOptions.type === 'input') {
|
||||||
|
data = dataElement.value;
|
||||||
|
} else if (promptOptions.type === 'select') {
|
||||||
|
if (promptOptions.selectMultiple) {
|
||||||
|
data = dataElement.querySelectorAll('option[selected]').map(o => o.getAttribute('value'));
|
||||||
|
} else {
|
||||||
|
data = dataElement.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.sendSync('prompt-post-data:' + promptId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptCreateInput() {
|
||||||
|
const dataElement = document.createElement('input');
|
||||||
|
dataElement.setAttribute('type', 'text');
|
||||||
|
|
||||||
|
if (promptOptions.value) {
|
||||||
|
dataElement.value = promptOptions.value;
|
||||||
|
} else {
|
||||||
|
dataElement.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promptOptions.inputAttrs && typeof (promptOptions.inputAttrs) === 'object') {
|
||||||
|
for (const k in promptOptions.inputAttrs) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(promptOptions.inputAttrs, k)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dataElement.setAttribute(k, promptOptions.inputAttrs[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataElement.addEventListener('keyup', event => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
promptCancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dataElement.addEventListener('keypress', event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
document.querySelector('#ok').click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return dataElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptCreateSelect() {
|
||||||
|
const dataElement = document.createElement('select');
|
||||||
|
let optionElement;
|
||||||
|
|
||||||
|
for (const k in promptOptions.selectOptions) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(promptOptions.selectOptions, k)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
optionElement = document.createElement('option');
|
||||||
|
optionElement.setAttribute('value', k);
|
||||||
|
optionElement.textContent = promptOptions.selectOptions[k];
|
||||||
|
if (k === promptOptions.value) {
|
||||||
|
optionElement.setAttribute('selected', 'selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
dataElement.append(optionElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptRegister() {
|
||||||
|
promptId = document.location.hash.replace('#', '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
promptOptions = JSON.parse(ipcRenderer.sendSync('prompt-get-options:' + promptId));
|
||||||
|
} catch (error) {
|
||||||
|
return promptError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promptOptions.useHtmlLabel) {
|
||||||
|
document.querySelector('#label').innerHTML = promptOptions.label;
|
||||||
|
} else {
|
||||||
|
document.querySelector('#label').textContent = promptOptions.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promptOptions.buttonLabels && promptOptions.buttonLabels.ok) {
|
||||||
|
document.querySelector('#ok').textContent = promptOptions.buttonLabels.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promptOptions.buttonLabels && promptOptions.buttonLabels.cancel) {
|
||||||
|
document.querySelector('#cancel').textContent = promptOptions.buttonLabels.cancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promptOptions.customStylesheet) {
|
||||||
|
try {
|
||||||
|
const customStyleContent = fs.readFileSync(promptOptions.customStylesheet);
|
||||||
|
if (customStyleContent) {
|
||||||
|
const customStyle = document.createElement('style');
|
||||||
|
customStyle.setAttribute('rel', 'stylesheet');
|
||||||
|
customStyle.append(document.createTextNode(customStyleContent));
|
||||||
|
document.head.append(customStyle);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return promptError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#form').addEventListener('submit', promptSubmit);
|
||||||
|
document.querySelector('#cancel').addEventListener('click', promptCancel);
|
||||||
|
|
||||||
|
const dataContainerElement = document.querySelector('#data-container');
|
||||||
|
|
||||||
|
let dataElement;
|
||||||
|
if (promptOptions.type === 'input') {
|
||||||
|
dataElement = promptCreateInput();
|
||||||
|
} else if (promptOptions.type === 'select') {
|
||||||
|
dataElement = promptCreateSelect();
|
||||||
|
} else {
|
||||||
|
return promptError(`Unhandled input type '${promptOptions.type}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataContainerElement.append(dataElement);
|
||||||
|
dataElement.setAttribute('id', 'data');
|
||||||
|
|
||||||
|
dataElement.focus();
|
||||||
|
if (promptOptions.type === 'input') {
|
||||||
|
dataElement.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promptOptions.customScript) {
|
||||||
|
try {
|
||||||
|
const customScript = require(promptOptions.customScript);
|
||||||
|
customScript();
|
||||||
|
} catch (error) {
|
||||||
|
return promptError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('error', error => {
|
||||||
|
if (promptId) {
|
||||||
|
promptError('An error has occured on the prompt window: \n' + error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
docReady(promptRegister);
|
||||||
@ -3093,13 +3093,6 @@ electron-localshortcut@^3.1.0, electron-localshortcut@^3.2.1:
|
|||||||
keyboardevent-from-electron-accelerator "^2.0.0"
|
keyboardevent-from-electron-accelerator "^2.0.0"
|
||||||
keyboardevents-areequal "^0.2.1"
|
keyboardevents-areequal "^0.2.1"
|
||||||
|
|
||||||
electron-prompt@^1.6.2:
|
|
||||||
version "1.6.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/electron-prompt/-/electron-prompt-1.6.2.tgz#e26bd3d359120bd45ce2f9625bdc380deb7ee269"
|
|
||||||
integrity sha512-gC9ZpMopIgz1kW92J7UfsXarkLdPH2zhr77bnJFlDSduYjRlr9nEPm4ux1vyzJsO72AdOyvuPPIQB0j+gWsncQ==
|
|
||||||
dependencies:
|
|
||||||
doc-ready "^1.0.4"
|
|
||||||
|
|
||||||
electron-publish@22.9.1:
|
electron-publish@22.9.1:
|
||||||
version "22.9.1"
|
version "22.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-22.9.1.tgz#7cc76ac4cc53efd29ee31c1e5facb9724329068e"
|
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-22.9.1.tgz#7cc76ac4cc53efd29ee31c1e5facb9724329068e"
|
||||||
|
|||||||
Reference in New Issue
Block a user