1.3.0 Master
This commit is contained in:
commit
e802cdfc1c
42
.compilerc
Normal file
42
.compilerc
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"env": {
|
||||
"development": {
|
||||
"application/javascript": {
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"electron": 1.6
|
||||
}
|
||||
}
|
||||
],
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-async-to-generator"
|
||||
],
|
||||
"sourceMaps": "inline"
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"application/javascript": {
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"electron": 1.6
|
||||
}
|
||||
}
|
||||
],
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-async-to-generator"
|
||||
],
|
||||
"sourceMaps": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
.eslintrc
Normal file
9
.eslintrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "eslint-config-airbnb",
|
||||
"rules": {
|
||||
"import/extensions": 0,
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"import/no-unresolved": [2, { "ignore": ["electron"] }],
|
||||
"linebreak-style": 0
|
||||
}
|
||||
}
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -61,4 +61,8 @@ typings/
|
||||
ffmpeg*
|
||||
ffplay*
|
||||
ffprobe*
|
||||
lib/youtube-dl*
|
||||
src/lib/youtube-dl*
|
||||
|
||||
node_modules
|
||||
out
|
||||
*.exe
|
||||
|
35
index.js
35
index.js
@ -1,35 +0,0 @@
|
||||
const path = require('path');
|
||||
global.dir = path.join(__dirname);
|
||||
const {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
dialog,
|
||||
ipcMain
|
||||
} = require('electron');
|
||||
const fs = require('fs');
|
||||
const yt_dl = require('./controller/youtube-dl')
|
||||
|
||||
var win ;
|
||||
|
||||
app.getPath('documents')
|
||||
app.on('ready', () => {
|
||||
win = new BrowserWindow({
|
||||
width: 1010,
|
||||
height: 800,
|
||||
minWidth: 1010,
|
||||
minHeight: 565,
|
||||
show: false,
|
||||
frame: true
|
||||
})
|
||||
win.loadURL(`file://${__dirname}/app/view/layout.html`)
|
||||
win.once('ready-to-show', () => {
|
||||
win.show()
|
||||
//var x = new yt_dl("https://www.youtube.com/watch?v=UbQgXeY_zi4")
|
||||
//x.download();
|
||||
})
|
||||
|
||||
win.on('closed', () => {
|
||||
app.quit();
|
||||
})
|
||||
})
|
74
package.json
74
package.json
@ -1,23 +1,67 @@
|
||||
{
|
||||
"name": "electron-simple-youtube-downloader",
|
||||
"name": "cyb3r-youtube-downloader",
|
||||
"productName": "cyb3r-youtube-downloader",
|
||||
"version": "1.0.0",
|
||||
"description": "electron-simple-youtube-downloader",
|
||||
"main": "index.js",
|
||||
"description": "My Electron application description",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "theen",
|
||||
"license": "MIT",
|
||||
"config": {
|
||||
"forge": {
|
||||
"make_targets": {
|
||||
"win32": [
|
||||
"squirrel"
|
||||
],
|
||||
"darwin": [
|
||||
"zip"
|
||||
],
|
||||
"linux": [
|
||||
"deb",
|
||||
"rpm"
|
||||
]
|
||||
},
|
||||
"electronPackagerConfig": {
|
||||
"packageManager": "npm"
|
||||
},
|
||||
"electronWinstallerConfig": {
|
||||
"name": "cyb3r_youtube_downloader",
|
||||
"icon": "app.ico"
|
||||
},
|
||||
"electronInstallerDebian": {},
|
||||
"electronInstallerRedhat": {},
|
||||
"github_repository": {
|
||||
"owner": "Theenoro",
|
||||
"name": "https://git.tooru.thee.moe/theenoro/electron-simple-youtube-downloader"
|
||||
},
|
||||
"windowsStoreConfig": {
|
||||
"packageName": "",
|
||||
"name": "cyb3ryoutubedownloader"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"electron": "^1.6.11",
|
||||
"electron-compile": "^6.4.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"request": "^2.81.0",
|
||||
"youtube-dl": "^1.11.1"
|
||||
"unzip": "^0.1.11"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "electron index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.tooru.thee.moe/theenoro/electron-simple-youtube-downloader.git"
|
||||
},
|
||||
"author": "Theenoro",
|
||||
"license": "ISC"
|
||||
"devDependencies": {
|
||||
"babel-plugin-transform-async-to-generator": "^6.24.1",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"electron-prebuilt-compile": "1.6.11",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-airbnb": "^15.1.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-jsx-a11y": "^5.1.1",
|
||||
"eslint-plugin-react": "^7.1.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
205
src/app/view/init.html
Normal file
205
src/app/view/init.html
Normal file
@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<script>
|
||||
if (typeof module === 'object') {
|
||||
window.module = module;
|
||||
module = undefined;
|
||||
}
|
||||
</script>
|
||||
<script src="./../libs/jquery/jquery-3.2.1.min.js"></script>
|
||||
<script src="./../libs/bootstrap/js/bootstrap.js"></script>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
@keyframes arrow-spin {
|
||||
100% {
|
||||
transform: rotate(179deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes arrow-spin {
|
||||
100% {
|
||||
-webkit-transform: rotate(179deg);
|
||||
}
|
||||
}
|
||||
|
||||
.psoload,
|
||||
.psoload *,
|
||||
.psoload *:before,
|
||||
.psoload *:after {
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s;
|
||||
-webkit-transition: all 0.3s;
|
||||
}
|
||||
|
||||
.psoload {
|
||||
position: relative;
|
||||
margin: 30px auto;
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.psoload .straight,
|
||||
.psoload .curve {
|
||||
position: absolute;
|
||||
top: 17.5%;
|
||||
left: 17.5%;
|
||||
width: 65%;
|
||||
height: 65%;
|
||||
border-radius: 100%;
|
||||
animation: arrow-spin 0.85s cubic-bezier(0.2, 0.8, 0.9, 0.1) infinite;
|
||||
-webkit-animation: arrow-spin 0.85s cubic-bezier(0.2, 0.8, 0.9, 0.1) infinite;
|
||||
}
|
||||
|
||||
.psoload .straight:before,
|
||||
.psoload .straight:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 15%;
|
||||
border-bottom: 3px solid #eee;
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.psoload .straight:before {
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.psoload .straight:after {
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.psoload .curve:before,
|
||||
.psoload .curve:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 10px;
|
||||
border: solid 3px transparent;
|
||||
border-top-color: #eee;
|
||||
border-radius: 50%/10px 10px 0 0;
|
||||
z-index: 90001;
|
||||
}
|
||||
|
||||
.psoload .curve:before {
|
||||
transform: rotate(-63deg) translateX(-27px) translateY(-4px);
|
||||
-webkit-transform: rotate(-63deg) translateX(-27px) translateY(-4px);
|
||||
}
|
||||
|
||||
.psoload .curve:after {
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
transform: rotate(115deg) translateX(-26px) translateY(-12px);
|
||||
-webkit-transform: rotate(115deg) translateX(-26px) translateY(-12px);
|
||||
}
|
||||
|
||||
.psoload .center {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 20%;
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
border-radius: 100%;
|
||||
border: 3px solid #eee;
|
||||
}
|
||||
|
||||
.psoload .inner {
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 25%;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
border-radius: 100%;
|
||||
animation: arrow-spin 0.85s cubic-bezier(0.2, 0.8, 0.9, 0.1) infinite reverse;
|
||||
-webkit-animation: arrow-spin 0.85s cubic-bezier(0.2, 0.8, 0.9, 0.1) infinite reverse;
|
||||
}
|
||||
|
||||
.psoload .inner:before,
|
||||
.psoload .inner:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 6px solid transparent;
|
||||
border-bottom-width: 11px;
|
||||
border-bottom-color: #eee;
|
||||
}
|
||||
|
||||
.psoload .inner:before {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
transform: rotate(128deg);
|
||||
-webkit-transform: rotate(128deg);
|
||||
}
|
||||
|
||||
.psoload .inner:after {
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
transform: rotate(-48deg);
|
||||
-webkit-transform: rotate(-48deg);
|
||||
}
|
||||
.progress{
|
||||
height:10px;
|
||||
}
|
||||
.progress .progress-bar{
|
||||
height:5px;
|
||||
width:30px;
|
||||
background-color:#FFF;
|
||||
content:"";
|
||||
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="psoload">
|
||||
<div class="straight"></div>
|
||||
<div class="curve"></div>
|
||||
<div class="center"></div>
|
||||
<div class="inner"></div>
|
||||
</div>
|
||||
<iframe id="coub" src="https://coub.com/embed/ulak9?muted=false&autostart=true&originalSize=false&startWithHD=false" allowfullscreen="true" frameborder="0" width="320" height="180"></iframe><script async src="https://c-cdn.coub.com/embed-runner.js"></script>
|
||||
<hr/>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" id="main"></div>
|
||||
<div class="progress-bar" id="single"></div>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
var start = [
|
||||
"whbex",
|
||||
"wf0jb",
|
||||
"ulak9",
|
||||
"wcmxa",
|
||||
"wc5e2"
|
||||
];
|
||||
var x = Math.floor(Math.random()*start.length)
|
||||
$('#coub').attr('src','https://coub.com/embed/'+start[x]+'?muted=false&autostart=true&originalSize=false&startWithHD=false')
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
ipcRenderer.send('download-lib', {});
|
||||
ipcRenderer.on('progress', function(event, arg) {
|
||||
console.log(arg); // prints "pong"
|
||||
$('#single').css("width",arg.percent+"%");
|
||||
}).on('fin-loading',function(event,arg){
|
||||
$('#main').css("width","100%");
|
||||
setTimeout(function(){
|
||||
ipcRenderer.send('start-full', {});
|
||||
},1000)
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
126
src/controller/download.js
Normal file
126
src/controller/download.js
Normal file
@ -0,0 +1,126 @@
|
||||
const request = require('request');
|
||||
const fs = require('fs')
|
||||
const ipcMain = require('electron').ipcMain;
|
||||
const unzip = require('unzip')
|
||||
|
||||
|
||||
var libsx = {
|
||||
"ffmpeg":"ffmpeg",
|
||||
"ffplay":"ffplay",
|
||||
"ffprobe":"ffprobe",
|
||||
"youtube-dl":"youtube-dl"
|
||||
}
|
||||
var libs = {};
|
||||
|
||||
libs.listen = null;
|
||||
libs.libs = null;
|
||||
ipcMain
|
||||
.on('download-lib', (event, arg)=>{
|
||||
console.log('TEST')
|
||||
libs.listen = event;
|
||||
console.log(`This platform is ${process.platform}`);
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
for (var lib in libsx) {
|
||||
libsx[lib] = libsx[lib]+'.exe';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
for (var lib in libsx) {
|
||||
if (fs.existsSync(global.dir+"/lib/"+libsx[lib])) {
|
||||
|
||||
}else{
|
||||
libsx[lib] = null;
|
||||
}
|
||||
}
|
||||
libs.libs = libsx;
|
||||
if(libsx['ffmpeg'] === null || libsx['ffplay'] === null || libsx['ffprobe'] === null ){
|
||||
var z = 0;
|
||||
libs.download(libs.ff(process.platform),"ffmpeg",function(data,file){
|
||||
fs.createReadStream(file)
|
||||
.pipe(unzip.Parse())
|
||||
.on('entry', function (entry) {
|
||||
var fileName = entry.path;
|
||||
var type = entry.type; // 'Directory' or 'File'
|
||||
var size = entry.size;
|
||||
console.log(fileName)
|
||||
if (fileName === "ffmpeg-latest-win32-static/bin/ffmpeg.exe" || fileName === "ffmpeg-latest-win32-static/bin/ffprobe.exe" || fileName === "ffmpeg-latest-win32-static/bin/ffplay.exe") {
|
||||
entry.pipe(fs.createWriteStream(global.dir+'/lib/'+fileName.split('/')[2]));
|
||||
console.log('TEST')
|
||||
z++;
|
||||
if(z == 3){
|
||||
libs.checkNext('youtube-dl');
|
||||
}
|
||||
} else {
|
||||
entry.autodrain();
|
||||
}
|
||||
});
|
||||
});
|
||||
}else{
|
||||
libs.checkNext('youtube-dl');
|
||||
}
|
||||
console.dir(libsx);
|
||||
})
|
||||
libs.checkNext = (nxt)=>{
|
||||
if( libs.libs['youtube-dl'] === null ){
|
||||
libs.yt_dl(process.platform,function(){
|
||||
libs.startFull();
|
||||
});
|
||||
}else{
|
||||
libs.startFull();
|
||||
}
|
||||
}
|
||||
libs.startFull = ()=>{
|
||||
libs.listen.sender.send('fin-loading',{});
|
||||
}
|
||||
libs.ff = (os)=>{
|
||||
var url = "";
|
||||
switch (os) {
|
||||
case "win32":
|
||||
url = 'http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-latest-win32-static.zip';
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
return url;
|
||||
}
|
||||
libs.yt_dl = (os,cb)=>{
|
||||
var url = "";
|
||||
switch (os) {
|
||||
case "win32":
|
||||
url = 'https://yt-dl.org/downloads/2017.08.13/youtube-dl.exe'
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
libs.download(url,"youtube-dl",function(data,file){
|
||||
fs.renameSync(file,global.dir+'/lib/youtube-dl.exe');
|
||||
cb();
|
||||
})
|
||||
|
||||
}
|
||||
libs.download = (url,data,cb)=>{
|
||||
var r = request(url);
|
||||
var actual = 1;
|
||||
var full = 100;
|
||||
var perc = 0;
|
||||
r.on('data', function (chunk) {
|
||||
actual += chunk.length;
|
||||
perc = actual / full * 100;
|
||||
console.log(perc);
|
||||
libs.listen.sender.send('progress',{percent :Math.floor(perc)});
|
||||
});
|
||||
|
||||
r.on('response', function (res) {
|
||||
res.pipe(fs.createWriteStream(global.dir+'/tmp/' + data + '.' + res.headers['content-type'].split('/')[1]));
|
||||
full = res.headers[ 'content-length' ] ;
|
||||
res.on('end', function () {
|
||||
cb(data,global.dir+'/tmp/' + data + '.' + res.headers['content-type'].split('/')[1]);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = libs;
|
10
src/index.html
Normal file
10
src/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
Well hey there!!!
|
||||
</body>
|
||||
</html>
|
108
src/index.js
Normal file
108
src/index.js
Normal file
@ -0,0 +1,108 @@
|
||||
const path = require('path');
|
||||
global.dir = path.join(__dirname);
|
||||
const {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
dialog,
|
||||
ipcMain
|
||||
} = require('electron');
|
||||
const fs = require('fs');
|
||||
const yt_dl = require('./controller/youtube-dl')
|
||||
const dl = require('./controller/download');
|
||||
|
||||
if (!fs.existsSync(global.dir+'/tmp/inst')) {
|
||||
fs.writeFileSync(global.dir+'/tmp/inst',"out");
|
||||
var win;
|
||||
const createWindow = () =>{
|
||||
win = new BrowserWindow({
|
||||
width: 320,
|
||||
height: 500,
|
||||
show: false,
|
||||
frame: false
|
||||
})
|
||||
win.once('ready-to-show', () => {
|
||||
})
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null;
|
||||
});
|
||||
}
|
||||
app.on('ready', createWindow);
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
var win ;
|
||||
|
||||
|
||||
app.getPath('documents')
|
||||
const createWindow = () =>{
|
||||
|
||||
//dl.download(dl.ff(process.platform));
|
||||
/*
|
||||
|
||||
win = new BrowserWindow({
|
||||
width: 1010,
|
||||
height: 800,
|
||||
minWidth: 1010,
|
||||
minHeight: 565,
|
||||
show: false,
|
||||
frame: true
|
||||
})
|
||||
*/
|
||||
win = new BrowserWindow({
|
||||
width: 320,
|
||||
height: 500,
|
||||
show: false,
|
||||
frame: false
|
||||
})
|
||||
ipcMain
|
||||
.on('start-full', (event, arg)=>{
|
||||
|
||||
var win2 = new BrowserWindow({
|
||||
width: 1010,
|
||||
height: 800,
|
||||
minWidth: 1010,
|
||||
minHeight: 565,
|
||||
show: true,
|
||||
frame: true
|
||||
})
|
||||
win.close();
|
||||
win2.loadURL(`file://${__dirname}/app/view/layout.html`)
|
||||
win = win2;
|
||||
});
|
||||
win.loadURL(`file://${__dirname}/app/view/init.html`)
|
||||
win.once('ready-to-show', () => {
|
||||
win.show()
|
||||
//var x = new yt_dl("https://www.youtube.com/watch?v=UbQgXeY_zi4")
|
||||
//x.download();
|
||||
})
|
||||
|
||||
win.on('closed', () => {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
win = null;
|
||||
});
|
||||
}
|
||||
app.on('ready', createWindow);
|
||||
app.on('window-all-closed', () => {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
app.on('activate', () => {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = app;
|
46
src/main.js
Normal file
46
src/main.js
Normal file
@ -0,0 +1,46 @@
|
||||
var app = require('./index');
|
||||
|
||||
var handleStartupEvent = function() {
|
||||
if (process.platform !== 'win32') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var squirrelCommand = process.argv[1];
|
||||
console.log(squirrelCommand);
|
||||
switch (squirrelCommand) {
|
||||
case '--squirrel-install':
|
||||
target = path.basename(process.execPath);
|
||||
updateDotExe = path.resolve(path.dirname(process.execPath), '..', 'update.exe');
|
||||
var createShortcut = updateDotExe + ' --createShortcut=' + target + ' --shortcut-locations=Desktop,StartMenu' ;
|
||||
console.log (createShortcut);
|
||||
exec(createShortcut);
|
||||
// Always quit when done
|
||||
app.quit();
|
||||
return true;
|
||||
|
||||
case '--squirrel-uninstall':
|
||||
// Undo anything you did in the --squirrel-install and
|
||||
// --squirrel-updated handlers
|
||||
target = path.basename(process.execPath);
|
||||
updateDotExe = path.resolve(path.dirname(process.execPath), '..', 'update.exe');
|
||||
var createShortcut = updateDotExe + ' --removeShortcut=' + target ;
|
||||
console.log (createShortcut);
|
||||
exec(createShortcut);
|
||||
// Always quit when done
|
||||
app.quit();
|
||||
return true;
|
||||
case '--squirrel-obsolete':
|
||||
// This is called on the outgoing version of your app before
|
||||
// we update to the new version - it's the opposite of
|
||||
// --squirrel-updated
|
||||
app.quit();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
(function(){
|
||||
if (handleStartupEvent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
})
|
0
src/tmp/dummy
Normal file
0
src/tmp/dummy
Normal file
Loading…
Reference in New Issue
Block a user