fix google translate and devtools deepl translate, add chrome file selector, add garbage filter

This commit is contained in:
Akash Mozumdar 2021-03-09 21:32:56 -07:00
parent 5537679442
commit 8543d49192
13 changed files with 114 additions and 77 deletions

View File

@ -110,7 +110,7 @@ namespace
QAction addExtension(ADD_EXTENSION), removeExtension(REMOVE_EXTENSION);
if (auto action = QMenu::exec({ &addExtension, &removeExtension }, ui.extenList->mapToGlobal(point), nullptr, This))
if (action == &removeExtension) Delete();
else if (QString extenFile = QFileDialog::getOpenFileName(This, ADD_EXTENSION, ".", EXTENSIONS + QString(" (*.xdll)\nLibraries (*.dll)")); !extenFile.isEmpty()) Add(extenFile);
else if (QString extenFile = QFileDialog::getOpenFileName(This, ADD_EXTENSION, ".", EXTENSIONS + QString(" (*.xdll);;Libraries (*.dll)")); !extenFile.isEmpty()) Add(extenFile);
}
}

View File

@ -104,7 +104,7 @@ void TextThread::Flush()
for (auto& sentence : sentences)
{
totalSize += sentence.size();
sentence.erase(std::remove(sentence.begin(), sentence.end(), L'\0'), sentence.end());
sentence.erase(std::remove(sentence.begin(), sentence.end(), 0), sentence.end());
if (Output(*this, sentence)) storage->append(sentence);
}

View File

@ -47,6 +47,8 @@ target_link_libraries(Regex\ Filter Qt5::Widgets)
target_link_libraries(Styler Qt5::Widgets)
target_link_libraries(Thread\ Linker Qt5::Widgets)
add_custom_target(Cleaner ALL COMMAND del *.xdll WORKING_DIRECTORY ${CMAKE_FINAL_OUTPUT_DIRECTORY})
if (NOT EXISTS ${CMAKE_FINAL_OUTPUT_DIRECTORY}/Qt5WebSockets.dll AND NOT EXISTS ${CMAKE_FINAL_OUTPUT_DIRECTORY}/Qt5WebSocketsd.dll)
add_custom_command(TARGET DevTools\ DeepL\ Translate
POST_BUILD

View File

@ -80,7 +80,7 @@ QStringList languages
};
std::wstring autoDetectLanguage = L"auto-detect";
bool translateSelectedOnly = false, rateLimitAll = true, rateLimitSelected = false, useCache = true;
bool translateSelectedOnly = false, rateLimitAll = true, rateLimitSelected = false, useCache = true, useFilter = true;
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 1000;
std::pair<bool, std::wstring> Translate(const std::wstring& text)

View File

@ -11,7 +11,7 @@ const char* TRANSLATION_PROVIDER = "DeepL Translate";
const char* GET_API_KEY_FROM = "https://www.deepl.com/pro.html";
QStringList languages
{
"Chinese (simplified): ZH",
"Chinese: ZH",
"Dutch: NL",
"English: EN",
"French: FR",
@ -25,7 +25,7 @@ QStringList languages
};
std::wstring autoDetectLanguage = L"auto";
bool translateSelectedOnly = true, rateLimitAll = true, rateLimitSelected = true, useCache = true;
bool translateSelectedOnly = true, rateLimitAll = true, rateLimitSelected = true, useCache = true, useFilter = true;
int tokenCount = 10, tokenRestoreDelay = 60000, maxSentenceSize = 1000;
enum KeyType { CAT, REST };

View File

