mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-09 06:54:11 +08:00
新增quickcommand.runCode,quickcommand.runInTerminal支持warp
This commit is contained in:
parent
4053f7a9c2
commit
0a8c24374a
150
plugin/lib/createTerminalCommand.js
Normal file
150
plugin/lib/createTerminalCommand.js
Normal file
@ -0,0 +1,150 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// 终端配置
|
||||
const DEFAULT_TERMINALS = {
|
||||
windows: ["wt", "cmd"],
|
||||
macos: ["warp", "iterm", "terminal"],
|
||||
};
|
||||
|
||||
// Windows 终端命令生成器
|
||||
const getWindowsTerminalCommand = (cmdline, options = {}) => {
|
||||
const { dir, terminal = "wt" } = options;
|
||||
const appPath = path.join(
|
||||
window.utools.getPath("home"),
|
||||
"/AppData/Local/Microsoft/WindowsApps/"
|
||||
);
|
||||
|
||||
const terminalCommands = {
|
||||
wt: () => {
|
||||
if (
|
||||
fs.existsSync(appPath) &&
|
||||
fs.readdirSync(appPath).includes("wt.exe")
|
||||
) {
|
||||
const escapedCmd = cmdline.replace(/"/g, `\\"`);
|
||||
const cd = dir ? `-d "${dir.replace(/\\/g, "/")}"` : "";
|
||||
return `${appPath}wt.exe ${cd} cmd /k "${escapedCmd}"`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
cmd: () => {
|
||||
const escapedCmd = cmdline.replace(/"/g, `^"`);
|
||||
const cd = dir ? `cd /d "${dir.replace(/\\/g, "/")}" &&` : "";
|
||||
return `${cd} start "" cmd /k "${escapedCmd}"`;
|
||||
},
|
||||
};
|
||||
|
||||
// 按优先级尝试不同终端
|
||||
const terminalPriority =
|
||||
terminal === "default"
|
||||
? DEFAULT_TERMINALS.windows
|
||||
: [terminal, ...DEFAULT_TERMINALS.windows];
|
||||
|
||||
for (const term of terminalPriority) {
|
||||
const command = terminalCommands[term]?.();
|
||||
if (command) return command;
|
||||
}
|
||||
|
||||
// 如果都失败了,返回默认的 cmd 命令
|
||||
return terminalCommands.cmd();
|
||||
};
|
||||
|
||||
// macOS 终端命令生成器
|
||||
const getMacTerminalCommand = (cmdline, options = {}) => {
|
||||
const { dir, terminal = "warp" } = options;
|
||||
|
||||
const terminalCommands = {
|
||||
warp: () => {
|
||||
if (fs.existsSync("/Applications/Warp.app")) {
|
||||
const workingDir = dir || process.cwd();
|
||||
// 创建临时的 launch configuration
|
||||
const configName = `temp_${Date.now()}`;
|
||||
const configPath = path.join(
|
||||
window.utools.getPath("home"),
|
||||
".warp/launch_configurations",
|
||||
`${configName}.yml`
|
||||
);
|
||||
|
||||
// 确保目录存在
|
||||
const configDir = path.dirname(configPath);
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 创建配置文件,对于 Warp,命令不需要转义,因为是通过 YAML 配置传递
|
||||
const config = `---
|
||||
name: ${configName}
|
||||
windows:
|
||||
- tabs:
|
||||
- layout:
|
||||
cwd: "${workingDir}"
|
||||
commands:
|
||||
- exec: ${cmdline}`;
|
||||
|
||||
fs.writeFileSync(configPath, config);
|
||||
|
||||
// 使用配置文件启动 Warp
|
||||
return `open "warp://launch/${configName}" && sleep 0.5 && rm "${configPath}"`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
iterm: () => {
|
||||
const escapedCmd = cmdline.replace(/"/g, `\\"`);
|
||||
const cd = dir ? `cd ${dir.replace(/ /g, "\\\\ ")} &&` : "";
|
||||
if (fs.existsSync("/Applications/iTerm.app")) {
|
||||
return `osascript -e 'tell application "iTerm"
|
||||
if application "iTerm" is running then
|
||||
create window with default profile
|
||||
end if
|
||||
tell current session of first window to write text "clear && ${cd} ${escapedCmd}"
|
||||
activate
|
||||
end tell'`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
terminal: () => {
|
||||
const escapedCmd = cmdline.replace(/"/g, `\\"`);
|
||||
const cd = dir ? `cd ${dir.replace(/ /g, "\\\\ ")} &&` : "";
|
||||
return `osascript -e 'tell application "Terminal"
|
||||
if application "Terminal" is running then
|
||||
do script "clear && ${cd} ${escapedCmd}"
|
||||
else
|
||||
do script "clear && ${cd} ${escapedCmd}" in window 1
|
||||
end if
|
||||
activate
|
||||
end tell'`;
|
||||
},
|
||||
};
|
||||
|
||||
// 按优先级尝试不同终端
|
||||
const terminalPriority =
|
||||
terminal === "default"
|
||||
? DEFAULT_TERMINALS.macos
|
||||
: [terminal, ...DEFAULT_TERMINALS.macos];
|
||||
|
||||
for (const term of terminalPriority) {
|
||||
const command = terminalCommands[term]?.();
|
||||
if (command) return command;
|
||||
}
|
||||
|
||||
// 如果都失败了,返回默认终端命令
|
||||
return terminalCommands.terminal();
|
||||
};
|
||||
|
||||
// 主函数
|
||||
const createTerminalCommand = (cmdline, options = {}) => {
|
||||
const { windows = "default", macos = "default" } = options;
|
||||
|
||||
if (window.utools.isWindows()) {
|
||||
return getWindowsTerminalCommand(cmdline, {
|
||||
...options,
|
||||
terminal: windows,
|
||||
});
|
||||
} else if (window.utools.isMacOs()) {
|
||||
return getMacTerminalCommand(cmdline, { ...options, terminal: macos });
|
||||
}
|
||||
|
||||
throw new Error("Unsupported operating system");
|
||||
};
|
||||
|
||||
module.exports = createTerminalCommand;
|
@ -1,44 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const getCommandToLaunchTerminal = (cmdline, dir) => {
|
||||
let cd, command;
|
||||
if (window.utools.isWindows()) {
|
||||
let appPath = path.join(
|
||||
window.utools.getPath("home"),
|
||||
"/AppData/Local/Microsoft/WindowsApps/"
|
||||
);
|
||||
// 直接 existsSync wt.exe 无效
|
||||
if (fs.existsSync(appPath) && fs.readdirSync(appPath).includes("wt.exe")) {
|
||||
cmdline = cmdline.replace(/"/g, `\\"`);
|
||||
cd = dir ? `-d "${dir.replace(/\\/g, "/")}"` : "";
|
||||
command = `${appPath}wt.exe ${cd} cmd /k "${cmdline}"`;
|
||||
} else {
|
||||
cmdline = cmdline.replace(/"/g, `^"`);
|
||||
cd = dir ? `cd /d "${dir.replace(/\\/g, "/")}" &&` : "";
|
||||
command = `${cd} start "" cmd /k "${cmdline}"`;
|
||||
}
|
||||
} else if (window.utools.isMacOs()) {
|
||||
cmdline = cmdline.replace(/"/g, `\\"`);
|
||||
cd = dir ? `cd ${dir.replace(/ /g, "\\\\ ")} &&` : "";
|
||||
command = fs.existsSync("/Applications/iTerm.app")
|
||||
? `osascript -e 'tell application "iTerm"
|
||||
if application "iTerm" is running then
|
||||
create window with default profile
|
||||
end if
|
||||
tell current session of first window to write text "clear && ${cd} ${cmdline}"
|
||||
activate
|
||||
end tell'`
|
||||
: `osascript -e 'tell application "Terminal"
|
||||
if application "Terminal" is running then
|
||||
do script "clear && ${cd} ${cmdline}"
|
||||
else
|
||||
do script "clear && ${cd} ${cmdline}" in window 1
|
||||
end if
|
||||
activate
|
||||
end tell'`;
|
||||
}
|
||||
return command;
|
||||
};
|
||||
|
||||
module.exports = getCommandToLaunchTerminal;
|
@ -10,7 +10,7 @@ const systemDialog = require("./systemDialog");
|
||||
|
||||
const { getQuickcommandTempFile } = require("./getQuickcommandFile");
|
||||
|
||||
const getCommandToLaunchTerminal = require("./getCommandToLaunchTerminal");
|
||||
const createTerminalCommand = require("./createTerminalCommand");
|
||||
|
||||
const ctlKey = window.utools.isMacOs() ? "command" : "control";
|
||||
|
||||
@ -302,8 +302,10 @@ window.runPythonCommand = (py) => {
|
||||
|
||||
if (process.platform !== "linux") {
|
||||
// 在终端中执行
|
||||
quickcommand.runInTerminal = function (cmdline, dir) {
|
||||
let command = getCommandToLaunchTerminal(cmdline, dir);
|
||||
quickcommand.runInTerminal = function (cmdline, options) {
|
||||
// 兼容老版本接口, 老版本第二个参数是dir
|
||||
if (typeof options === "string") options = { dir: options };
|
||||
let command = createTerminalCommand(cmdline, options);
|
||||
child_process.exec(command);
|
||||
};
|
||||
// 系统级弹窗
|
||||
|
@ -18,7 +18,7 @@ const md5 = (input) => {
|
||||
|
||||
window.lodashM = require("./lib/lodashMini");
|
||||
|
||||
const getCommandToLaunchTerminal = require("./lib/getCommandToLaunchTerminal");
|
||||
const createTerminalCommand = require("./lib/createTerminalCommand");
|
||||
const shortCodes = require("./lib/shortCodes");
|
||||
const { pluginInfo, getUtoolsPlugins } = require("./lib/getUtoolsPlugins");
|
||||
const {
|
||||
@ -42,7 +42,8 @@ window.getuToolsLite = require("./lib/utoolsLite");
|
||||
window.quickcommand = require("./lib/quickcommand");
|
||||
window.quickcomposer = require("./lib/quickcomposer");
|
||||
window.showUb = require("./lib/showDocs");
|
||||
window.getQuickcommandTempFile = require("./lib/getQuickcommandFile").getQuickcommandTempFile;
|
||||
window.getQuickcommandTempFile =
|
||||
require("./lib/getQuickcommandFile").getQuickcommandTempFile;
|
||||
|
||||
window.getSharedQcById = async (id) => {
|
||||
const url = "https://qc.qaz.ink/home/quick/script/getScript";
|
||||
@ -211,66 +212,95 @@ window.runCodeInSandbox = (code, callback, addVars = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
window.runCodeFile = (cmd, option, terminal, callback, realTime = true) => {
|
||||
let { bin, argv, ext, charset, scptarg, envPath, alias } = option;
|
||||
let script = getQuickcommandTempFile(ext, "quickcommandTempScript");
|
||||
// 批处理和 powershell 默认编码为 GBK, 解决批处理的换行问题
|
||||
if (charset.scriptCode)
|
||||
cmd = iconv.encode(cmd.replace(/\n/g, "\r\n"), charset.scriptCode);
|
||||
fs.writeFileSync(script, cmd);
|
||||
// var argvs = [script]
|
||||
// if (argv) {
|
||||
// argvs = argv.split(' ')
|
||||
// argvs.push(script);
|
||||
// }
|
||||
let child, cmdline;
|
||||
if (bin.slice(-7) == "csc.exe") {
|
||||
cmdline = `${bin} ${argv} /out:"${
|
||||
script.slice(0, -2) + "exe"
|
||||
}" "${script}" && "${script.slice(0, -2) + "exe"}" ${scptarg}`;
|
||||
} else if (bin == "gcc") {
|
||||
var suffix = utools.isWindows() ? ".exe" : "";
|
||||
cmdline = `${bin} ${argv} "${script.slice(0, -2)}" "${script}" && "${
|
||||
script.slice(0, -2) + suffix
|
||||
}" ${scptarg}`;
|
||||
} else if (utools.isWindows() && bin == "bash") {
|
||||
cmdline = `${bin} ${argv} "${script
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/C:/i, "/mnt/c")}" ${scptarg}`;
|
||||
} else {
|
||||
cmdline = `${bin} ${argv} "${script}" ${scptarg}`;
|
||||
// 构建命令行字符串的工具函数
|
||||
const buildCommandLine = (bin, argv, script, scptarg) => {
|
||||
if (bin.slice(-7) === "csc.exe") {
|
||||
const outFile = script.slice(0, -2) + "exe";
|
||||
return `${bin} ${argv} /out:"${outFile}" "${script}" && "${outFile}" ${scptarg}`;
|
||||
}
|
||||
let processEnv = window.lodashM.cloneDeep(process.env);
|
||||
|
||||
if (bin === "gcc") {
|
||||
const suffix = utools.isWindows() ? ".exe" : "";
|
||||
const outFile = script.slice(0, -2) + suffix;
|
||||
return `${bin} ${argv} "${script.slice(
|
||||
0,
|
||||
-2
|
||||
)}" "${script}" && "${outFile}" ${scptarg}`;
|
||||
}
|
||||
|
||||
if (utools.isWindows() && bin === "bash") {
|
||||
const wslPath = script.replace(/\\/g, "/").replace(/C:/i, "/mnt/c");
|
||||
return `${bin} ${argv} "${wslPath}" ${scptarg}`;
|
||||
}
|
||||
|
||||
return `${bin} ${argv} "${script}" ${scptarg}`;
|
||||
};
|
||||
|
||||
// 处理进程输出的工具函数
|
||||
const handleProcessOutput = (child, charset, callback, realTime) => {
|
||||
const chunks = [];
|
||||
const errChunks = [];
|
||||
|
||||
child.stdout.on("data", (chunk) => {
|
||||
const decodedChunk = charset.outputCode
|
||||
? iconv.decode(chunk, charset.outputCode)
|
||||
: chunk;
|
||||
realTime
|
||||
? callback(decodedChunk.toString(), null)
|
||||
: chunks.push(decodedChunk);
|
||||
});
|
||||
|
||||
child.stderr.on("data", (errChunk) => {
|
||||
const decodedChunk = charset.outputCode
|
||||
? iconv.decode(errChunk, charset.outputCode)
|
||||
: errChunk;
|
||||
realTime
|
||||
? callback(null, decodedChunk.toString())
|
||||
: errChunks.push(decodedChunk);
|
||||
});
|
||||
|
||||
if (!realTime) {
|
||||
child.on("close", () => {
|
||||
callback(chunks.join(""), errChunks.join(""));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.runCodeFile = (
|
||||
cmd,
|
||||
option,
|
||||
terminalOptions,
|
||||
callback,
|
||||
realTime = true
|
||||
) => {
|
||||
const { bin, argv, ext, charset, scptarg, envPath, alias } = option;
|
||||
const script = getQuickcommandTempFile(ext, "quickcommandTempScript");
|
||||
|
||||
// 处理编码和换行
|
||||
const processedCmd = charset.scriptCode
|
||||
? iconv.encode(cmd.replace(/\n/g, "\r\n"), charset.scriptCode)
|
||||
: cmd;
|
||||
fs.writeFileSync(script, processedCmd);
|
||||
// 构建命令行
|
||||
let cmdline = buildCommandLine(bin, argv, script, scptarg);
|
||||
|
||||
// 处理环境变量
|
||||
const processEnv = window.lodashM.cloneDeep(process.env);
|
||||
if (envPath) processEnv.PATH = envPath;
|
||||
if (alias) cmdline = alias + "\n" + cmdline;
|
||||
// 在终端中输出
|
||||
if (terminal) cmdline = getCommandToLaunchTerminal(cmdline);
|
||||
child = child_process.spawn(cmdline, {
|
||||
if (alias) cmdline = `${alias}\n${cmdline}`;
|
||||
if (!!terminalOptions) {
|
||||
cmdline = createTerminalCommand(cmdline, terminalOptions);
|
||||
}
|
||||
|
||||
// 创建子进程
|
||||
const child = child_process.spawn(cmdline, {
|
||||
encoding: "buffer",
|
||||
shell: true,
|
||||
env: processEnv,
|
||||
});
|
||||
let chunks = [],
|
||||
err_chunks = [];
|
||||
|
||||
console.log("Running: " + cmdline);
|
||||
child.stdout.on("data", (chunk) => {
|
||||
if (charset.outputCode) chunk = iconv.decode(chunk, charset.outputCode);
|
||||
realTime ? callback(chunk.toString(), null) : chunks.push(chunk);
|
||||
});
|
||||
child.stderr.on("data", (err_chunk) => {
|
||||
if (charset.outputCode)
|
||||
err_chunk = iconv.decode(err_chunk, charset.outputCode);
|
||||
realTime
|
||||
? callback(null, err_chunk.toString())
|
||||
: err_chunks.push(err_chunk);
|
||||
});
|
||||
if (!realTime) {
|
||||
child.on("close", (code) => {
|
||||
let stdout = chunks.join("");
|
||||
let stderr = err_chunks.join("");
|
||||
callback(stdout, stderr);
|
||||
});
|
||||
}
|
||||
handleProcessOutput(child, charset, callback, realTime);
|
||||
return child;
|
||||
};
|
||||
|
||||
|
@ -159,7 +159,7 @@ export default {
|
||||
this.childProcess = window.runCodeFile(
|
||||
currentCommand.cmd,
|
||||
this.getCommandOpt(currentCommand),
|
||||
currentCommand.output === "terminal",
|
||||
currentCommand.output === "terminal" ? {} : false,
|
||||
(stdout, stderr) => this.handleResult(stdout, stderr, resultOpts)
|
||||
);
|
||||
this.listenStopSign();
|
||||
|
@ -36,6 +36,7 @@ import ButtonBox from "components/quickcommandUI/ButtonBox";
|
||||
import ConfirmBox from "components/quickcommandUI/ConfirmBox";
|
||||
import TextArea from "components/quickcommandUI/TextArea";
|
||||
import SelectList from "components/quickcommandUI/SelectList";
|
||||
import programs from "js/options/programs";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -256,8 +257,26 @@ export default {
|
||||
});
|
||||
},
|
||||
};
|
||||
// 将quickcommandUI添加到quickcommand
|
||||
Object.assign(window.quickcommand, quickcommandUI);
|
||||
|
||||
// 获取用户数据
|
||||
window.quickcommand.userData = this.$root.utools.userData;
|
||||
|
||||
// 添加runCode方法,不在preload中加是因为programs写在了src中-_-
|
||||
quickcommand.runCode = (code, program, runInTerminal = false) => {
|
||||
return new Promise((reslove, reject) => {
|
||||
window.runCodeFile(
|
||||
code,
|
||||
{ ...programs[program], charset: {}, scptarg: "" },
|
||||
runInTerminal,
|
||||
(result, err) => (err ? reject(err) : reslove(result))
|
||||
);
|
||||
false;
|
||||
});
|
||||
};
|
||||
|
||||
// 冻结quickcommand
|
||||
Object.freeze(quickcommand);
|
||||
},
|
||||
methods: {
|
||||
|
@ -17,7 +17,7 @@ import { imageCommands } from "./imageCommands";
|
||||
import { windowsCommands } from "./windowsCommands";
|
||||
import { statusCommands } from "./statusCommands";
|
||||
import { macosCommands } from "./macosCommands";
|
||||
|
||||
import { scriptCommands } from "./scriptCommands";
|
||||
let commands = [
|
||||
fileCommands,
|
||||
networkCommands,
|
||||
@ -30,6 +30,7 @@ let commands = [
|
||||
dataCommands,
|
||||
codingCommands,
|
||||
controlCommands,
|
||||
scriptCommands,
|
||||
uiCommands,
|
||||
simulateCommands,
|
||||
mathCommands,
|
||||
|
53
src/js/composer/commands/scriptCommands.js
Normal file
53
src/js/composer/commands/scriptCommands.js
Normal file
@ -0,0 +1,53 @@
|
||||
export const scriptCommands = {
|
||||
label: "编程相关",
|
||||
icon: "integration_instructions",
|
||||
commands: [
|
||||
{
|
||||
value: "",
|
||||
label: "赋值",
|
||||
icon: "script",
|
||||
outputVariable: "value",
|
||||
saveOutput: true,
|
||||
config: [
|
||||
{
|
||||
label: "值或表达式",
|
||||
component: "VariableInput",
|
||||
width: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "(function(code){new Function(code)()})",
|
||||
label: "注入JS脚本",
|
||||
icon: "script",
|
||||
config: [
|
||||
{
|
||||
label: "JS脚本",
|
||||
component: "CodeEditor",
|
||||
width: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "quickcommand.runAppleScript",
|
||||
label: "执行 AppleScript",
|
||||
icon: "script",
|
||||
outputVariable: "result",
|
||||
saveOutput: true,
|
||||
config: [
|
||||
{
|
||||
label: "脚本",
|
||||
component: "CodeEditor",
|
||||
width: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "quickcommand.runCsharp",
|
||||
label: "执行C#脚本",
|
||||
icon: "script",
|
||||
outputVariable: "result",
|
||||
saveOutput: true,
|
||||
},
|
||||
],
|
||||
};
|
52
src/plugins/monaco/types/quickcommand.api.d.ts
vendored
52
src/plugins/monaco/types/quickcommand.api.d.ts
vendored
@ -378,11 +378,23 @@ interface quickcommandApi {
|
||||
* 在终端运行,不支持 Linux
|
||||
*
|
||||
* @param command 要在终端运行的命令
|
||||
* @param options 终端运行参数
|
||||
* @param options.dir 运行目录
|
||||
* @param options.windows 终端类型,默认wt
|
||||
* @param options.macos 终端类型,默认warp
|
||||
*
|
||||
* ```js
|
||||
* quickcommand.runInTerminal(`whoami`)
|
||||
* ```
|
||||
*/
|
||||
runInTerminal(command: string);
|
||||
runInTerminal(
|
||||
command: string,
|
||||
options?: {
|
||||
dir?: string;
|
||||
windows?: "wt" | "cmd";
|
||||
macos?: "warp" | "iterm" | "terminal";
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 对应 utools.onPluginEnter 的 `code` `type` 和 `payload`
|
||||
@ -597,6 +609,44 @@ interface quickcommandApi {
|
||||
defaultText?: string,
|
||||
title?: string
|
||||
): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* 运行代码
|
||||
* @param code 代码
|
||||
* @param program 编程语言
|
||||
* @param runInTerminal 终端运行参数,不传则不在终端运行
|
||||
* @param runInTerminal.dir 运行目录
|
||||
* @param runInTerminal.windows windows使用的终端,默认wt
|
||||
* @param runInTerminal.macos macos使用的终端,默认warp
|
||||
*
|
||||
* 支持的编程语言:
|
||||
* shell, applescript, cmd, python, powershell, javascript, ruby, php, lua, perl, csharp, c
|
||||
*
|
||||
* ```js
|
||||
* quickcommand.runCode("print('Hello, World!');", "python");
|
||||
* ```
|
||||
*/
|
||||
runCode(
|
||||
code: string,
|
||||
program:
|
||||
| "shell"
|
||||
| "applescript"
|
||||
| "cmd"
|
||||
| "python"
|
||||
| "powershell"
|
||||
| "javascript"
|
||||
| "ruby"
|
||||
| "php"
|
||||
| "lua"
|
||||
| "perl"
|
||||
| "csharp"
|
||||
| "c",
|
||||
runInTerminal?: {
|
||||
dir?: string;
|
||||
windows?: "wt" | "cmd";
|
||||
macos?: "warp" | "iterm" | "terminal";
|
||||
}
|
||||
): Promise<string>;
|
||||
}
|
||||
|
||||
declare var quickcommand: quickcommandApi;
|
||||
|
Loading…
x
Reference in New Issue
Block a user