add devtools api and new deepl extension
Added the DevTools API and the wrapper with the use of the QtWebSockets library. Added a new DeepL extension which uses the DevTools API.
This commit is contained in:
parent
fe1cdfc947
commit
6b54ec0733
@ -1,7 +1,7 @@
|
|||||||
include(QtUtils)
|
include(QtUtils)
|
||||||
msvc_registry_search()
|
msvc_registry_search()
|
||||||
find_qt5(Core Widgets)
|
find_qt5(Core Widgets WebSockets)
|
||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
cmake_policy(SET CMP0037 OLD)
|
cmake_policy(SET CMP0037 OLD)
|
||||||
|
|
||||||
add_library(Bing\ Translate MODULE bingtranslate.cpp translatewrapper.cpp network.cpp extensionimpl.cpp)
|
add_library(Bing\ Translate MODULE bingtranslate.cpp translatewrapper.cpp network.cpp extensionimpl.cpp)
|
||||||
@ -10,6 +10,7 @@ add_library(DeepL\ Translate MODULE deepltranslate.cpp translatewrapper.cpp netw
|
|||||||
add_library(Extra\ Newlines MODULE extranewlines.cpp extensionimpl.cpp)
|
add_library(Extra\ Newlines MODULE extranewlines.cpp extensionimpl.cpp)
|
||||||
add_library(Extra\ Window MODULE extrawindow.cpp extensionimpl.cpp)
|
add_library(Extra\ Window MODULE extrawindow.cpp extensionimpl.cpp)
|
||||||
add_library(Google\ Translate MODULE googletranslate.cpp translatewrapper.cpp network.cpp extensionimpl.cpp)
|
add_library(Google\ Translate MODULE googletranslate.cpp translatewrapper.cpp network.cpp extensionimpl.cpp)
|
||||||
|
add_library(DevTools\ DeepL\ Translate MODULE devtoolsdeepltranslate.cpp devtoolswrapper.cpp devtools.cpp network.cpp extensionimpl.cpp)
|
||||||
add_library(Lua MODULE lua.cpp extensionimpl.cpp)
|
add_library(Lua MODULE lua.cpp extensionimpl.cpp)
|
||||||
add_library(Regex\ Filter MODULE regexfilter.cpp extensionimpl.cpp)
|
add_library(Regex\ Filter MODULE regexfilter.cpp extensionimpl.cpp)
|
||||||
add_library(Remove\ Repeated\ Characters MODULE removerepeatchar.cpp extensionimpl.cpp)
|
add_library(Remove\ Repeated\ Characters MODULE removerepeatchar.cpp extensionimpl.cpp)
|
||||||
@ -25,6 +26,7 @@ target_precompile_headers(DeepL\ Translate REUSE_FROM pch)
|
|||||||
target_precompile_headers(Extra\ Newlines REUSE_FROM pch)
|
target_precompile_headers(Extra\ Newlines REUSE_FROM pch)
|
||||||
target_precompile_headers(Extra\ Window REUSE_FROM pch)
|
target_precompile_headers(Extra\ Window REUSE_FROM pch)
|
||||||
target_precompile_headers(Google\ Translate REUSE_FROM pch)
|
target_precompile_headers(Google\ Translate REUSE_FROM pch)
|
||||||
|
target_precompile_headers(DevTools\ DeepL\ Translate REUSE_FROM pch)
|
||||||
target_precompile_headers(Lua REUSE_FROM pch)
|
target_precompile_headers(Lua REUSE_FROM pch)
|
||||||
target_precompile_headers(Regex\ Filter REUSE_FROM pch)
|
target_precompile_headers(Regex\ Filter REUSE_FROM pch)
|
||||||
target_precompile_headers(Remove\ Repeated\ Characters REUSE_FROM pch)
|
target_precompile_headers(Remove\ Repeated\ Characters REUSE_FROM pch)
|
||||||
@ -38,6 +40,16 @@ target_link_libraries(Bing\ Translate winhttp Qt5::Widgets)
|
|||||||
target_link_libraries(DeepL\ Translate winhttp Qt5::Widgets)
|
target_link_libraries(DeepL\ Translate winhttp Qt5::Widgets)
|
||||||
target_link_libraries(Extra\ Window Qt5::Widgets)
|
target_link_libraries(Extra\ Window Qt5::Widgets)
|
||||||
target_link_libraries(Google\ Translate winhttp Qt5::Widgets)
|
target_link_libraries(Google\ Translate winhttp Qt5::Widgets)
|
||||||
|
target_link_libraries(DevTools\ DeepL\ Translate winhttp Qt5::Widgets Qt5::WebSockets)
|
||||||
target_link_libraries(Lua lua53 Qt5::Widgets)
|
target_link_libraries(Lua lua53 Qt5::Widgets)
|
||||||
target_link_libraries(Regex\ Filter Qt5::Widgets)
|
target_link_libraries(Regex\ Filter Qt5::Widgets)
|
||||||
target_link_libraries(Thread\ Linker Qt5::Widgets)
|
target_link_libraries(Thread\ Linker Qt5::Widgets)
|
||||||
|
|
||||||
|
if (NOT EXISTS ${CMAKE_FINAL_OUTPUT_DIRECTORY}/Qt5WebSockets.dll)
|
||||||
|
add_custom_command(TARGET DevTools\ DeepL\ Translate
|
||||||
|
POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E remove_directory "${CMAKE_CURRENT_BINARY_DIR}/windeployqt"
|
||||||
|
COMMAND set PATH=%PATH%$<SEMICOLON>${qt5_install_prefix}/bin
|
||||||
|
COMMAND Qt5::windeployqt --dir $<TARGET_FILE_DIR:${PROJECT_NAME}> "$<TARGET_FILE_DIR:${PROJECT_NAME}>/DevTools\ DeepL\ Translate.dll"
|
||||||
|
)
|
||||||
|
endif()
|
278
extensions/devtools.cpp
Normal file
278
extensions/devtools.cpp
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
#include "devtools.h"
|
||||||
|
|
||||||
|
DevTools::DevTools(QObject* parent) :
|
||||||
|
QObject(parent),
|
||||||
|
idcounter(0),
|
||||||
|
pagenavigated(false),
|
||||||
|
translateready(false),
|
||||||
|
status("Stopped"),
|
||||||
|
session(1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void DevTools::startDevTools(QString path, bool headless, int port)
|
||||||
|
{
|
||||||
|
if (startChrome(path, headless, port))
|
||||||
|
{
|
||||||
|
QString webSocketDebuggerUrl;
|
||||||
|
if (GetwebSocketDebuggerUrl(webSocketDebuggerUrl, port))
|
||||||
|
{
|
||||||
|
connect(&webSocket, &QWebSocket::stateChanged, this, &DevTools::stateChanged);
|
||||||
|
connect(&webSocket, &QWebSocket::textMessageReceived, this, &DevTools::onTextMessageReceived);
|
||||||
|
webSocket.open(webSocketDebuggerUrl);
|
||||||
|
session += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status = "Failed to find chrome debug port!";
|
||||||
|
emit statusChanged(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status = "Failed to start chrome!";
|
||||||
|
emit statusChanged(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int DevTools::getSession()
|
||||||
|
{
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DevTools::getStatus()
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
DevTools::~DevTools()
|
||||||
|
{
|
||||||
|
closeDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DevTools::startChrome(QString path, bool headless, int port)
|
||||||
|
{
|
||||||
|
if (!std::filesystem::exists(path.toStdWString()))
|
||||||
|
return false;
|
||||||
|
DWORD exitCode = 0;
|
||||||
|
if ((GetExitCodeProcess(processInfo.hProcess, &exitCode) != FALSE) && (exitCode == STILL_ACTIVE))
|
||||||
|
return false;
|
||||||
|
QString args = "--proxy-server=direct:// --disable-extensions --disable-gpu --user-data-dir="
|
||||||
|
+ QString::fromStdWString(std::filesystem::current_path())
|
||||||
|
+ "\\devtoolscache --remote-debugging-port="
|
||||||
|
+ QString::number(port);
|
||||||
|
if (headless)
|
||||||
|
args += " --headless";
|
||||||
|
STARTUPINFOW dummy = { sizeof(dummy) };
|
||||||
|
if (!CreateProcessW(NULL, (wchar_t*)(path + " " + args).utf16(), nullptr, nullptr,
|
||||||
|
FALSE, 0, nullptr, nullptr, &dummy, &processInfo))
|
||||||
|
return false;
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DevTools::GetwebSocketDebuggerUrl(QString& url, int port)
|
||||||
|
{
|
||||||
|
url.clear();
|
||||||
|
if (HttpRequest httpRequest{
|
||||||
|
L"Mozilla/5.0 Textractor",
|
||||||
|
L"127.0.0.1",
|
||||||
|
L"POST",
|
||||||
|
FormatString(L"/json/list").c_str(),
|
||||||
|
"",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
WINHTTP_FLAG_ESCAPE_DISABLE,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
DWORD(port)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
QString qtString = QString::fromStdWString(httpRequest.response);
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(qtString.toUtf8());
|
||||||
|
QJsonArray rootObject = doc.array();
|
||||||
|
|
||||||
|
for (const auto obj : rootObject)
|
||||||
|
if (obj.toObject().value("type") == "page")
|
||||||
|
{
|
||||||
|
url.append(obj.toObject().value("webSocketDebuggerUrl").toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!url.isEmpty())
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DevTools::stateChanged(QAbstractSocket::SocketState state)
|
||||||
|
{
|
||||||
|
QMetaEnum metaenum = QMetaEnum::fromType<QAbstractSocket::SocketState>();
|
||||||
|
status = metaenum.valueToKey(state);
|
||||||
|
emit statusChanged(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DevTools::setNavigated(bool value)
|
||||||
|
{
|
||||||
|
mutex.lock();
|
||||||
|
pagenavigated = value;
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DevTools::getNavigated()
|
||||||
|
{
|
||||||
|
return pagenavigated;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DevTools::setTranslate(bool value)
|
||||||
|
{
|
||||||
|
mutex.lock();
|
||||||
|
translateready = value;
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DevTools::getTranslate()
|
||||||
|
{
|
||||||
|
return translateready;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DevTools::SendRequest(QString method, QJsonObject params, QJsonObject& root)
|
||||||
|
{
|
||||||
|
if (!isConnected())
|
||||||
|
return false;
|
||||||
|
root = QJsonObject();
|
||||||
|
QJsonObject json;
|
||||||
|
task_completion_event<QJsonObject> response;
|
||||||
|
long id = idIncrement();
|
||||||
|
json.insert("id", id);
|
||||||
|
json.insert("method", method);
|
||||||
|
if (!params.isEmpty())
|
||||||
|
json.insert("params", params);
|
||||||
|
QJsonDocument doc(json);
|
||||||
|
QString message(doc.toJson(QJsonDocument::Compact));
|
||||||
|
mutex.lock();
|
||||||
|
mapqueue.insert(std::make_pair(id, response));
|
||||||
|
mutex.unlock();
|
||||||
|
webSocket.sendTextMessage(message);
|
||||||
|
webSocket.flush();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
root = create_task(response).get();
|
||||||
|
}
|
||||||
|
catch (const std::exception& ex)
|
||||||
|
{
|
||||||
|
response.set_exception(ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!root.isEmpty())
|
||||||
|
{
|
||||||
|
if (root.contains("error"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (root.contains("result"))
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long DevTools::idIncrement()
|
||||||
|
{
|
||||||
|
return ++idcounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DevTools::isConnected()
|
||||||
|
{
|
||||||
|
if (webSocket.state() == QAbstractSocket::ConnectedState)
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DevTools::onTextMessageReceived(QString message)
|
||||||
|
{
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
|
||||||
|
if (doc.isObject())
|
||||||
|
{
|
||||||
|
QJsonObject root = doc.object();
|
||||||
|
if (root.contains("method"))
|
||||||
|
{
|
||||||
|
if (root.value("method").toString() == "Page.navigatedWithinDocument")
|
||||||
|
{
|
||||||
|
mutex.lock();
|
||||||
|
pagenavigated = true;
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
if (root.value("method").toString() == "DOM.attributeModified")
|
||||||
|
{
|
||||||
|
if (root.value("params").toObject().value("value") == "lmt__mobile_share_container")
|
||||||
|
{
|
||||||
|
mutex.lock();
|
||||||
|
translateready = true;
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
if (root.contains("id"))
|
||||||
|
{
|
||||||
|
long id = root.value("id").toInt();
|
||||||
|
MapResponse::iterator request = mapqueue.find(id);
|
||||||
|
if (request != mapqueue.end())
|
||||||
|
{
|
||||||
|
request->second.set(root);
|
||||||
|
mutex.lock();
|
||||||
|
mapqueue.erase(request);
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DevTools::closeDevTools()
|
||||||
|
{
|
||||||
|
if (this->mapqueue.size() > 0)
|
||||||
|
{
|
||||||
|
MapResponse::iterator iter = this->mapqueue.begin();
|
||||||
|
MapResponse::iterator iend = this->mapqueue.end();
|
||||||
|
for (; iter != iend; iter++)
|
||||||
|
{
|
||||||
|
iter->second.set_exception("exception");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
webSocket.close();
|
||||||
|
mapqueue.clear();
|
||||||
|
idcounter = 0;
|
||||||
|
|
||||||
|
DWORD exitCode = 0;
|
||||||
|
if (GetExitCodeProcess(processInfo.hProcess, &exitCode) != FALSE)
|
||||||
|
{
|
||||||
|
if (exitCode == STILL_ACTIVE)
|
||||||
|
{
|
||||||
|
TerminateProcess(processInfo.hProcess, 0);
|
||||||
|
WaitForSingleObject(processInfo.hProcess, 100);
|
||||||
|
CloseHandle(processInfo.hProcess);
|
||||||
|
CloseHandle(processInfo.hThread);
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::filesystem::remove_all(L"devtoolscache");
|
||||||
|
}
|
||||||
|
catch (const std::exception&)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status = "Stopped";
|
||||||
|
emit statusChanged(status);
|
||||||
|
}
|
49
extensions/devtools.h
Normal file
49
extensions/devtools.h
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#include <fstream>
|
||||||
|
#include <QtCore>
|
||||||
|
#include <QtWebSockets/QWebSocket>
|
||||||
|
#include <ppltasks.h>
|
||||||
|
#include "network.h"
|
||||||
|
|
||||||
|
using namespace Concurrency;
|
||||||
|
|
||||||
|
typedef std::map<long, task_completion_event<QJsonObject>> MapResponse;
|
||||||
|
|
||||||
|
class DevTools : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit DevTools(QObject* parent = nullptr);
|
||||||
|
~DevTools();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void statusChanged(const QString &);
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void stateChanged(QAbstractSocket::SocketState state);
|
||||||
|
void onTextMessageReceived(QString message);
|
||||||
|
|
||||||
|
public:
|
||||||
|
void startDevTools(QString path, bool headless = false, int port = 9222);
|
||||||
|
void closeDevTools();
|
||||||
|
void setNavigated(bool value);
|
||||||
|
bool getNavigated();
|
||||||
|
void setTranslate(bool value);
|
||||||
|
bool getTranslate();
|
||||||
|
int getSession();
|
||||||
|
bool SendRequest(QString command, QJsonObject params, QJsonObject& result);
|
||||||
|
QString getStatus();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool isConnected();
|
||||||
|
bool startChrome(QString path, bool headless = false, int port = 9222);
|
||||||
|
bool GetwebSocketDebuggerUrl(QString& url, int port = 9222);
|
||||||
|
long idIncrement();
|
||||||
|
int session;
|
||||||
|
QWebSocket webSocket;
|
||||||
|
std::mutex mutex;
|
||||||
|
MapResponse mapqueue;
|
||||||
|
bool pagenavigated;
|
||||||
|
bool translateready;
|
||||||
|
long idcounter;
|
||||||
|
PROCESS_INFORMATION processInfo;
|
||||||
|
QString status;
|
||||||
|
};
|
195
extensions/devtoolsdeepltranslate.cpp
Normal file
195
extensions/devtoolsdeepltranslate.cpp
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
#include "qtcommon.h"
|
||||||
|
#include "extension.h"
|
||||||
|
#include "network.h"
|
||||||
|
#include "devtools.h"
|
||||||
|
|
||||||
|
extern const wchar_t* TRANSLATION_ERROR;
|
||||||
|
extern Synchronized<std::wstring> translateTo;
|
||||||
|
|
||||||
|
bool useCache = true, autostartchrome = false, headlesschrome = true;
|
||||||
|
int maxSentenceSize = 500, chromeport = 9222;
|
||||||
|
|
||||||
|
const char* TRANSLATION_PROVIDER = "DevTools DeepL Translate";
|
||||||
|
QString URL = "https://www.deepl.com/en/translator";
|
||||||
|
QStringList languages
|
||||||
|
{
|
||||||
|
"Chinese (simplified): zh",
|
||||||
|
"Dutch: nl",
|
||||||
|
"English: en",
|
||||||
|
"French: fr",
|
||||||
|
"German: de",
|
||||||
|
"Italian: it",
|
||||||
|
"Japanese: ja",
|
||||||
|
"Polish: pl",
|
||||||
|
"Portuguese: pt",
|
||||||
|
"Russian: ru",
|
||||||
|
"Spanish: es",
|
||||||
|
};
|
||||||
|
|
||||||
|
int docfound = -1;
|
||||||
|
int targetNodeId = -1;
|
||||||
|
int session = -1;
|
||||||
|
|
||||||
|
std::pair<bool, std::wstring> Translate(const std::wstring& text, DevTools* devtools)
|
||||||
|
{
|
||||||
|
if (devtools->getStatus() == "Stopped")
|
||||||
|
{
|
||||||
|
return { false, FormatString(L"Error: chrome not started") };
|
||||||
|
}
|
||||||
|
if ((devtools->getStatus().startsWith("Fail")) || (devtools->getStatus().startsWith("Unconnected")))
|
||||||
|
{
|
||||||
|
return { false, FormatString(L"Error: %s", S(devtools->getStatus())) };
|
||||||
|
}
|
||||||
|
if (session != devtools->getSession())
|
||||||
|
{
|
||||||
|
session = devtools->getSession();
|
||||||
|
docfound = -1;
|
||||||
|
targetNodeId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString qtext = S(text);
|
||||||
|
|
||||||
|
// Check text for repeated symbols (e.g. only ellipsis)
|
||||||
|
if (qtext.length() > 2)
|
||||||
|
for (int i = 1; i < (qtext.length() - 1); i++)
|
||||||
|
{
|
||||||
|
if (qtext[i] != qtext[1])
|
||||||
|
break;
|
||||||
|
if ((i + 2) == qtext.length() && (qtext.front() == qtext.back()))
|
||||||
|
{
|
||||||
|
return { false, text };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add spaces near ellipsis for better translation and check for quotes
|
||||||
|
qtext.replace(QRegularExpression("[" + QString(8230) + "]" + "[" + QString(8230) + "]" + "[" + QString(8230) + "]"), QString(8230));
|
||||||
|
qtext.replace(QRegularExpression("[" + QString(8230) + "]" + "[" + QString(8230) + "]"), QString(8230));
|
||||||
|
qtext.replace(QRegularExpression("[" + QString(8230) + "]"), " " + QString(8230) + " ");
|
||||||
|
bool checkquote = false;
|
||||||
|
if ((qtext.front() == QString(12300)) && (qtext.back() == QString(12301)))
|
||||||
|
{
|
||||||
|
checkquote = true;
|
||||||
|
qtext.remove(0, 1);
|
||||||
|
qtext.chop(1);
|
||||||
|
}
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject result;
|
||||||
|
|
||||||
|
// Enable page feedback
|
||||||
|
if (!devtools->SendRequest("Page.enable", {}, root))
|
||||||
|
{
|
||||||
|
return { false, FormatString(L"Error: page enable failed! %s", TRANSLATION_ERROR) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to site
|
||||||
|
QString fullurl = URL + "#ja/" + S(translateTo.Copy()) + "/" + qtext;
|
||||||
|
devtools->setNavigated(false);
|
||||||
|
devtools->setTranslate(false);
|
||||||
|
if (devtools->SendRequest("Page.navigate", { {"url", fullurl} }, root))
|
||||||
|
{
|
||||||
|
// Wait until page is loaded
|
||||||
|
float timer = 0;
|
||||||
|
int timer_stop = 10;
|
||||||
|
while (!devtools->getNavigated() && timer < timer_stop)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
timer += 0.1;
|
||||||
|
}
|
||||||
|
if (timer >= timer_stop)
|
||||||
|
{
|
||||||
|
return { false, FormatString(L"Error: page load timeout %d s! %s", timer_stop, TRANSLATION_ERROR) };
|
||||||
|
}
|
||||||
|
QString OuterHTML("<div></div>");
|
||||||
|
|
||||||
|
// Get document
|
||||||
|
if (docfound == -1)
|
||||||
|
{
|
||||||
|
if (!devtools->SendRequest("DOM.getDocument", {}, root))
|
||||||
|
{
|
||||||
|
docfound = -1;
|
||||||
|
return { false, FormatString(L"Error: getDocument failed! %s", TRANSLATION_ERROR) };
|
||||||
|
}
|
||||||
|
docfound = root.value("result").toObject().value("root").toObject().value("nodeId").toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get target selector
|
||||||
|
if (targetNodeId == -1)
|
||||||
|
{
|
||||||
|
if (!(devtools->SendRequest("DOM.querySelector", { {"nodeId", docfound}, {"selector", "textarea.lmt__target_textarea"} }, root))
|
||||||
|
|| (root.value("result").toObject().value("nodeId").toInt() == 0))
|
||||||
|
{
|
||||||
|
docfound = -1;
|
||||||
|
return { false, FormatString(L"Error: querySelector result failed! %s", TRANSLATION_ERROR) };
|
||||||
|
}
|
||||||
|
targetNodeId = root.value("result").toObject().value("nodeId").toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for translation to appear on the web page
|
||||||
|
timer = 0;
|
||||||
|
while (!devtools->getTranslate() && timer < timer_stop)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
timer += 0.1;
|
||||||
|
}
|
||||||
|
if (timer >= timer_stop)
|
||||||
|
{
|
||||||
|
// Catch notification if timeout
|
||||||
|
int noteNodeId = -1;
|
||||||
|
if (!(devtools->SendRequest("DOM.querySelector", { {"nodeId", docfound}, {"selector", "div.lmt__system_notification"} }, root))
|
||||||
|
|| (root.value("result").toObject().value("nodeId").toInt() == 0))
|
||||||
|
{
|
||||||
|
return { false, FormatString(L"Error: result timeout %d s! %s", timer_stop, TRANSLATION_ERROR) };
|
||||||
|
}
|
||||||
|
noteNodeId = root.value("result").toObject().value("nodeId").toInt();
|
||||||
|
|
||||||
|
if (devtools->SendRequest("DOM.getOuterHTML", { {"nodeId", noteNodeId + 1} }, root))
|
||||||
|
{
|
||||||
|
OuterHTML = root.value("result").toObject().value("outerHTML").toString();
|
||||||
|
OuterHTML.remove(QRegExp("<[^>]*>"));
|
||||||
|
OuterHTML = OuterHTML.trimmed();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OuterHTML = "Could not get notification";
|
||||||
|
}
|
||||||
|
return { false, FormatString(L"Error: got notification from translator: %s", S(OuterHTML)) };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch the translation
|
||||||
|
devtools->SendRequest("DOM.getOuterHTML", { {"nodeId", targetNodeId + 1} }, root);
|
||||||
|
result = root.value("result").toObject();
|
||||||
|
OuterHTML = result.value("outerHTML").toString();
|
||||||
|
OuterHTML.remove(QRegExp("<[^>]*>"));
|
||||||
|
OuterHTML = OuterHTML.trimmed();
|
||||||
|
|
||||||
|
// Check if the translator output language does not match the selected language
|
||||||
|
if (devtools->SendRequest("DOM.getAttributes", { {"nodeId", targetNodeId} }, root))
|
||||||
|
{
|
||||||
|
QJsonObject result = root.value("result").toObject();
|
||||||
|
QJsonArray attributes = result.value("attributes").toArray();
|
||||||
|
for (size_t i = 0; i < attributes.size(); i++)
|
||||||
|
{
|
||||||
|
if (attributes[i].toString() == "lang")
|
||||||
|
{
|
||||||
|
QString targetlang = attributes[i + 1].toString().mid(0, 2);
|
||||||
|
if (targetlang != S(translateTo.Copy()))
|
||||||
|
{
|
||||||
|
return { false, FormatString(L"Error: target langs do not match (%s): %s", S(targetlang), S(OuterHTML)) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get quotes back
|
||||||
|
if (checkquote)
|
||||||
|
{
|
||||||
|
OuterHTML = "\"" + OuterHTML + "\"";
|
||||||
|
}
|
||||||
|
return { true, S(OuterHTML) };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return { false, FormatString(L"Error: navigate failed! %s", TRANSLATION_ERROR) };
|
||||||
|
}
|
||||||
|
}
|
194
extensions/devtoolswrapper.cpp
Normal file
194
extensions/devtoolswrapper.cpp
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
#include "qtcommon.h"
|
||||||
|
#include "extension.h"
|
||||||
|
#include "blockmarkup.h"
|
||||||
|
#include "network.h"
|
||||||
|
#include <map>
|
||||||
|
#include <fstream>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include "devtools.h"
|
||||||
|
|
||||||
|
extern const char* NATIVE_LANGUAGE;
|
||||||
|
extern const char* TRANSLATE_TO;
|
||||||
|
extern const char* TRANSLATE_SELECTED_THREAD_ONLY;
|
||||||
|
extern const char* USE_TRANS_CACHE;
|
||||||
|
extern const char* MAX_SENTENCE_SIZE;
|
||||||
|
extern const char* TRANSLATION_PROVIDER;
|
||||||
|
extern QStringList languages;
|
||||||
|
const char* PATH_TO_CHROME = u8"Path to chrome";
|
||||||
|
const char* AUTO_START_CHROME = u8"Start chrome automatically";
|
||||||
|
const char* HEADLESS_CHROME = u8"Start in headless mode";
|
||||||
|
const char* CHROME_DEBUG_PORT = u8"Chrome debug port";
|
||||||
|
const char* DEV_TOOLS_STATUS = u8"Status: ";
|
||||||
|
const char* START_DEV_TOOLS_BUTTON = u8"Start";
|
||||||
|
const char* START_DEV_TOOLS = u8"Start chrome";
|
||||||
|
const char* STOP_DEV_TOOLS_BUTTON = u8"Stop";
|
||||||
|
const char* STOP_DEV_TOOLS = u8"Stop chrome";
|
||||||
|
|
||||||
|
extern bool useCache, autostartchrome, headlesschrome;
|
||||||
|
extern int maxSentenceSize, chromeport;
|
||||||
|
|
||||||
|
std::pair<bool, std::wstring> Translate(const std::wstring& text, DevTools* devtools);
|
||||||
|
|
||||||
|
const char* LANGUAGE = u8"Language";
|
||||||
|
const std::string TRANSLATION_CACHE_FILE = FormatString("%s Cache.txt", TRANSLATION_PROVIDER);
|
||||||
|
|
||||||
|
QFormLayout* display;
|
||||||
|
QSettings settings = openSettings();
|
||||||
|
Synchronized<std::wstring> translateTo = L"en";
|
||||||
|
Synchronized<std::map<std::wstring, std::wstring>> translationCache;
|
||||||
|
|
||||||
|
int savedSize;
|
||||||
|
DevTools* devtools = nullptr;
|
||||||
|
std::wstring pathtochrome = L"";
|
||||||
|
|
||||||
|
void SaveCache()
|
||||||
|
{
|
||||||
|
std::wstring allTranslations(L"\xfeff");
|
||||||
|
for (const auto& [sentence, translation] : translationCache.Acquire().contents)
|
||||||
|
allTranslations.append(L"|SENTENCE|").append(sentence).append(L"|TRANSLATION|").append(translation).append(L"|END|\r\n");
|
||||||
|
std::ofstream(TRANSLATION_CACHE_FILE, std::ios::binary | std::ios::trunc).write((const char*)allTranslations.c_str(), allTranslations.size() * sizeof(wchar_t));
|
||||||
|
savedSize = translationCache->size();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Window : public QDialog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Window() :
|
||||||
|
QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||||
|
{
|
||||||
|
display = new QFormLayout(this);
|
||||||
|
settings.beginGroup(TRANSLATION_PROVIDER);
|
||||||
|
|
||||||
|
auto languageBox = new QComboBox(this);
|
||||||
|
languageBox->addItems(languages);
|
||||||
|
int language = -1;
|
||||||
|
if (settings.contains(LANGUAGE)) language = languageBox->findText(settings.value(LANGUAGE).toString(), Qt::MatchEndsWith);
|
||||||
|
if (language < 0) language = languageBox->findText(NATIVE_LANGUAGE, Qt::MatchStartsWith);
|
||||||
|
if (language < 0) language = languageBox->findText("English", Qt::MatchStartsWith);
|
||||||
|
languageBox->setCurrentIndex(language);
|
||||||
|
saveLanguage(languageBox->currentText());
|
||||||
|
display->addRow(TRANSLATE_TO, languageBox);
|
||||||
|
connect(languageBox, &QComboBox::currentTextChanged, this, &Window::saveLanguage);
|
||||||
|
for (auto [value, label] : Array<bool&, const char*>{
|
||||||
|
{ useCache, USE_TRANS_CACHE },
|
||||||
|
{ autostartchrome, AUTO_START_CHROME },
|
||||||
|
//{ headlesschrome, HEADLESS_CHROME }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
value = settings.value(label, value).toBool();
|
||||||
|
auto checkBox = new QCheckBox(this);
|
||||||
|
checkBox->setChecked(value);
|
||||||
|
display->addRow(label, checkBox);
|
||||||
|
connect(checkBox, &QCheckBox::clicked, [label, &value](bool checked) { settings.setValue(label, value = checked); });
|
||||||
|
}
|
||||||
|
for (auto [value, label] : Array<int&, const char*>{
|
||||||
|
{ maxSentenceSize, MAX_SENTENCE_SIZE },
|
||||||
|
{ chromeport, CHROME_DEBUG_PORT },
|
||||||
|
})
|
||||||
|
{
|
||||||
|
value = settings.value(label, value).toInt();
|
||||||
|
auto spinBox = new QSpinBox(this);
|
||||||
|
spinBox->setRange(0, INT_MAX);
|
||||||
|
spinBox->setValue(value);
|
||||||
|
display->addRow(label, spinBox);
|
||||||
|
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), [label, &value](int newValue) { settings.setValue(label, value = newValue); });
|
||||||
|
}
|
||||||
|
|
||||||
|
auto keyInput = new QLineEdit(settings.value(PATH_TO_CHROME).toString());
|
||||||
|
pathtochrome = (S(keyInput->text()));
|
||||||
|
if (pathtochrome.empty())
|
||||||
|
{
|
||||||
|
for (auto defaultpath : Array<std::wstring>{
|
||||||
|
{ L"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" },
|
||||||
|
{ L"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" },
|
||||||
|
})
|
||||||
|
if (std::filesystem::exists(defaultpath))
|
||||||
|
{
|
||||||
|
pathtochrome = defaultpath;
|
||||||
|
keyInput->setText(S(pathtochrome));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connect(keyInput, &QLineEdit::textChanged, [keyInput](QString key) { settings.setValue(PATH_TO_CHROME, S(pathtochrome = (S(key)))); });
|
||||||
|
display->addRow(PATH_TO_CHROME, keyInput);
|
||||||
|
|
||||||
|
connect(&startButton, &QPushButton::clicked, this, &Window::start);
|
||||||
|
connect(&stopButton, &QPushButton::clicked, this, &Window::stop);
|
||||||
|
display->addRow(START_DEV_TOOLS, &startButton);
|
||||||
|
display->addRow(STOP_DEV_TOOLS, &stopButton);
|
||||||
|
|
||||||
|
status.setFrameStyle(QFrame::Panel | QFrame::Sunken);
|
||||||
|
display->addRow(DEV_TOOLS_STATUS, &status);
|
||||||
|
|
||||||
|
setWindowTitle(TRANSLATION_PROVIDER);
|
||||||
|
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||||
|
|
||||||
|
std::ifstream stream(TRANSLATION_CACHE_FILE, std::ios::binary);
|
||||||
|
BlockMarkupIterator savedTranslations(stream, Array<std::wstring_view>{ L"|SENTENCE|", L"|TRANSLATION|" });
|
||||||
|
auto translationCache = ::translationCache.Acquire();
|
||||||
|
|
||||||
|
while (auto read = savedTranslations.Next())
|
||||||
|
{
|
||||||
|
auto& [sentence, translation] = read.value();
|
||||||
|
translationCache->try_emplace(std::move(sentence), std::move(translation));
|
||||||
|
}
|
||||||
|
savedSize = translationCache->size();
|
||||||
|
|
||||||
|
devtools = new DevTools(this);
|
||||||
|
connect(devtools, &DevTools::statusChanged, [this](QString text)
|
||||||
|
{
|
||||||
|
status.setText(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (autostartchrome)
|
||||||
|
QMetaObject::invokeMethod(this, &Window::start, Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
~Window()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
if (devtools != nullptr)
|
||||||
|
delete devtools;
|
||||||
|
SaveCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void start()
|
||||||
|
{
|
||||||
|
if (devtools->getStatus() == "Stopped")
|
||||||
|
devtools->startDevTools(S(pathtochrome), headlesschrome, chromeport);
|
||||||
|
}
|
||||||
|
void stop()
|
||||||
|
{
|
||||||
|
devtools->closeDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void saveLanguage(QString language)
|
||||||
|
{
|
||||||
|
settings.setValue(LANGUAGE, S(translateTo->assign(S(language.split(": ")[1]))));
|
||||||
|
}
|
||||||
|
QPushButton startButton{ START_DEV_TOOLS_BUTTON, this };
|
||||||
|
QPushButton stopButton{ STOP_DEV_TOOLS_BUTTON, this };
|
||||||
|
QLabel status{ "Stopped" };
|
||||||
|
} window;
|
||||||
|
|
||||||
|
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||||
|
{
|
||||||
|
if (sentenceInfo["text number"] == 0 || sentence.size() > maxSentenceSize) return false;
|
||||||
|
|
||||||
|
bool cache = false;
|
||||||
|
std::wstring translation;
|
||||||
|
if (useCache)
|
||||||
|
{
|
||||||
|
auto translationCache = ::translationCache.Acquire();
|
||||||
|
if (auto it = translationCache->find(sentence); it != translationCache->end()) translation = it->second + L"\x200b";
|
||||||
|
}
|
||||||
|
if (translation.empty() && (sentenceInfo["current select"]))
|
||||||
|
std::tie(cache, translation) = Translate(sentence, devtools);
|
||||||
|
if (cache) translationCache->try_emplace(sentence, translation);
|
||||||
|
if (cache && translationCache->size() > savedSize + 50) SaveCache();
|
||||||
|
|
||||||
|
JSON::Unescape(translation);
|
||||||
|
sentence += L"\n" + translation;
|
||||||
|
return true;
|
||||||
|
}
|
@ -10,13 +10,14 @@ HttpRequest::HttpRequest(
|
|||||||
const wchar_t* referrer,
|
const wchar_t* referrer,
|
||||||
DWORD requestFlags,
|
DWORD requestFlags,
|
||||||
const wchar_t* httpVersion,
|
const wchar_t* httpVersion,
|
||||||
const wchar_t** acceptTypes
|
const wchar_t** acceptTypes,
|
||||||
|
DWORD port
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
static std::atomic<HINTERNET> internet = NULL;
|
static std::atomic<HINTERNET> internet = NULL;
|
||||||
if (!internet) internet = WinHttpOpen(agentName, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0);
|
if (!internet) internet = WinHttpOpen(agentName, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0);
|
||||||
if (internet)
|
if (internet)
|
||||||
if (InternetHandle connection = WinHttpConnect(internet, serverName, INTERNET_DEFAULT_PORT, 0))
|
if (InternetHandle connection = WinHttpConnect(internet, serverName, port, 0))
|
||||||
if (InternetHandle request = WinHttpOpenRequest(connection, action, objectName, httpVersion, referrer, acceptTypes, requestFlags))
|
if (InternetHandle request = WinHttpOpenRequest(connection, action, objectName, httpVersion, referrer, acceptTypes, requestFlags))
|
||||||
if (WinHttpSendRequest(request, headers, -1UL, body.empty() ? NULL : body.data(), body.size(), body.size(), NULL))
|
if (WinHttpSendRequest(request, headers, -1UL, body.empty() ? NULL : body.data(), body.size(), body.size(), NULL))
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,8 @@ struct HttpRequest
|
|||||||
const wchar_t* referrer = NULL,
|
const wchar_t* referrer = NULL,
|
||||||
DWORD requestFlags = WINHTTP_FLAG_SECURE | WINHTTP_FLAG_ESCAPE_DISABLE,
|
DWORD requestFlags = WINHTTP_FLAG_SECURE | WINHTTP_FLAG_ESCAPE_DISABLE,
|
||||||
const wchar_t* httpVersion = NULL,
|
const wchar_t* httpVersion = NULL,
|
||||||
const wchar_t** acceptTypes = NULL
|
const wchar_t** acceptTypes = NULL,
|
||||||
|
DWORD port = INTERNET_DEFAULT_PORT
|
||||||
);
|
);
|
||||||
operator bool() { return errorCode == ERROR_SUCCESS; }
|
operator bool() { return errorCode == ERROR_SUCCESS; }
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user