@ -14,8 +14,7 @@ namespace
auto _ = ([]
{
QObject::connect(&webSocket, &QWebSocket::stateChanged,
[](QAbstractSocket::SocketState state) { OnStatusChanged(QMetaEnum::fromType<QAbstractSocket::SocketState>().valueToKey(state)); }
);
[](QAbstractSocket::SocketState state) { OnStatusChanged(QMetaEnum::fromType<QAbstractSocket::SocketState>().valueToKey(state)); });
QObject::connect(&webSocket, &QWebSocket::textMessageReceived, [](QString message)
{
auto result = JSON::Parse(S(message));
@ -84,7 +83,7 @@ namespace DevTools
if (GetExitCodeProcess(processInfo.hProcess, &exitCode) && exitCode == STILL_ACTIVE)
{
TerminateProcess(processInfo.hProcess, 0);
WaitForSingleObject(processInfo.hProcess, 100);
WaitForSingleObject(processInfo.hProcess, 2000);
CloseHandle(processInfo.hProcess);
CloseHandle(processInfo.hThread);
}
@ -98,11 +97,11 @@ namespace DevTools
return webSocket.state() == QAbstractSocket::ConnectedState;
}
JSON::Value<wchar_t> SendRequest(const std::wstring& method, const std::wstring& params)
JSON::Value<wchar_t> SendRequest(const char* method, const std::wstring& params)
{
concurrency::task_completion_event<JSON::Value<wchar_t>> response;
int id = idCounter += 1;
auto message = FormatString(LR"({"id":%d,"method":"%s","params":%s})", id, method, params);
auto message = FormatString(LR"({"id":%d,"method":"%S","params":%s})", id, method, params);
{
std::scoped_lock lock(devToolsMutex);
if (webSocket.state() != QAbstractSocket::ConnectedState) return {};

View File

@ -6,5 +6,5 @@ namespace DevTools
void Start(const std::wstring& path, std::function<void(QString)> statusChanged, bool headless);
void Close();
bool Connected();
JSON::Value<wchar_t> SendRequest(const std::wstring& method, const std::wstring& params = L"{}");
JSON::Value<wchar_t> SendRequest(const char* method, const std::wstring& params = L"{}");
}

View File

@ -1,17 +1,10 @@
#include "qtcommon.h"
#include "devtools.h"
#include <QFileDialog>
#include <QMouseEvent>
#include <ShlObj.h>
extern const wchar_t* TRANSLATION_ERROR;
extern Synchronized<std::wstring> translateTo, translateFrom;
extern QFormLayout* display;
extern Settings settings;
const char* TRANSLATION_PROVIDER = "DevTools DeepL Translate";
const char* GET_API_KEY_FROM = nullptr;
bool translateSelectedOnly = true, rateLimitAll = false, rateLimitSelected = false, useCache = true;
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 2500;
extern const char* CHROME_LOCATION;
extern const char* START_DEVTOOLS;
extern const char* STOP_DEVTOOLS;
@ -20,9 +13,18 @@ extern const char* DEVTOOLS_STATUS;
extern const char* AUTO_START;
extern const wchar_t* ERROR_START_CHROME;
extern Synchronized<std::wstring> translateTo, translateFrom;
extern QFormLayout* display;
extern Settings settings;
const char* TRANSLATION_PROVIDER = "DevTools DeepL Translate";
const char* GET_API_KEY_FROM = nullptr;
bool translateSelectedOnly = true, rateLimitAll = false, rateLimitSelected = false, useCache = true, useFilter = true;
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 2500;
QStringList languages
{
"Chinese (simplified): zh",
"Chinese: zh",
"Dutch: nl",
"English: en",
"French: fr",
@ -51,6 +53,18 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
if (std::filesystem::exists(programFiles)) chromePath = S(programFiles);
}
auto chromePathEdit = new QLineEdit(chromePath);
static struct : QObject
{
bool eventFilter(QObject* object, QEvent* event)
{
if (auto mouseEvent = dynamic_cast<QMouseEvent*>(event))
if (mouseEvent->button() == Qt::LeftButton)
if (QString chromePath = QFileDialog::getOpenFileName(nullptr, TRANSLATION_PROVIDER, "/", "Chrome (*.exe)"); !chromePath.isEmpty())
((QLineEdit*)object)->setText(chromePath);
return false;
}
} chromeSelector;
chromePathEdit->installEventFilter(&chromeSelector);
QObject::connect(chromePathEdit, &QLineEdit::textChanged, [chromePathEdit](QString path) { settings.setValue(CHROME_LOCATION, path); });
display->addRow(CHROME_LOCATION, chromePathEdit);
auto statusLabel = new QLabel("Stopped");
@ -58,7 +72,8 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
auto headlessCheck = new QCheckBox();
headlessCheck->setChecked(settings.value(HEADLESS_MODE, true).toBool());
QObject::connect(headlessCheck, &QCheckBox::clicked, [](bool headless) { settings.setValue(HEADLESS_MODE, headless); });
QObject::connect(startButton, &QPushButton::clicked, [statusLabel, chromePathEdit, headlessCheck] {
QObject::connect(startButton, &QPushButton::clicked, [statusLabel, chromePathEdit, headlessCheck]
{
DevTools::Start(
S(chromePathEdit->text()),
[statusLabel](QString status)
@ -79,8 +94,10 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
})
if (auto userAgent = Copy(JSON::Parse(httpRequest.response)[L"User-Agent"].String()))
if (userAgent->find(L"Headless") != std::string::npos)
DevTools::SendRequest(L"Network.setUserAgentOverride",
FormatString(LR"({"userAgent":"%s"})", userAgent->replace(userAgent->find(L"Headless"), 8, L"")));
DevTools::SendRequest(
"Network.setUserAgentOverride",
FormatString(LR"({"userAgent":"%s"})", userAgent->replace(userAgent->find(L"Headless"), 8, L""))
);
}).detach();
},
headlessCheck->isChecked()
@ -91,14 +108,14 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
buttons->addWidget(startButton);
buttons->addWidget(stopButton);
display->addRow(HEADLESS_MODE, headlessCheck);
auto autoStartButton = new QCheckBox();
autoStartButton->setChecked(settings.value(AUTO_START, false).toBool());
QObject::connect(autoStartButton, &QCheckBox::clicked, [](bool autoStart) { settings.setValue(AUTO_START, autoStart); });
display->addRow(AUTO_START, autoStartButton);
auto autoStartCheck = new QCheckBox();
autoStartCheck->setChecked(settings.value(AUTO_START, false).toBool());
QObject::connect(autoStartCheck, &QCheckBox::clicked, [](bool autoStart) { settings.setValue(AUTO_START, autoStart); });
display->addRow(AUTO_START, autoStartCheck);
display->addRow(buttons);
statusLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);
display->addRow(DEVTOOLS_STATUS, statusLabel);
if (autoStartButton->isChecked()) QMetaObject::invokeMethod(startButton, &QPushButton::click, Qt::QueuedConnection);
if (autoStartCheck->isChecked()) QMetaObject::invokeMethod(startButton, &QPushButton::click, Qt::QueuedConnection);
}
break;
case DLL_PROCESS_DETACH:
@ -116,16 +133,20 @@ std::pair<bool, std::wstring> Translate(const std::wstring& text)
// DevTools can't handle concurrent translations yet
static std::mutex translationMutex;
std::scoped_lock lock(translationMutex);
DevTools::SendRequest(L"Page.navigate", FormatString(LR"({"url":"https://www.deepl.com/translator#%s/%s/%s"})", translateFrom.Copy(), translateTo.Copy(), Escape(text)));
DevTools::SendRequest("Page.navigate", FormatString(LR"({"url":"https://www.deepl.com/translator#any/%s/%s"})", translateTo.Copy(), Escape(text)));
if (translateFrom.Copy() != autoDetectLanguage)
DevTools::SendRequest("Runtime.evaluate", FormatString(LR"({"expression":"
document.querySelector('.lmt__language_select--source').querySelector('button').click(),
document.evaluate(`//button[contains(text(),'%s')]`,document.querySelector('.lmt__language_select__menu'),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue.click()
"})", S(std::find_if(languages.begin(), languages.end(), [end = S(translateFrom.Copy())](const QString& language) { return language.endsWith(end); })->split(":")[0])));
for (int retry = 0; ++retry < 100; Sleep(100))
if (auto translation = Copy(
DevTools::SendRequest(L"Runtime.evaluate", LR"({"expression":"document.querySelector('#target-dummydiv').innerHTML","returnByValue":true})")[L"result"][L"value"].String()
)) if (!translation->empty()) return { true, translation.value() };
if (auto errorMessage = Copy(
DevTools::SendRequest(
L"Runtime.evaluate",
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
LR"({"expression":"document.querySelector('#target-dummydiv').innerHTML.trim() ","returnByValue":true})"
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
if (auto errorMessage = Copy(DevTools::SendRequest("Runtime.evaluate",
LR"({"expression":"document.querySelector('div.lmt__system_notification').innerHTML","returnByValue":true})"
)[L"result"][L"value"].String()
)) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, errorMessage.value()) };
)[L"result"][L"value"].String())) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, errorMessage.value()) };
return { false, TRANSLATION_ERROR };
}

View File

@ -122,7 +122,7 @@ QStringList languages
};
std::wstring autoDetectLanguage = L"auto";
bool translateSelectedOnly = false, rateLimitAll = true, rateLimitSelected = false, useCache = true;
bool translateSelectedOnly = false, rateLimitAll = true, rateLimitSelected = false, useCache = true, useFilter = true;
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 1000;
std::pair<bool, std::wstring> Translate(const std::wstring& text)
@ -145,34 +145,12 @@ std::pair<bool, std::wstring> Translate(const std::wstring& text)
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"translate.google.com",
L"POST",
L"/_/TranslateWebserverUi/data/batchexecute?rpcids=MkEWBc",
"f.req=" + Escape(WideStringToString(
FormatString(LR"([[["MkEWBc","[[\"%s\",\"%s\",\"%s\",true],[null]]",null,"generic"]]])", JSON::Escape((JSON::Escape(text))), translateFrom.Copy(), translateTo.Copy())
)),
L"Content-Type: application/x-www-form-urlencoded"
L"GET",
FormatString(L"/m?sl=%s&tl=%s&q=%s", translateFrom.Copy(), translateTo.Copy(), Escape(text)).c_str()
})
{
if (auto start = httpRequest.response.find(L"[["); start != std::string::npos)
{
if (auto blob = Copy(JSON::Parse(httpRequest.response.substr(start))[0][2].String())) if (auto translations = Copy(JSON::Parse(blob.value())[1][0].Array()))
{
std::wstring translation;
if (translations->size() == 1)
{
if (translations = Copy(translations.value()[0][5].Array()))
for (const auto& sentence : translations.value())
if (sentence[0].String()) (translation += *sentence[0].String()) += L" ";
}
else
{
for (const auto& conjugation : translations.value())
if (auto sentence = conjugation[0].String()) if (auto gender = conjugation[2].String()) translation += FormatString(L"%s %s\n", *sentence, *gender);
}
if (!translation.empty()) return { true, translation };
return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, blob.value()) };
}
}
auto start = httpRequest.response.find(L"result-container\">") + 18, end = httpRequest.response.find(L'<', start);
if (start != end) return { true, HTML::Unescape(httpRequest.response.substr(start, end - start)) };
return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
}
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };

View File

@ -32,6 +32,36 @@ struct HttpRequest
std::wstring Escape(const std::wstring& text);
std::string Escape(const std::string& text);
namespace HTML
{
template <typename C>
std::basic_string<C> Unescape(std::basic_string<C> text)
{
constexpr C
lt[] = { '&', 'l', 't', ';' },
gt[] = { '&', 'g', 't', ';' },
apos1[] = { '&', 'a', 'p', 'o', 's', ';' },
apos2[] = { '&', '#', '3', '9', ';' },
apos3[] = { '&', '#', 'x', '2', '7', ';' },
apos4[] = { '&', '#', 'X', '2', '7', ';' },
quot[] = { '&', 'q', 'u', 'o', 't', ';' },
amp[] = { '&', 'a', 'm', 'p', ';' };
for (int i = 0; i < text.size(); ++i)
if (text[i] == '&')
for (auto [original, length, replacement] : Array<const C*, size_t, C>{
{ lt, std::size(lt), '<' },
{ gt, std::size(gt), '>' },
{ apos1, std::size(apos1), '\'' },
{ apos2, std::size(apos2), '\'' },
{ apos3, std::size(apos3), '\'' },
{ apos4, std::size(apos4), '\'' },
{ quot, std::size(quot), '"' },
{ amp, std::size(amp), '&' }
}) if (std::char_traits<C>::compare(text.data() + i, original, length) == 0) text.replace(i, length, 1, replacement);
return text;
}
}
namespace JSON
{
template <typename C>
@ -136,7 +166,7 @@ namespace JSON
if (SkipWhitespace()) return {};
static C nullStr[] = { 'n', 'u', 'l', 'l' }, trueStr[] = { 't', 'r', 'u', 'e' }, falseStr[] = { 'f', 'a', 'l', 's', 'e' };
constexpr C nullStr[] = { 'n', 'u', 'l', 'l' }, trueStr[] = { 't', 'r', 'u', 'e' }, falseStr[] = { 'f', 'a', 'l', 's', 'e' };
if (ch == nullStr[0])
if (std::char_traits<C>::compare(text.data() + i, nullStr, std::size(nullStr)) == 0) return i += std::size(nullStr), nullptr;
else return {};

View File

@ -6,7 +6,7 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
std::vector<int> repeatNumbers(sentence.size() + 1, 0);
int repeatNumber = 1;
wchar_t prevChar = L'\0';
wchar_t prevChar = 0;
for (auto nextChar : sentence)
{
if (nextChar == prevChar)

View File

@ -13,6 +13,7 @@ extern const char* TRANSLATE_SELECTED_THREAD_ONLY;
extern const char* RATE_LIMIT_ALL_THREADS;
extern const char* RATE_LIMIT_SELECTED_THREAD;
extern const char* USE_TRANS_CACHE;
extern const char* FILTER_GARBAGE;
extern const char* RATE_LIMIT_TOKEN_COUNT;
extern const char* RATE_LIMIT_TOKEN_RESTORE_DELAY;
extern const char* MAX_SENTENCE_SIZE;
@ -23,7 +24,7 @@ extern const char* TRANSLATION_PROVIDER;
extern const char* GET_API_KEY_FROM;
extern QStringList languages;
extern std::wstring autoDetectLanguage;
extern bool translateSelectedOnly, rateLimitAll, rateLimitSelected, useCache;
extern bool translateSelectedOnly, rateLimitAll, rateLimitSelected, useCache, useFilter;
extern int tokenCount, tokenRestoreDelay, maxSentenceSize;
std::pair<bool, std::wstring> Translate(const std::wstring& text);
@ -84,6 +85,7 @@ public:
{ rateLimitAll, RATE_LIMIT_ALL_THREADS },
{ rateLimitSelected, RATE_LIMIT_SELECTED_THREAD },
{ useCache, USE_TRANS_CACHE },
{ useFilter, FILTER_GARBAGE }
})
{
value = settings.value(label, value).toBool();
@ -165,7 +167,7 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
Synchronized<std::vector<DWORD>> tokens;
} rateLimiter;
auto StripWhitespace = [](std::wstring& text)
auto Trim = [](std::wstring& text)
{
text.erase(text.begin(), std::find_if_not(text.begin(), text.end(), iswspace));
text.erase(std::find_if_not(text.rbegin(), text.rend(), iswspace).base(), text.end());
@ -173,7 +175,11 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
bool cache = false;
std::wstring translation;
StripWhitespace(sentence);
if (useFilter)
{
Trim(sentence);
sentence.erase(std::find_if(sentence.begin(), sentence.end(), [](wchar_t ch) { return ch < ' ' && ch != '\n'; }), sentence.end());
}
if (useCache)
{
auto translationCache = ::translationCache.Acquire();
@ -182,7 +188,7 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
if (translation.empty() && (!translateSelectedOnly || sentenceInfo["current select"]))
if (rateLimiter.Request() || !rateLimitAll || (!rateLimitSelected && sentenceInfo["current select"])) std::tie(cache, translation) = Translate(sentence);
else translation = TOO_MANY_TRANS_REQUESTS;
StripWhitespace(translation);
if (useFilter) Trim(translation);
if (cache) translationCache->try_emplace(sentence, translation);
if (cache && translationCache->size() > savedSize + 50) SaveCache();

View File

@ -57,7 +57,7 @@ Negatives for data_offset/split_offset refer to registers
-C for RAX, -14 for RBX, -1C for RCX, -24 for RDX, and so on for RSP, RBP, RSI, RDI, R8-R15
* means dereference pointer+deref_offset)";
const char* SAVE_SETTINGS = u8"Save settings";
const char* EXTEN_WINDOW_INSTRUCTIONS = u8R"(Right click the list to add/remove extensions
const char* EXTEN_WINDOW_INSTRUCTIONS = u8R"(Right click the list to add or remove extensions
Drag and drop extensions within the list to reorder them
(Extensions are used from top to bottom: order DOES matter))";
const char* ADD_EXTENSION = u8"Add extension";
@ -97,14 +97,14 @@ const char* MAX_HISTORY_SIZE = u8"Max history size";
const char* CONFIG_JP_LOCALE = u8"Launch with JP locale";
const wchar_t* CONSOLE = L"Console";
const wchar_t* CLIPBOARD = L"Clipboard";
const wchar_t* ABOUT = L"Textractor " ARCH L" v" VERSION LR"( made by me: Artikash (email: akashmozumdar@gmail.com)
const wchar_t* ABOUT = L"Textractor " ARCH L" v" VERSION LR"( made by Artikash (email: akashmozumdar@gmail.com)
Project homepage: https://github.com/Artikash/Textractor
Tutorial video: https://tinyurl.com/textractor-tutorial
FAQ: https://github.com/Artikash/Textractor/wiki/FAQ
Please contact me with any problems, feature requests, or questions relating to Textractor
Please contact Artikash with any problems, feature requests, or questions relating to Textractor
You can do so via the project homepage (issues section) or via email
Source code available under GPLv3 at project homepage
If you like this project, please tell everyone about it :))";
If you like this project, please tell everyone it's time to put down AGTH :))";
const wchar_t* CL_OPTIONS = LR"(usage: Textractor [-p{process id|"process name"}]...
example: Textractor -p4466 -p"My Game.exe" tries to inject processes with id 4466 or with name My Game.exe)";
const wchar_t* UPDATE_AVAILABLE = L"Update available: download it from https://github.com/Artikash/Textractor/releases";
@ -136,6 +136,7 @@ const char* HIJACK_ERROR = u8"Textractor: Hijack ERROR";
const char* COULD_NOT_FIND = u8"Textractor: could not find text";
const char* TRANSLATE_TO = u8"Translate to";
const char* TRANSLATE_FROM = u8"Translate from";
const char* FILTER_GARBAGE = u8"Filter garbage characters";
const char* TRANSLATE_SELECTED_THREAD_ONLY = u8"Translate selected text thread only";
const char* RATE_LIMIT_ALL_THREADS = u8"Rate limit all text threads";
const char* RATE_LIMIT_SELECTED_THREAD = u8"Rate limit selected text thread";
@ -146,7 +147,7 @@ const wchar_t* TOO_MANY_TRANS_REQUESTS = L"Rate limit exceeded: refuse to make m
const wchar_t* TRANSLATION_ERROR = L"Error while translating";
const char* USE_PREV_SENTENCE_CONTEXT = u8"Use previous sentence as context";
const char* API_KEY = u8"API key";
const char* CHROME_LOCATION = "Google Chrome file location";
const char* CHROME_LOCATION = u8"Google Chrome file location";
const char* START_DEVTOOLS = u8"Start DevTools";
const char* STOP_DEVTOOLS = u8"Stop DevTools";
const char* HEADLESS_MODE = u8"Headless mode";