mirror of
https://github.com/HIllya51/LunaHook.git
synced 2024-11-26 23:34:01 +08:00
exts
This commit is contained in:
parent
e9229a0a73
commit
792041595c
@ -33,15 +33,15 @@ if(NOT DEFINED LANGUAGE)
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED WINXP)
|
||||
set(WINXP "")
|
||||
set(WINXPAPP "")
|
||||
else()
|
||||
set(WINXP "_winxp")
|
||||
set(WINXPAPP "_winxp")
|
||||
endif()
|
||||
|
||||
add_definitions(-DLANGUAGE=${LANGUAGE})
|
||||
|
||||
set(CMAKE_FINAL_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_x${bitappendix}_${LANGUAGE}${WINXP})
|
||||
set(binary_out_putpath ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_${LANGUAGE}${WINXP})
|
||||
set(CMAKE_FINAL_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_x${bitappendix}_${LANGUAGE}${WINXPAPP})
|
||||
set(binary_out_putpath ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_${LANGUAGE}${WINXPAPP})
|
||||
#set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY $<1:${CMAKE_FINAL_OUTPUT_DIRECTORY}>)
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY $<1:${binary_out_putpath}>)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $<1:${binary_out_putpath}>)
|
||||
@ -55,7 +55,7 @@ include(generate_product_version)
|
||||
|
||||
set(VERSION_MAJOR 3)
|
||||
set(VERSION_MINOR 0)
|
||||
set(VERSION_PATCH 1)
|
||||
set(VERSION_PATCH 2)
|
||||
set(VERSION_REVISION 0)
|
||||
|
||||
add_subdirectory(include)
|
||||
|
@ -9,9 +9,9 @@ else()
|
||||
set(collector "enginecollection32.cpp")
|
||||
endif()
|
||||
string(REPLACE ";" ".cpp;${enginepath}/" enginessrc "${enginessrc}")
|
||||
message("${enginessrc}")
|
||||
#message("${enginessrc}")
|
||||
set(enginessrc "${enginepath}/${enginessrc}.cpp")
|
||||
message("${enginessrc}")
|
||||
#message("${enginessrc}")
|
||||
set_source_files_properties(${enginessrc} PROPERTIES SOURCE_ENCODING "UTF-8")
|
||||
|
||||
set(texthook_src
|
||||
|
@ -1,4 +1,4 @@
|
||||
add_executable(LunaHost WIN32 confighelper.cpp controls.cpp main.cpp processlistwindow.cpp LunaHost.cpp window.cpp luna.rc pluginmanager.cpp Plugin/pluginexample.cpp QtLoader_inline.cpp app.manifest ${versioninfohost})
|
||||
add_executable(LunaHost WIN32 confighelper.cpp controls.cpp main.cpp processlistwindow.cpp LunaHost.cpp window.cpp luna.rc pluginmanager.cpp Plugin/extensionimpl.cpp Plugin/copyclipboard.cpp QtLoader_inline.cpp app.manifest ${versioninfohost})
|
||||
target_precompile_headers(LunaHost REUSE_FROM pch)
|
||||
set_target_properties(LunaHost PROPERTIES OUTPUT_NAME "LunaHost${bitappendix}")
|
||||
target_link_libraries(LunaHost comctl32 winhttp version pch host ${YY_Thunks_for_WinXP})
|
||||
|
@ -1,9 +1,3 @@
|
||||
if(0) #仅作为参考范例,实际上已经链接到exe中
|
||||
add_library(ToClipboard MODULE pluginexample.cpp)
|
||||
target_precompile_headers(ToClipboard REUSE_FROM pch)
|
||||
set_target_properties(ToClipboard PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/plugin${bitappendix}")
|
||||
endif()
|
||||
|
||||
if(0)
|
||||
include(QtUtils.cmake)
|
||||
msvc_registry_search()
|
||||
@ -17,3 +11,12 @@ if(Qt5_DIR)
|
||||
set_target_properties(QtLoader PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/plugin${bitappendix}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED WINXP)
|
||||
include(QtUtils.cmake)
|
||||
msvc_registry_search()
|
||||
if(Qt5_DIR)
|
||||
find_qt5(Core Widgets WebSockets)
|
||||
add_subdirectory(extensions)
|
||||
endif()
|
||||
endif()
|
11
LunaHost/GUI/Plugin/copyclipboard.cpp
Normal file
11
LunaHost/GUI/Plugin/copyclipboard.cpp
Normal file
@ -0,0 +1,11 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool sendclipboarddata(const std::wstring&text,HWND hwnd);
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["current select"] && sentenceInfo["process id"] != 0 &&sentenceInfo["toclipboard"])
|
||||
{
|
||||
sendclipboarddata(sentence,(HWND)sentenceInfo["HostHWND"]);
|
||||
}
|
||||
return false;
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
#include<Windows.h>
|
||||
#ifndef LUNA_PLUGIN_DEF_H
|
||||
#define LUNA_PLUGIN_DEF_H
|
||||
#pragma once
|
||||
|
||||
struct InfoForExtension
|
||||
{
|
||||
const char* name;
|
||||
@ -17,6 +16,6 @@ struct SentenceInfo
|
||||
return *(int*)0xDEAD = 0; // gives better error message than alternatives
|
||||
}
|
||||
};
|
||||
typedef wchar_t* (*OnNewSentence_t)(wchar_t*, const InfoForExtension*);
|
||||
|
||||
#endif
|
||||
struct SKIP {};
|
||||
inline void Skip() { throw SKIP(); }
|
33
LunaHost/GUI/Plugin/extensionimpl.cpp
Normal file
33
LunaHost/GUI/Plugin/extensionimpl.cpp
Normal file
@ -0,0 +1,33 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo);
|
||||
|
||||
/*
|
||||
You shouldn't mess with this or even look at it unless you're certain you know what you're doing.
|
||||
Param sentence: pointer to sentence received by Textractor (UTF-16).
|
||||
This can be modified. Textractor uses the modified sentence for future processing and display. If empty (starts with null terminator), Textractor will destroy it.
|
||||
Textractor will display the sentence after all extensions have had a chance to process and/or modify it.
|
||||
The buffer is allocated using HeapAlloc(). If you want to make it larger, please use HeapReAlloc().
|
||||
Param sentenceInfo: pointer to array containing misc info about the sentence. End of array is marked with name being nullptr.
|
||||
Return value: the buffer used for the sentence. Remember to return a new pointer if HeapReAlloc() gave you one.
|
||||
This function may be run concurrently with itself: please make sure it's thread safe.
|
||||
It will not be run concurrently with DllMain.
|
||||
*/
|
||||
extern "C" __declspec(dllexport) wchar_t* OnNewSentence(wchar_t* sentence, const InfoForExtension* sentenceInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::wstring sentenceCopy(sentence);
|
||||
int oldSize = sentenceCopy.size();
|
||||
if (ProcessSentence(sentenceCopy, SentenceInfo{ sentenceInfo }))
|
||||
{
|
||||
if (sentenceCopy.size() > oldSize) sentence = (wchar_t*)HeapReAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sentence, (sentenceCopy.size() + 1) * sizeof(wchar_t));
|
||||
wcscpy_s(sentence, sentenceCopy.size() + 1, sentenceCopy.c_str());
|
||||
}
|
||||
}
|
||||
catch (SKIP)
|
||||
{
|
||||
*sentence = L'\0';
|
||||
}
|
||||
return sentence;
|
||||
}
|
52
LunaHost/GUI/Plugin/extensions/CMakeLists.txt
Normal file
52
LunaHost/GUI/Plugin/extensions/CMakeLists.txt
Normal file
@ -0,0 +1,52 @@
|
||||
cmake_policy(SET CMP0037 OLD)
|
||||
|
||||
include_directories(../)
|
||||
add_library(extpch text.cpp)
|
||||
target_precompile_headers(extpch PUBLIC extpch.h)
|
||||
|
||||
set(disttarget "${CMAKE_SOURCE_DIR}/builds/plugin${bitappendix}")
|
||||
message(${disttarget})
|
||||
function(add_library_and_link_target TARGET_NAME)
|
||||
add_library(${TARGET_NAME} MODULE ${ARGN} ../extensionimpl.cpp)
|
||||
target_precompile_headers(${TARGET_NAME} REUSE_FROM extpch)
|
||||
target_link_libraries(${TARGET_NAME} PRIVATE extpch shell32 winhttp Qt5::Widgets Qt5::WebSockets)
|
||||
set_target_properties(${TARGET_NAME} PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY ${disttarget}
|
||||
LIBRARY_OUTPUT_DIRECTORY_DEBUG ${disttarget}
|
||||
LIBRARY_OUTPUT_DIRECTORY_RELEASE ${disttarget}
|
||||
)
|
||||
|
||||
|
||||
endfunction()
|
||||
|
||||
add_library_and_link_target(Bing\ Translate bingtranslate.cpp translatewrapper.cpp network.cpp)
|
||||
#add_library_and_link_target(Copy\ to\ Clipboard copyclipboard.cpp)
|
||||
add_library_and_link_target(DeepL\ Translate deepltranslate.cpp translatewrapper.cpp network.cpp)
|
||||
|
||||
|
||||
add_library_and_link_target(DevTools\ DeepL\ Translate devtoolsdeepltranslate.cpp devtools.cpp translatewrapper.cpp network.cpp)
|
||||
add_library_and_link_target(DevTools\ Papago\ Translate devtoolspapagotranslate.cpp devtools.cpp translatewrapper.cpp network.cpp)
|
||||
add_library_and_link_target(DevTools\ Systran\ Translate devtoolssystrantranslate.cpp devtools.cpp translatewrapper.cpp network.cpp)
|
||||
add_library_and_link_target(Extra\ Newlines extranewlines.cpp)
|
||||
add_library_and_link_target(Extra\ Window extrawindow.cpp)
|
||||
add_library_and_link_target(Google\ Translate googletranslate.cpp translatewrapper.cpp network.cpp)
|
||||
add_library_and_link_target(Regex\ Filter regexfilter.cpp)
|
||||
add_library_and_link_target(Regex\ Replacer regexreplacer.cpp)
|
||||
add_library_and_link_target(Remove\ Repeated\ Characters removerepeatchar.cpp)
|
||||
add_library_and_link_target(Remove\ Repeated\ Phrases removerepeatphrase.cpp)
|
||||
add_library_and_link_target(Remove\ Repeated\ Phrases\ 2 removerepeatphrase2.cpp)
|
||||
add_library_and_link_target(Remove\ 30\ Repeated\ Sentences removerepeatsentence.cpp)
|
||||
add_library_and_link_target(Replacer replacer.cpp)
|
||||
add_library_and_link_target(Styler styler.cpp)
|
||||
add_library_and_link_target(Thread\ Linker threadlinker.cpp)
|
||||
|
||||
|
||||
|
||||
if (NOT EXISTS ${disttarget}/Qt5WebSockets.dll AND NOT EXISTS ${disttarget}/Qt5WebSocketsd.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 ${disttarget} "${disttarget}/DevTools\ DeepL\ Translate.dll"
|
||||
)
|
||||
endif()
|
241
LunaHost/GUI/Plugin/extensions/bingtranslate.cpp
Normal file
241
LunaHost/GUI/Plugin/extensions/bingtranslate.cpp
Normal file
@ -0,0 +1,241 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "network.h"
|
||||
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "Bing Translate";
|
||||
const char* GET_API_KEY_FROM = "https://www.microsoft.com/en-us/translator/business/trial/#get-started";
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Afrikaans",
|
||||
"Albanian",
|
||||
"Amharic",
|
||||
"Arabic",
|
||||
"Armenian",
|
||||
"Assamese",
|
||||
"Azerbaijani",
|
||||
"Bangla",
|
||||
"Bosnian (Latin)",
|
||||
"Bulgarian",
|
||||
"Cantonese (Traditional)",
|
||||
"Catalan",
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"Croatian",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dari",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Fijian",
|
||||
"Filipino",
|
||||
"Finnish",
|
||||
"French",
|
||||
"French (Canada)",
|
||||
"German",
|
||||
"Greek",
|
||||
"Gujarati",
|
||||
"Haitian Creole",
|
||||
"Hebrew",
|
||||
"Hindi",
|
||||
"Hmong Daw",
|
||||
"Hungarian",
|
||||
"Icelandic",
|
||||
"Indonesian",
|
||||
"Inuktitut",
|
||||
"Irish",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Kannada",
|
||||
"Kazakh",
|
||||
"Khmer",
|
||||
"Klingon",
|
||||
"Korean",
|
||||
"Kurdish (Central)",
|
||||
"Kurdish (Northern)",
|
||||
"Lao",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Malagasy",
|
||||
"Malay",
|
||||
"Malayalam",
|
||||
"Maltese",
|
||||
"Maori",
|
||||
"Marathi",
|
||||
"Myanmar",
|
||||
"Nepali",
|
||||
"Norwegian",
|
||||
"Odia",
|
||||
"Pashto",
|
||||
"Persian",
|
||||
"Polish",
|
||||
"Portuguese (Brazil)",
|
||||
"Portuguese (Portugal)",
|
||||
"Punjabi",
|
||||
"Queretaro Otomi",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Samoan",
|
||||
"Serbian (Cyrillic)",
|
||||
"Serbian (Latin)",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swahili",
|
||||
"Swedish",
|
||||
"Tahitian",
|
||||
"Tamil",
|
||||
"Telugu",
|
||||
"Thai",
|
||||
"Tigrinya",
|
||||
"Tongan",
|
||||
"Turkish",
|
||||
"Ukrainian",
|
||||
"Urdu",
|
||||
"Vietnamese",
|
||||
"Welsh",
|
||||
"Yucatec Maya"
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Afrikaans" }, { L"af" } },
|
||||
{ { L"Albanian" }, { L"sq" } },
|
||||
{ { L"Amharic" }, { L"am" } },
|
||||
{ { L"Arabic" }, { L"ar" } },
|
||||
{ { L"Armenian" }, { L"hy" } },
|
||||
{ { L"Assamese" }, { L"as" } },
|
||||
{ { L"Azerbaijani" }, { L"az" } },
|
||||
{ { L"Bangla" }, { L"bn" } },
|
||||
{ { L"Bosnian (Latin)" }, { L"bs" } },
|
||||
{ { L"Bulgarian" }, { L"bg" } },
|
||||
{ { L"Cantonese (Traditional)" }, { L"yue" } },
|
||||
{ { L"Catalan" }, { L"ca" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"zh-Hans" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zh-Hant" } },
|
||||
{ { L"Croatian" }, { L"hr" } },
|
||||
{ { L"Czech" }, { L"cs" } },
|
||||
{ { L"Danish" }, { L"da" } },
|
||||
{ { L"Dari" }, { L"prs" } },
|
||||
{ { L"Dutch" }, { L"nl" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"Estonian" }, { L"et" } },
|
||||
{ { L"Fijian" }, { L"fj" } },
|
||||
{ { L"Filipino" }, { L"fil" } },
|
||||
{ { L"Finnish" }, { L"fi" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"French (Canada)" }, { L"fr-ca" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Greek" }, { L"el" } },
|
||||
{ { L"Gujarati" }, { L"gu" } },
|
||||
{ { L"Haitian Creole" }, { L"ht" } },
|
||||
{ { L"Hebrew" }, { L"he" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Hmong Daw" }, { L"mww" } },
|
||||
{ { L"Hungarian" }, { L"hu" } },
|
||||
{ { L"Icelandic" }, { L"is" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Inuktitut" }, { L"iu" } },
|
||||
{ { L"Irish" }, { L"ga" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Kannada" }, { L"kn" } },
|
||||
{ { L"Kazakh" }, { L"kk" } },
|
||||
{ { L"Khmer" }, { L"km" } },
|
||||
{ { L"Klingon" }, { L"tlh-Latn" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Kurdish (Central)" }, { L"ku" } },
|
||||
{ { L"Kurdish (Northern)" }, { L"kmr" } },
|
||||
{ { L"Lao" }, { L"lo" } },
|
||||
{ { L"Latvian" }, { L"lv" } },
|
||||
{ { L"Lithuanian" }, { L"lt" } },
|
||||
{ { L"Malagasy" }, { L"mg" } },
|
||||
{ { L"Malay" }, { L"ms" } },
|
||||
{ { L"Malayalam" }, { L"ml" } },
|
||||
{ { L"Maltese" }, { L"mt" } },
|
||||
{ { L"Maori" }, { L"mi" } },
|
||||
{ { L"Marathi" }, { L"mr" } },
|
||||
{ { L"Myanmar" }, { L"my" } },
|
||||
{ { L"Nepali" }, { L"ne" } },
|
||||
{ { L"Norwegian" }, { L"nb" } },
|
||||
{ { L"Odia" }, { L"or" } },
|
||||
{ { L"Pashto" }, { L"ps" } },
|
||||
{ { L"Persian" }, { L"fa" } },
|
||||
{ { L"Polish" }, { L"pl" } },
|
||||
{ { L"Portuguese (Brazil)" }, { L"pt" } },
|
||||
{ { L"Portuguese (Portugal)" }, { L"pt-pt" } },
|
||||
{ { L"Punjabi" }, { L"pa" } },
|
||||
{ { L"Queretaro Otomi" }, { L"otq" } },
|
||||
{ { L"Romanian" }, { L"ro" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Samoan" }, { L"sm" } },
|
||||
{ { L"Serbian (Cyrillic)" }, { L"sr-Cyrl" } },
|
||||
{ { L"Serbian (Latin)" }, { L"sr-Latn" } },
|
||||
{ { L"Slovak" }, { L"sk" } },
|
||||
{ { L"Slovenian" }, { L"sl" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Swahili" }, { L"sw" } },
|
||||
{ { L"Swedish" }, { L"sv" } },
|
||||
{ { L"Tahitian" }, { L"ty" } },
|
||||
{ { L"Tamil" }, { L"ta" } },
|
||||
{ { L"Telugu" }, { L"te" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Tigrinya" }, { L"ti" } },
|
||||
{ { L"Tongan" }, { L"to" } },
|
||||
{ { L"Turkish" }, { L"tr" } },
|
||||
{ { L"Ukrainian" }, { L"uk" } },
|
||||
{ { L"Urdu" }, { L"ur" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"Welsh" }, { L"cy" } },
|
||||
{ { L"Yucatec Maya" }, { L"yua" } },
|
||||
{ { L"?" }, { L"auto-detect" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = false, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 1000;
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!tlp.authKey.empty())
|
||||
{
|
||||
std::wstring translateFromComponent = tlp.translateFrom == L"?" ? L"" : L"&from=" + codes.at(tlp.translateFrom);
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
L"api.cognitive.microsofttranslator.com",
|
||||
L"POST",
|
||||
FormatString(L"/translate?api-version=3.0&to=%s%s", codes.at(tlp.translateTo), translateFromComponent).c_str(),
|
||||
FormatString(R"([{"text":"%s"}])", JSON::Escape(WideStringToString(text))),
|
||||
FormatString(L"Content-Type: application/json; charset=UTF-8\r\nOcp-Apim-Subscription-Key:%s", tlp.authKey).c_str()
|
||||
})
|
||||
if (auto translation = Copy(JSON::Parse(httpRequest.response)[0][L"translations"][0][L"text"].String())) return { true, translation.value() };
|
||||
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
|
||||
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
|
||||
}
|
||||
|
||||
static std::atomic<int> i = 0;
|
||||
static Synchronized<std::wstring> token;
|
||||
if (token->empty()) if (HttpRequest httpRequest{ L"Mozilla/5.0 Textractor", L"www.bing.com", L"GET", L"translator" })
|
||||
{
|
||||
std::wstring tokenBuilder;
|
||||
if (auto tokenPos = httpRequest.response.find(L"[" + std::to_wstring(time(nullptr) / 100)); tokenPos != std::string::npos)
|
||||
tokenBuilder = FormatString(L"&key=%s&token=%s", httpRequest.response.substr(tokenPos + 1, 13), httpRequest.response.substr(tokenPos + 16, 32));
|
||||
if (auto tokenPos = httpRequest.response.find(L"IG:\""); tokenPos != std::string::npos)
|
||||
tokenBuilder += L"&IG=" + httpRequest.response.substr(tokenPos + 4, 32);
|
||||
if (auto tokenPos = httpRequest.response.find(L"data-iid=\""); tokenPos != std::string::npos)
|
||||
tokenBuilder += L"&IID=" + httpRequest.response.substr(tokenPos + 10, 15);
|
||||
if (!tokenBuilder.empty()) token->assign(tokenBuilder);
|
||||
else return { false, FormatString(L"%s: %s\ntoken not found", TRANSLATION_ERROR, httpRequest.response) };
|
||||
}
|
||||
else return { false, FormatString(L"%s: could not acquire token", TRANSLATION_ERROR) };
|
||||
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
L"www.bing.com",
|
||||
L"POST",
|
||||
FormatString(L"/ttranslatev3?fromLang=%s&to=%s&text=%s%s.%d", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text), token.Copy(), i++).c_str()
|
||||
})
|
||||
if (auto translation = Copy(JSON::Parse(httpRequest.response)[0][L"translations"][0][L"text"].String())) return { true, translation.value() };
|
||||
else return { false, FormatString(L"%s (token=%s): %s", TRANSLATION_ERROR, std::exchange(token.Acquire().contents, L""), httpRequest.response) };
|
||||
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
|
||||
}
|
57
LunaHost/GUI/Plugin/extensions/blockmarkup.h
Normal file
57
LunaHost/GUI/Plugin/extensions/blockmarkup.h
Normal file
@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <istream>
|
||||
|
||||
template <typename C, int delimiterCount, int blockSize = 0x1000 / sizeof(C)> // windows file block size
|
||||
class BlockMarkupIterator
|
||||
{
|
||||
public:
|
||||
BlockMarkupIterator(const std::istream& stream, const std::basic_string_view<C>(&delimiters)[delimiterCount]) : streambuf(*stream.rdbuf())
|
||||
{
|
||||
std::copy_n(delimiters, delimiterCount, this->delimiters.begin());
|
||||
}
|
||||
|
||||
std::optional<std::array<std::basic_string<C>, delimiterCount>> Next()
|
||||
{
|
||||
std::array<std::basic_string<C>, delimiterCount> results;
|
||||
Find(delimiters[0], true);
|
||||
for (int i = 0; i < delimiterCount; ++i)
|
||||
{
|
||||
const auto delimiter = i + 1 < delimiterCount ? delimiters[i + 1] : end;
|
||||
if (auto found = Find(delimiter, false)) results[i] = std::move(found.value());
|
||||
else return {};
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<std::basic_string<C>> Find(std::basic_string_view<C> delimiter, bool discard)
|
||||
{
|
||||
for (int i = 0; ;)
|
||||
{
|
||||
int pos = buffer.find(delimiter, i);
|
||||
if (pos != std::string::npos)
|
||||
{
|
||||
auto result = !discard ? std::optional(std::basic_string(buffer.begin(), buffer.begin() + pos)) : std::nullopt;
|
||||
buffer.erase(buffer.begin(), buffer.begin() + pos + delimiter.size());
|
||||
return result;
|
||||
}
|
||||
int oldSize = buffer.size();
|
||||
buffer.resize(oldSize + blockSize);
|
||||
if (!streambuf.sgetn((char*)(buffer.data() + oldSize), blockSize * sizeof(C))) return {};
|
||||
i = max(0, oldSize - (int)delimiter.size());
|
||||
if (discard)
|
||||
{
|
||||
buffer.erase(0, i);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr C endImpl[5] = { '|', 'E', 'N', 'D', '|' };
|
||||
static constexpr std::basic_string_view<C> end{ endImpl, 5 };
|
||||
|
||||
std::basic_streambuf<char>& streambuf;
|
||||
std::basic_string<C> buffer;
|
||||
std::array<std::basic_string_view<C>, delimiterCount> delimiters;
|
||||
};
|
180
LunaHost/GUI/Plugin/extensions/deepltranslate.cpp
Normal file
180
LunaHost/GUI/Plugin/extensions/deepltranslate.cpp
Normal file
@ -0,0 +1,180 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "network.h"
|
||||
#include <random>
|
||||
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DeepL Translate";
|
||||
const char* GET_API_KEY_FROM = "https://www.deepl.com/pro.html#developer";
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Bulgarian",
|
||||
"Chinese (Simplified)",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English (American)",
|
||||
"English (British)",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese (Brazil)",
|
||||
"Portuguese (Portugal)",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish",
|
||||
"Turkish"
|
||||
},
|
||||
languagesFrom
|
||||
{
|
||||
"Bulgarian",
|
||||
"Chinese",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish",
|
||||
"Turkish"
|
||||
};
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Bulgarian" }, { L"BG" } },
|
||||
{ { L"Chinese" }, { L"ZH" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"ZH" } },
|
||||
{ { L"Czech" }, { L"CS" } },
|
||||
{ { L"Danish" }, { L"DA" } },
|
||||
{ { L"Dutch" }, { L"NL" } },
|
||||
{ { L"English" }, { L"EN" } },
|
||||
{ { L"English (American)" }, { L"EN-US" } },
|
||||
{ { L"English (British)" }, { L"EN-GB" } },
|
||||
{ { L"Estonian" }, { L"ET" } },
|
||||
{ { L"Finnish" }, { L"FI" } },
|
||||
{ { L"French" }, { L"FR" } },
|
||||
{ { L"German" }, { L"DE" } },
|
||||
{ { L"Greek" }, { L"EL" } },
|
||||
{ { L"Hungarian" }, { L"HU" } },
|
||||
{ { L"Indonesian" }, { L"ID" } },
|
||||
{ { L"Italian" }, { L"IT" } },
|
||||
{ { L"Japanese" }, { L"JA" } },
|
||||
{ { L"Latvian" }, { L"LV" } },
|
||||
{ { L"Lithuanian" }, { L"LT" } },
|
||||
{ { L"Polish" }, { L"PL" } },
|
||||
{ { L"Portuguese" }, { L"PT" } },
|
||||
{ { L"Portuguese (Brazil)" }, { L"PT-BR" } },
|
||||
{ { L"Portuguese (Portugal)" }, { L"PT-PT" } },
|
||||
{ { L"Romanian" }, { L"RO" } },
|
||||
{ { L"Russian" }, { L"RU" } },
|
||||
{ { L"Slovak" }, { L"SK" } },
|
||||
{ { L"Slovenian" }, { L"SL" } },
|
||||
{ { L"Spanish" }, { L"ES" } },
|
||||
{ { L"Swedish" }, { L"SV" } },
|
||||
{ { L"Turkish" }, { L"TR" } },
|
||||
{ { L"?" }, { L"auto" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = true, useCache = true, useFilter = true;
|
||||
int tokenCount = 10, rateLimitTimespan = 60000, maxSentenceSize = 1000;
|
||||
|
||||
enum KeyType { CAT, REST };
|
||||
int keyType = REST;
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!tlp.authKey.empty())
|
||||
{
|
||||
std::string translateFromComponent = tlp.translateFrom == L"?" ? "" : "&source_lang=" + WideStringToString(codes.at(tlp.translateFrom));
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
tlp.authKey.find(L":fx") == std::string::npos ? L"api.deepl.com" : L"api-free.deepl.com",
|
||||
L"POST",
|
||||
keyType == CAT ? L"/v1/translate" : L"/v2/translate",
|
||||
FormatString("text=%S&auth_key=%S&target_lang=%S", Escape(text), tlp.authKey, codes.at(tlp.translateTo)) + translateFromComponent,
|
||||
L"Content-Type: application/x-www-form-urlencoded"
|
||||
}; httpRequest && (httpRequest.response.find(L"translations") != std::string::npos || (httpRequest = HttpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
tlp.authKey.find(L":fx") == std::string::npos ? L"api.deepl.com" : L"api-free.deepl.com",
|
||||
L"POST",
|
||||
(keyType = !keyType) == CAT ? L"/v1/translate" : L"/v2/translate",
|
||||
FormatString("text=%S&auth_key=%S&target_lang=%S", Escape(text), tlp.authKey, codes.at(tlp.translateTo)) + translateFromComponent,
|
||||
L"Content-Type: application/x-www-form-urlencoded"
|
||||
})))
|
||||
// Response formatted as JSON: translation starts with text":" and ends with "}]
|
||||
if (auto translation = Copy(JSON::Parse(httpRequest.response)[L"translations"][0][L"text"].String())) return { true, translation.value() };
|
||||
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
|
||||
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
|
||||
}
|
||||
|
||||
// the following code was reverse engineered from the DeepL website; it's as close as I could make it but I'm not sure what parts of this could be removed and still have it work
|
||||
int id = 10000 * std::uniform_int_distribution(0, 9999)(std::random_device()) + 1;
|
||||
int64_t r = _time64(nullptr), n = std::count(text.begin(), text.end(), L'i') + 1;
|
||||
// user_preferred_langs? what should priority be? does timestamp do anything? other translation quality options?
|
||||
auto body = FormatString(R"(
|
||||
{
|
||||
"id": %d,
|
||||
"jsonrpc": "2.0",
|
||||
"method": "LMT_handle_jobs",
|
||||
"params": {
|
||||
"priority": -1,
|
||||
"timestamp": %lld,
|
||||
"lang": {
|
||||
"target_lang": "%.2S",
|
||||
"source_lang_user_selected": "%S"
|
||||
},
|
||||
"jobs": [{
|
||||
"raw_en_sentence": "%s",
|
||||
"raw_en_context_before": [],
|
||||
"kind": "default",
|
||||
"preferred_num_beams": 1,
|
||||
"quality": "fast",
|
||||
"raw_en_context_after": []
|
||||
}]
|
||||
}
|
||||
}
|
||||
)", id, r + (n - r % n), codes.at(tlp.translateTo), codes.at(tlp.translateFrom), JSON::Escape(WideStringToString(text)));
|
||||
// missing accept-encoding header since it fucks up HttpRequest
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
L"www2.deepl.com",
|
||||
L"POST",
|
||||
L"/jsonrpc",
|
||||
body,
|
||||
L"Host: www2.deepl.com\r\nAccept-Language: en-US,en;q=0.5\r\nContent-type: application/json; charset=utf-8\r\nOrigin: https://www.deepl.com\r\nTE: Trailers",
|
||||
INTERNET_DEFAULT_PORT,
|
||||
L"https://www.deepl.com/translator",
|
||||
WINHTTP_FLAG_SECURE
|
||||
})
|
||||
if (auto translation = Copy(JSON::Parse(httpRequest.response)[L"result"][L"translations"][0][L"beams"][0][L"postprocessed_sentence"].String())) return { true, translation.value() };
|
||||
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
|
||||
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
|
||||
}
|
173
LunaHost/GUI/Plugin/extensions/devtools.cpp
Normal file
173
LunaHost/GUI/Plugin/extensions/devtools.cpp
Normal file
@ -0,0 +1,173 @@
|
||||
#include "devtools.h"
|
||||
#include <ppltasks.h>
|
||||
#include <ShlObj.h>
|
||||
#include <QWebSocket>
|
||||
#include <QMetaEnum>
|
||||
#include <QFileDialog>
|
||||
#include <QMouseEvent>
|
||||
|
||||
extern const char* CHROME_LOCATION;
|
||||
extern const char* START_DEVTOOLS;
|
||||
extern const char* STOP_DEVTOOLS;
|
||||
extern const char* HIDE_CHROME;
|
||||
extern const char* DEVTOOLS_STATUS;
|
||||
extern const char* AUTO_START;
|
||||
|
||||
extern const char* TRANSLATION_PROVIDER;
|
||||
|
||||
extern QFormLayout* display;
|
||||
extern Settings settings;
|
||||
|
||||
namespace
|
||||
{
|
||||
QLabel* statusLabel;
|
||||
AutoHandle<> process = NULL;
|
||||
QWebSocket webSocket;
|
||||
std::atomic<int> idCounter = 0;
|
||||
Synchronized<std::unordered_map<int, concurrency::task_completion_event<JSON::Value<wchar_t>>>> mapQueue;
|
||||
|
||||
void StatusChanged(QString status)
|
||||
{
|
||||
QMetaObject::invokeMethod(statusLabel, std::bind(&QLabel::setText, statusLabel, status));
|
||||
}
|
||||
void Start(std::wstring chromePath, bool headless)
|
||||
{
|
||||
if (process) DevTools::Close();
|
||||
|
||||
auto args = FormatString(
|
||||
L"%s --proxy-server=direct:// --disable-extensions --disable-gpu --no-first-run --user-data-dir=\"%s\\devtoolscache\" --remote-debugging-port=9222",
|
||||
chromePath,
|
||||
std::filesystem::current_path().wstring()
|
||||
);
|
||||
args += headless ? L" --window-size=1920,1080 --headless" : L" --window-size=850,900";
|
||||
DWORD exitCode = 0;
|
||||
STARTUPINFOW DUMMY = { sizeof(DUMMY) };
|
||||
PROCESS_INFORMATION processInfo = {};
|
||||
if (!CreateProcessW(NULL, args.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &DUMMY, &processInfo)) return StatusChanged("StartupFailed");
|
||||
CloseHandle(processInfo.hThread);
|
||||
process = processInfo.hProcess;
|
||||
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
L"127.0.0.1",
|
||||
L"POST",
|
||||
L"/json/list",
|
||||
"",
|
||||
NULL,
|
||||
9222,
|
||||
NULL,
|
||||
WINHTTP_FLAG_ESCAPE_DISABLE
|
||||
})
|
||||
if (auto list = Copy(JSON::Parse(httpRequest.response).Array())) if (auto it = std::find_if(
|
||||
list->begin(),
|
||||
list->end(),
|
||||
[](const JSON::Value<wchar_t>& object) { return object[L"type"].String() && *object[L"type"].String() == L"page" && object[L"webSocketDebuggerUrl"].String(); }
|
||||
); it != list->end()) return webSocket.open(S(*(*it)[L"webSocketDebuggerUrl"].String()));
|
||||
|
||||
StatusChanged("ConnectingFailed");
|
||||
}
|
||||
|
||||
auto _ = ([]
|
||||
{
|
||||
QObject::connect(&webSocket, &QWebSocket::stateChanged,
|
||||
[](QAbstractSocket::SocketState state) { StatusChanged(QMetaEnum::fromType<QAbstractSocket::SocketState>().valueToKey(state)); });
|
||||
QObject::connect(&webSocket, &QWebSocket::textMessageReceived, [](QString message)
|
||||
{
|
||||
auto result = JSON::Parse(S(message));
|
||||
auto mapQueue = ::mapQueue.Acquire();
|
||||
if (auto id = result[L"id"].Number()) if (auto request = mapQueue->find((int)*id); request != mapQueue->end())
|
||||
{
|
||||
request->second.set(result);
|
||||
mapQueue->erase(request);
|
||||
}
|
||||
});
|
||||
}(), 0);
|
||||
}
|
||||
|
||||
namespace DevTools
|
||||
{
|
||||
void Initialize()
|
||||
{
|
||||
QString chromePath = settings.value(CHROME_LOCATION).toString();
|
||||
if (chromePath.isEmpty())
|
||||
{
|
||||
for (auto [_, process] : GetAllProcesses())
|
||||
if (process && (process->find(L"\\chrome.exe") != std::string::npos || process->find(L"\\msedge.exe") != std::string::npos)) chromePath = S(process.value());
|
||||
wchar_t programFiles[MAX_PATH + 100] = {};
|
||||
for (auto folder : { CSIDL_PROGRAM_FILESX86, CSIDL_PROGRAM_FILES, CSIDL_LOCAL_APPDATA })
|
||||
{
|
||||
SHGetFolderPathW(NULL, folder, NULL, SHGFP_TYPE_CURRENT, programFiles);
|
||||
wcscat_s(programFiles, L"/Google/Chrome/Application/chrome.exe");
|
||||
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, "/", "Google 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 headlessCheck = new QCheckBox();
|
||||
auto startButton = new QPushButton(START_DEVTOOLS), stopButton = new QPushButton(STOP_DEVTOOLS);
|
||||
headlessCheck->setChecked(settings.value(HIDE_CHROME, true).toBool());
|
||||
QObject::connect(headlessCheck, &QCheckBox::clicked, [](bool headless) { settings.setValue(HIDE_CHROME, headless); });
|
||||
QObject::connect(startButton, &QPushButton::clicked, [chromePathEdit, headlessCheck] { Start(S(chromePathEdit->text()), headlessCheck->isChecked()); });
|
||||
QObject::connect(stopButton, &QPushButton::clicked, &Close);
|
||||
auto buttons = new QHBoxLayout();
|
||||
buttons->addWidget(startButton);
|
||||
buttons->addWidget(stopButton);
|
||||
display->addRow(HIDE_CHROME, headlessCheck);
|
||||
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 = new QLabel("Stopped");
|
||||
statusLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);
|
||||
display->addRow(DEVTOOLS_STATUS, statusLabel);
|
||||
if (autoStartCheck->isChecked()) QMetaObject::invokeMethod(startButton, &QPushButton::click, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void Close()
|
||||
{
|
||||
webSocket.close();
|
||||
for (const auto& [_, task] : mapQueue.Acquire().contents) task.set_exception(std::runtime_error("closed"));
|
||||
mapQueue->clear();
|
||||
|
||||
if (process)
|
||||
{
|
||||
TerminateProcess(process, 0);
|
||||
WaitForSingleObject(process, 1000);
|
||||
for (int retry = 0; ++retry < 20; Sleep(100))
|
||||
try { std::filesystem::remove_all(L"devtoolscache"); break; }
|
||||
catch (std::filesystem::filesystem_error) { continue; }
|
||||
}
|
||||
process = NULL;
|
||||
StatusChanged("Stopped");
|
||||
}
|
||||
|
||||
bool Connected()
|
||||
{
|
||||
return webSocket.state() == QAbstractSocket::ConnectedState;
|
||||
}
|
||||
|
||||
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;
|
||||
if (!Connected()) return {};
|
||||
mapQueue->try_emplace(id, response);
|
||||
QMetaObject::invokeMethod(&webSocket, std::bind(&QWebSocket::sendTextMessage, &webSocket, S(FormatString(LR"({"id":%d,"method":"%S","params":%s})", id, method, params))));
|
||||
try { if (auto result = create_task(response).get()[L"result"]) return result; } catch (...) {}
|
||||
return {};
|
||||
}
|
||||
}
|
10
LunaHost/GUI/Plugin/extensions/devtools.h
Normal file
10
LunaHost/GUI/Plugin/extensions/devtools.h
Normal file
@ -0,0 +1,10 @@
|
||||
#include "qtcommon.h"
|
||||
#include "network.h"
|
||||
|
||||
namespace DevTools
|
||||
{
|
||||
void Initialize();
|
||||
void Close();
|
||||
bool Connected();
|
||||
JSON::Value<wchar_t> SendRequest(const char* method, const std::wstring& params = L"{}");
|
||||
}
|
148
LunaHost/GUI/Plugin/extensions/devtoolsdeepltranslate.cpp
Normal file
148
LunaHost/GUI/Plugin/extensions/devtoolsdeepltranslate.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "devtools.h"
|
||||
|
||||
extern const wchar_t* ERROR_START_CHROME;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DevTools DeepL Translate";
|
||||
const char* GET_API_KEY_FROM = nullptr;
|
||||
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Bulgarian",
|
||||
"Chinese (Simplified)",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English (American)",
|
||||
"English (British)",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Portuguese (Brazilian)",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish"
|
||||
},
|
||||
languagesFrom =
|
||||
{
|
||||
"Bulgarian",
|
||||
"Chinese",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish"
|
||||
};
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Bulgarian" }, { L"Bulgarian" } },
|
||||
{ { L"Chinese" }, { L"Chinese" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"Chinese (simplified)" } },
|
||||
{ { L"Czech" }, { L"Czech" } },
|
||||
{ { L"Danish" }, { L"Danish" } },
|
||||
{ { L"Dutch" }, { L"Dutch" } },
|
||||
{ { L"English" }, { L"English" } },
|
||||
{ { L"English (American)" }, { L"English (American)" } },
|
||||
{ { L"English (British)" }, { L"English (British)" } },
|
||||
{ { L"Estonian" }, { L"Estonian" } },
|
||||
{ { L"Finnish" }, { L"Finnish" } },
|
||||
{ { L"French" }, { L"French" } },
|
||||
{ { L"German" }, { L"German" } },
|
||||
{ { L"Greek" }, { L"Greek" } },
|
||||
{ { L"Hungarian" }, { L"Hungarian" } },
|
||||
{ { L"Italian" }, { L"Italian" } },
|
||||
{ { L"Japanese" }, { L"Japanese" } },
|
||||
{ { L"Latvian" }, { L"Latvian" } },
|
||||
{ { L"Lithuanian" }, { L"Lithuanian" } },
|
||||
{ { L"Polish" }, { L"Polish" } },
|
||||
{ { L"Portuguese" }, { L"Portuguese" } },
|
||||
{ { L"Portuguese (Brazilian)" }, { L"Portuguese (Brazilian)" } },
|
||||
{ { L"Romanian" }, { L"Romanian" } },
|
||||
{ { L"Russian" }, { L"Russian" } },
|
||||
{ { L"Slovak" }, { L"Slovak" } },
|
||||
{ { L"Slovenian" }, { L"Slovenian" } },
|
||||
{ { L"Spanish" }, { L"Spanish" } },
|
||||
{ { L"Swedish" }, { L"Swedish" } },
|
||||
{ { L"?" }, { L"Detect language" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
DevTools::Initialize();
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
DevTools::Close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
|
||||
// DevTools can't handle concurrent translations yet
|
||||
static std::mutex translationMutex;
|
||||
std::scoped_lock lock(translationMutex);
|
||||
std::wstring escaped; // DeepL breaks with slash in input
|
||||
for (auto ch : text) ch == '/' ? escaped += L"\\/" : escaped += ch;
|
||||
DevTools::SendRequest("Page.navigate", FormatString(LR"({"url":"https://www.deepl.com/en/translator#en/en/%s"})", Escape(escaped)));
|
||||
for (int retry = 0; ++retry < 20; Sleep(100))
|
||||
if (Copy(DevTools::SendRequest("Runtime.evaluate", LR"({"expression":"document.readyState"})")[L"result"][L"value"].String()) == L"complete") break;
|
||||
|
||||
DevTools::SendRequest("Runtime.evaluate", FormatString(LR"({"expression":"
|
||||
document.querySelector('.lmt__language_select--source').querySelector('button').click();
|
||||
document.evaluate(`//*[text()='%s']`,document.querySelector('.lmt__language_select__menu'),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue.click();
|
||||
document.querySelector('.lmt__language_select--target').querySelector('button').click();
|
||||
document.evaluate(`//*[text()='%s']`,document.querySelector('.lmt__language_select__menu'),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue.click();
|
||||
"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo)));
|
||||
|
||||
for (int retry = 0; ++retry < 100; Sleep(100))
|
||||
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()) };
|
||||
return { false, TRANSLATION_ERROR };
|
||||
}
|
82
LunaHost/GUI/Plugin/extensions/devtoolspapagotranslate.cpp
Normal file
82
LunaHost/GUI/Plugin/extensions/devtoolspapagotranslate.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "devtools.h"
|
||||
|
||||
extern const wchar_t* ERROR_START_CHROME;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DevTools Papago Translate";
|
||||
const char* GET_API_KEY_FROM = nullptr;
|
||||
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"English",
|
||||
"French",
|
||||
"German",
|
||||
"Hindi",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Korean",
|
||||
"Portuguese",
|
||||
"Russian",
|
||||
"Spanish",
|
||||
"Thai",
|
||||
"Vietnamese",
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Chinese (Simplified)" }, { L"zh-CN" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zt-TW" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Portuguese" }, { L"pt" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"?" }, { L"auto" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
DevTools::Initialize();
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
DevTools::Close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
|
||||
// DevTools can't handle concurrent translations yet
|
||||
static std::mutex translationMutex;
|
||||
std::scoped_lock lock(translationMutex);
|
||||
DevTools::SendRequest("Page.navigate", FormatString(LR"({"url":"https://papago.naver.com/?sk=%s&tk=%s&st=%s"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text)));
|
||||
for (int retry = 0; ++retry < 100; Sleep(100))
|
||||
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('#txtTarget').textContent.trim() ","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
|
||||
return { false, TRANSLATION_ERROR };
|
||||
}
|
152
LunaHost/GUI/Plugin/extensions/devtoolssystrantranslate.cpp
Normal file
152
LunaHost/GUI/Plugin/extensions/devtoolssystrantranslate.cpp
Normal file
@ -0,0 +1,152 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "devtools.h"
|
||||
|
||||
extern const wchar_t* ERROR_START_CHROME;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DevTools Systran Translate";
|
||||
const char* GET_API_KEY_FROM = nullptr;
|
||||
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Albanian",
|
||||
"Arabic",
|
||||
"Bengali",
|
||||
"Bulgarian",
|
||||
"Burmese",
|
||||
"Catalan",
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"Croatian",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hebrew",
|
||||
"Hindi",
|
||||
"Hungarian",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Korean",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Malay",
|
||||
"Norwegian",
|
||||
"Pashto",
|
||||
"Persian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Serbian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Somali",
|
||||
"Spanish",
|
||||
"Swedish",
|
||||
"Tagalog",
|
||||
"Tamil",
|
||||
"Thai",
|
||||
"Turkish",
|
||||
"Ukrainian",
|
||||
"Urdu",
|
||||
"Vietnamese"
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Albanian" }, { L"sq" } },
|
||||
{ { L"Arabic" }, { L"ar" } },
|
||||
{ { L"Bengali" }, { L"bn" } },
|
||||
{ { L"Bulgarian" }, { L"bg" } },
|
||||
{ { L"Burmese" }, { L"my" } },
|
||||
{ { L"Catalan" }, { L"ca" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"zh" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zt" } },
|
||||
{ { L"Croatian" }, { L"hr" } },
|
||||
{ { L"Czech" }, { L"cs" } },
|
||||
{ { L"Danish" }, { L"da" } },
|
||||
{ { L"Dutch" }, { L"nl" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"Estonian" }, { L"et" } },
|
||||
{ { L"Finnish" }, { L"fi" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Greek" }, { L"el" } },
|
||||
{ { L"Hebrew" }, { L"he" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Hungarian" }, { L"hu" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Latvian" }, { L"lv" } },
|
||||
{ { L"Lithuanian" }, { L"lt" } },
|
||||
{ { L"Malay" }, { L"ms" } },
|
||||
{ { L"Norwegian" }, { L"no" } },
|
||||
{ { L"Pashto" }, { L"ps" } },
|
||||
{ { L"Persian" }, { L"fa" } },
|
||||
{ { L"Polish" }, { L"pl" } },
|
||||
{ { L"Portuguese" }, { L"pt" } },
|
||||
{ { L"Romanian" }, { L"ro" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Serbian" }, { L"sr" } },
|
||||
{ { L"Slovak" }, { L"sk" } },
|
||||
{ { L"Slovenian" }, { L"sl" } },
|
||||
{ { L"Somali" }, { L"so" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Swedish" }, { L"sv" } },
|
||||
{ { L"Tagalog" }, { L"tl" } },
|
||||
{ { L"Tamil" }, { L"ta" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Turkish" }, { L"tr" } },
|
||||
{ { L"Ukrainian" }, { L"uk" } },
|
||||
{ { L"Urdu" }, { L"ur" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"?" }, { L"autodetect" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
DevTools::Initialize();
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
DevTools::Close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
|
||||
// DevTools can't handle concurrent translations yet
|
||||
static std::mutex translationMutex;
|
||||
std::scoped_lock lock(translationMutex);
|
||||
|
||||
DevTools::SendRequest(
|
||||
"Page.navigate",
|
||||
FormatString(LR"({"url":"https://translate.systran.net/?source=%s&target=%s&input=%s"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text))
|
||||
);
|
||||
for (int retry = 0; ++retry < 100; Sleep(100))
|
||||
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('#outputEditor').textContent.trim() ","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
|
||||
return { false, TRANSLATION_ERROR };
|
||||
}
|
191
LunaHost/GUI/Plugin/extensions/extpch.h
Normal file
191
LunaHost/GUI/Plugin/extensions/extpch.h
Normal file
@ -0,0 +1,191 @@
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <concrt.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <Psapi.h>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include <regex>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <locale>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <utility>
|
||||
#include <cassert>
|
||||
#include <variant>
|
||||
|
||||
|
||||
template <typename T, typename... Xs> struct ArrayImpl { using Type = std::tuple<T, Xs...>[]; };
|
||||
template <typename T> struct ArrayImpl<T> { using Type = T[]; };
|
||||
template <typename... Ts> using Array = typename ArrayImpl<Ts...>::Type;
|
||||
|
||||
template <auto F> using Functor = std::integral_constant<std::remove_reference_t<decltype(F)>, F>; // shouldn't need remove_reference_t but MSVC is bugged
|
||||
|
||||
struct PermissivePointer
|
||||
{
|
||||
template <typename T> operator T*() { return (T*)p; }
|
||||
void* p;
|
||||
};
|
||||
|
||||
template <typename HandleCloser = Functor<CloseHandle>>
|
||||
class AutoHandle
|
||||
{
|
||||
public:
|
||||
AutoHandle(HANDLE h) : h(h) {}
|
||||
operator HANDLE() { return h.get(); }
|
||||
PHANDLE operator&() { static_assert(sizeof(*this) == sizeof(HANDLE)); assert(!h); return (PHANDLE)this; }
|
||||
operator bool() { return h.get() != NULL && h.get() != INVALID_HANDLE_VALUE; }
|
||||
|
||||
private:
|
||||
struct HandleCleaner { void operator()(void* h) { if (h != INVALID_HANDLE_VALUE) HandleCloser()(PermissivePointer{ h }); } };
|
||||
std::unique_ptr<void, HandleCleaner> h;
|
||||
};
|
||||
|
||||
template<typename T, typename M = std::mutex>
|
||||
class Synchronized
|
||||
{
|
||||
public:
|
||||
template <typename... Args>
|
||||
Synchronized(Args&&... args) : contents(std::forward<Args>(args)...) {}
|
||||
|
||||
struct Locker
|
||||
{
|
||||
T* operator->() { return &contents; }
|
||||
std::unique_lock<M> lock;
|
||||
T& contents;
|
||||
};
|
||||
|
||||
Locker Acquire() { return { std::unique_lock(m), contents }; }
|
||||
Locker operator->() { return Acquire(); }
|
||||
T Copy() { return Acquire().contents; }
|
||||
|
||||
private:
|
||||
T contents;
|
||||
M m;
|
||||
};
|
||||
|
||||
template <typename F>
|
||||
void SpawnThread(const F& f) // works in DllMain unlike std thread
|
||||
{
|
||||
F* copy = new F(f);
|
||||
CloseHandle(CreateThread(nullptr, 0, [](void* copy)
|
||||
{
|
||||
(*(F*)copy)();
|
||||
delete (F*)copy;
|
||||
return 0UL;
|
||||
}, copy, 0, nullptr));
|
||||
}
|
||||
|
||||
inline struct
|
||||
{
|
||||
inline static BYTE DUMMY[100];
|
||||
template <typename T> operator T*() { static_assert(sizeof(T) < sizeof(DUMMY)); return (T*)DUMMY; }
|
||||
} DUMMY;
|
||||
|
||||
inline auto Swallow = [](auto&&...) {};
|
||||
|
||||
template <typename T> std::optional<std::remove_cv_t<T>> Copy(T* ptr) { if (ptr) return *ptr; return {}; }
|
||||
|
||||
template <typename T> inline auto FormatArg(T arg) { return arg; }
|
||||
template <typename C> inline auto FormatArg(const std::basic_string<C>& arg) { return arg.c_str(); }
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable: 4996)
|
||||
template <typename... Args>
|
||||
inline std::string FormatString(const char* format, const Args&... args)
|
||||
{
|
||||
std::string buffer(snprintf(nullptr, 0, format, FormatArg(args)...), '\0');
|
||||
sprintf(buffer.data(), format, FormatArg(args)...);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
inline std::wstring FormatString(const wchar_t* format, const Args&... args)
|
||||
{
|
||||
std::wstring buffer(_snwprintf(nullptr, 0, format, FormatArg(args)...), L'\0');
|
||||
_swprintf(buffer.data(), format, FormatArg(args)...);
|
||||
return buffer;
|
||||
}
|
||||
#pragma warning(pop)
|
||||
|
||||
inline void 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());
|
||||
}
|
||||
|
||||
inline std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding)
|
||||
{
|
||||
std::vector<wchar_t> buffer(text.size() + 1);
|
||||
if (int length = MultiByteToWideChar(encoding, 0, text.c_str(), text.size() + 1, buffer.data(), buffer.size()))
|
||||
return std::wstring(buffer.data(), length - 1);
|
||||
return {};
|
||||
}
|
||||
|
||||
inline std::wstring StringToWideString(const std::string& text)
|
||||
{
|
||||
std::vector<wchar_t> buffer(text.size() + 1);
|
||||
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, buffer.data(), buffer.size());
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
inline std::string WideStringToString(const std::wstring& text)
|
||||
{
|
||||
std::vector<char> buffer((text.size() + 1) * 4);
|
||||
WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, buffer.data(), buffer.size(), nullptr, nullptr);
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
inline void TEXTRACTOR_MESSAGE(const wchar_t* format, const Args&... args) { MessageBoxW(NULL, FormatString(format, args...).c_str(), L"Textractor", MB_OK); }
|
||||
|
||||
template <typename... Args>
|
||||
inline void TEXTRACTOR_DEBUG(const wchar_t* format, const Args&... args) { SpawnThread([=] { TEXTRACTOR_MESSAGE(format, args...); }); }
|
||||
|
||||
void Localize();
|
||||
|
||||
#ifdef _DEBUG
|
||||
#define TEST(...) static auto _ = CreateThread(nullptr, 0, [](auto) { __VA_ARGS__; return 0UL; }, NULL, 0, nullptr)
|
||||
#else
|
||||
#define TEST(...)
|
||||
#endif
|
||||
|
||||
inline std::optional<std::wstring> GetModuleFilename(DWORD processId, HMODULE module = NULL)
|
||||
{
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
if (AutoHandle<> process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, processId))
|
||||
if (GetModuleFileNameExW(process, module, buffer.data(), MAX_PATH)) return buffer.data();
|
||||
return {};
|
||||
}
|
||||
|
||||
inline std::optional<std::wstring> GetModuleFilename(HMODULE module = NULL)
|
||||
{
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
if (GetModuleFileNameW(module, buffer.data(), MAX_PATH)) return buffer.data();
|
||||
return {};
|
||||
}
|
||||
|
||||
inline std::vector<std::pair<DWORD, std::optional<std::wstring>>> GetAllProcesses()
|
||||
{
|
||||
std::vector<DWORD> processIds(10000);
|
||||
DWORD spaceUsed = 0;
|
||||
EnumProcesses(processIds.data(), 10000 * sizeof(DWORD), &spaceUsed);
|
||||
std::vector<std::pair<DWORD, std::optional<std::wstring>>> processes;
|
||||
for (int i = 0; i < spaceUsed / sizeof(DWORD); ++i) processes.push_back({ processIds[i], GetModuleFilename(processIds[i]) });
|
||||
return processes;
|
||||
}
|
8
LunaHost/GUI/Plugin/extensions/extranewlines.cpp
Normal file
8
LunaHost/GUI/Plugin/extensions/extranewlines.cpp
Normal file
@ -0,0 +1,8 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
sentence += L"\n";
|
||||
return true;
|
||||
}
|
606
LunaHost/GUI/Plugin/extensions/extrawindow.cpp
Normal file
606
LunaHost/GUI/Plugin/extensions/extrawindow.cpp
Normal file
@ -0,0 +1,606 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "ui_extrawindow.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <fstream>
|
||||
#include <process.h>
|
||||
#include <QRegularExpression>
|
||||
#include <QColorDialog>
|
||||
#include <QFontDialog>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QGraphicsEffect>
|
||||
#include <QFontMetrics>
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QScrollArea>
|
||||
#include <QAbstractNativeEventFilter>
|
||||
|
||||
extern const char* EXTRA_WINDOW_INFO;
|
||||
extern const char* TOPMOST;
|
||||
extern const char* OPACITY;
|
||||
extern const char* SHOW_ORIGINAL;
|
||||
extern const char* ORIGINAL_AFTER_TRANSLATION;
|
||||
extern const char* SIZE_LOCK;
|
||||
extern const char* POSITION_LOCK;
|
||||
extern const char* CENTERED_TEXT;
|
||||
extern const char* AUTO_RESIZE_WINDOW_HEIGHT;
|
||||
extern const char* CLICK_THROUGH;
|
||||
extern const char* HIDE_MOUSEOVER;
|
||||
extern const char* DICTIONARY;
|
||||
extern const char* DICTIONARY_INSTRUCTIONS;
|
||||
extern const char* BG_COLOR;
|
||||
extern const char* TEXT_COLOR;
|
||||
extern const char* TEXT_OUTLINE;
|
||||
extern const char* OUTLINE_COLOR;
|
||||
extern const char* OUTLINE_SIZE;
|
||||
extern const char* OUTLINE_SIZE_INFO;
|
||||
extern const char* FONT;
|
||||
|
||||
constexpr auto DICTIONARY_SAVE_FILE = u8"SavedDictionary.txt";
|
||||
constexpr int CLICK_THROUGH_HOTKEY = 0xc0d0;
|
||||
|
||||
QColor colorPrompt(QWidget* parent, QColor default, const QString& title, bool customOpacity = true)
|
||||
{
|
||||
QColor color = QColorDialog::getColor(default, parent, title);
|
||||
if (customOpacity) color.setAlpha(255 * QInputDialog::getDouble(parent, title, OPACITY, default.alpha() / 255.0, 0, 1, 3, nullptr, Qt::WindowCloseButtonHint));
|
||||
return color;
|
||||
}
|
||||
|
||||
struct PrettyWindow : QDialog, Localizer
|
||||
{
|
||||
PrettyWindow(const char* name)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
ui.display->setGraphicsEffect(outliner = new Outliner);
|
||||
setWindowFlags(Qt::FramelessWindowHint);
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
|
||||
settings.beginGroup(name);
|
||||
QFont font = ui.display->font();
|
||||
if (font.fromString(settings.value(FONT, font.toString()).toString())) ui.display->setFont(font);
|
||||
SetBackgroundColor(settings.value(BG_COLOR, backgroundColor).value<QColor>());
|
||||
SetTextColor(settings.value(TEXT_COLOR, TextColor()).value<QColor>());
|
||||
outliner->color = settings.value(OUTLINE_COLOR, outliner->color).value<QColor>();
|
||||
outliner->size = settings.value(OUTLINE_SIZE, outliner->size).toDouble();
|
||||
autoHide = settings.value(HIDE_MOUSEOVER, autoHide).toBool();
|
||||
menu.addAction(FONT, this, &PrettyWindow::RequestFont);
|
||||
menu.addAction(BG_COLOR, [this] { SetBackgroundColor(colorPrompt(this, backgroundColor, BG_COLOR)); });
|
||||
menu.addAction(TEXT_COLOR, [this] { SetTextColor(colorPrompt(this, TextColor(), TEXT_COLOR)); });
|
||||
QAction* outlineAction = menu.addAction(TEXT_OUTLINE, this, &PrettyWindow::SetOutline);
|
||||
outlineAction->setCheckable(true);
|
||||
outlineAction->setChecked(outliner->size >= 0);
|
||||
QAction* autoHideAction = menu.addAction(HIDE_MOUSEOVER, this, [this](bool autoHide) { settings.setValue(HIDE_MOUSEOVER, this->autoHide = autoHide); });
|
||||
autoHideAction->setCheckable(true);
|
||||
autoHideAction->setChecked(autoHide);
|
||||
connect(this, &QDialog::customContextMenuRequested, [this](QPoint point) { menu.exec(mapToGlobal(point)); });
|
||||
connect(ui.display, &QLabel::customContextMenuRequested, [this](QPoint point) { menu.exec(ui.display->mapToGlobal(point)); });
|
||||
startTimer(50);
|
||||
}
|
||||
|
||||
~PrettyWindow()
|
||||
{
|
||||
settings.sync();
|
||||
}
|
||||
|
||||
Ui::ExtraWindow ui;
|
||||
|
||||
protected:
|
||||
void timerEvent(QTimerEvent*) override
|
||||
{
|
||||
if (autoHide && geometry().contains(QCursor::pos()))
|
||||
{
|
||||
if (!hidden)
|
||||
{
|
||||
if (backgroundColor.alphaF() > 0.05) backgroundColor.setAlphaF(0.05);
|
||||
if (outliner->color.alphaF() > 0.05) outliner->color.setAlphaF(0.05);
|
||||
QColor hiddenTextColor = TextColor();
|
||||
if (hiddenTextColor.alphaF() > 0.05) hiddenTextColor.setAlphaF(0.05);
|
||||
ui.display->setPalette(QPalette(hiddenTextColor, {}, {}, {}, {}, {}, {}));
|
||||
hidden = true;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
else if (hidden)
|
||||
{
|
||||
backgroundColor.setAlpha(settings.value(BG_COLOR).value<QColor>().alpha());
|
||||
outliner->color.setAlpha(settings.value(OUTLINE_COLOR).value<QColor>().alpha());
|
||||
ui.display->setPalette(QPalette(settings.value(TEXT_COLOR).value<QColor>(), {}, {}, {}, {}, {}, {}));
|
||||
hidden = false;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
QMenu menu{ ui.display };
|
||||
Settings settings{ this };
|
||||
|
||||
private:
|
||||
void RequestFont()
|
||||
{
|
||||
if (QFont font = QFontDialog::getFont(&ok, ui.display->font(), this, FONT); ok)
|
||||
{
|
||||
settings.setValue(FONT, font.toString());
|
||||
ui.display->setFont(font);
|
||||
}
|
||||
};
|
||||
|
||||
void SetBackgroundColor(QColor color)
|
||||
{
|
||||
if (!color.isValid()) return;
|
||||
if (color.alpha() == 0) color.setAlpha(1);
|
||||
backgroundColor = color;
|
||||
repaint();
|
||||
settings.setValue(BG_COLOR, color.name(QColor::HexArgb));
|
||||
};
|
||||
|
||||
QColor TextColor()
|
||||
{
|
||||
return ui.display->palette().color(QPalette::WindowText);
|
||||
}
|
||||
|
||||
void SetTextColor(QColor color)
|
||||
{
|
||||
if (!color.isValid()) return;
|
||||
ui.display->setPalette(QPalette(color, {}, {}, {}, {}, {}, {}));
|
||||
settings.setValue(TEXT_COLOR, color.name(QColor::HexArgb));
|
||||
};
|
||||
|
||||
void SetOutline(bool enable)
|
||||
{
|
||||
if (enable)
|
||||
{
|
||||
QColor color = colorPrompt(this, outliner->color, OUTLINE_COLOR);
|
||||
if (color.isValid()) outliner->color = color;
|
||||
outliner->size = QInputDialog::getDouble(this, OUTLINE_SIZE, OUTLINE_SIZE_INFO, -outliner->size, 0, INT_MAX, 2, nullptr, Qt::WindowCloseButtonHint);
|
||||
}
|
||||
else outliner->size = -outliner->size;
|
||||
settings.setValue(OUTLINE_COLOR, outliner->color.name(QColor::HexArgb));
|
||||
settings.setValue(OUTLINE_SIZE, outliner->size);
|
||||
}
|
||||
|
||||
void paintEvent(QPaintEvent*) override
|
||||
{
|
||||
QPainter(this).fillRect(rect(), backgroundColor);
|
||||
}
|
||||
|
||||
bool autoHide = false, hidden = false;
|
||||
QColor backgroundColor{ palette().window().color() };
|
||||
struct Outliner : QGraphicsEffect
|
||||
{
|
||||
void draw(QPainter* painter) override
|
||||
{
|
||||
if (size < 0) return drawSource(painter);
|
||||
QPoint offset;
|
||||
QPixmap pixmap = sourcePixmap(Qt::LogicalCoordinates, &offset);
|
||||
offset.setX(offset.x() + size);
|
||||
for (auto offset2 : Array<QPointF>{ { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 }, { 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 } })
|
||||
{
|
||||
QImage outline = pixmap.toImage();
|
||||
QPainter outlinePainter(&outline);
|
||||
outlinePainter.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
outlinePainter.fillRect(outline.rect(), color);
|
||||
painter->drawImage(offset + offset2 * size, outline);
|
||||
}
|
||||
painter->drawPixmap(offset, pixmap);
|
||||
}
|
||||
QColor color{ Qt::black };
|
||||
double size = -0.5;
|
||||
}* outliner;
|
||||
};
|
||||
|
||||
class ExtraWindow : public PrettyWindow, QAbstractNativeEventFilter
|
||||
{
|
||||
public:
|
||||
ExtraWindow() : PrettyWindow("Extra Window")
|
||||
{
|
||||
ui.display->setTextFormat(Qt::PlainText);
|
||||
if (settings.contains(WINDOW) && QApplication::screenAt(settings.value(WINDOW).toRect().bottomRight())) setGeometry(settings.value(WINDOW).toRect());
|
||||
|
||||
for (auto [name, default, slot] : Array<const char*, bool, void(ExtraWindow::*)(bool)>{
|
||||
{ TOPMOST, false, &ExtraWindow::SetTopmost },
|
||||
{ SIZE_LOCK, false, &ExtraWindow::SetSizeLock },
|
||||
{ POSITION_LOCK, false, &ExtraWindow::SetPositionLock },
|
||||
{ CENTERED_TEXT, false, &ExtraWindow::SetCenteredText },
|
||||
{ AUTO_RESIZE_WINDOW_HEIGHT, false, &ExtraWindow::SetAutoResize },
|
||||
{ SHOW_ORIGINAL, true, &ExtraWindow::SetShowOriginal },
|
||||
{ ORIGINAL_AFTER_TRANSLATION, true, &ExtraWindow::SetShowOriginalAfterTranslation },
|
||||
{ DICTIONARY, false, &ExtraWindow::SetUseDictionary },
|
||||
})
|
||||
{
|
||||
// delay processing anything until Textractor has finished initializing
|
||||
QMetaObject::invokeMethod(this, std::bind(slot, this, default = settings.value(name, default).toBool()), Qt::QueuedConnection);
|
||||
auto action = menu.addAction(name, this, slot);
|
||||
action->setCheckable(true);
|
||||
action->setChecked(default);
|
||||
}
|
||||
|
||||
menu.addAction(CLICK_THROUGH, this, &ExtraWindow::ToggleClickThrough);
|
||||
|
||||
ui.display->installEventFilter(this);
|
||||
qApp->installNativeEventFilter(this);
|
||||
|
||||
QMetaObject::invokeMethod(this, [this]
|
||||
{
|
||||
RegisterHotKey((HWND)winId(), CLICK_THROUGH_HOTKEY, MOD_ALT | MOD_NOREPEAT, 0x58);
|
||||
show();
|
||||
AddSentence(EXTRA_WINDOW_INFO);
|
||||
}, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
~ExtraWindow()
|
||||
{
|
||||
settings.setValue(WINDOW, geometry());
|
||||
}
|
||||
|
||||
void AddSentence(QString sentence)
|
||||
{
|
||||
sanitize(sentence);
|
||||
sentence.chop(std::distance(std::remove(sentence.begin(), sentence.end(), QChar::Tabulation), sentence.end()));
|
||||
sentenceHistory.push_back(sentence);
|
||||
if (sentenceHistory.size() > 1000) sentenceHistory.erase(sentenceHistory.begin());
|
||||
historyIndex = sentenceHistory.size() - 1;
|
||||
DisplaySentence();
|
||||
}
|
||||
|
||||
private:
|
||||
void DisplaySentence()
|
||||
{
|
||||
if (sentenceHistory.empty()) return;
|
||||
QString sentence = sentenceHistory[historyIndex];
|
||||
if (sentence.contains(u8"\x200b \n"))
|
||||
if (!showOriginal) sentence = sentence.split(u8"\x200b \n")[1];
|
||||
else if (showOriginalAfterTranslation) sentence = sentence.split(u8"\x200b \n")[1] + "\n" + sentence.split(u8"\x200b \n")[0];
|
||||
|
||||
if (sizeLock && !autoResize)
|
||||
{
|
||||
QFontMetrics fontMetrics(ui.display->font(), ui.display);
|
||||
int low = 0, high = sentence.size(), last = 0;
|
||||
while (low <= high)
|
||||
{
|
||||
int mid = (low + high) / 2;
|
||||
if (fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence.left(mid)).height() <= ui.display->height())
|
||||
{
|
||||
last = mid;
|
||||
low = mid + 1;
|
||||
}
|
||||
else high = mid - 1;
|
||||
}
|
||||
sentence = sentence.left(last);
|
||||
}
|
||||
|
||||
ui.display->setText(sentence);
|
||||
if (autoResize)
|
||||
resize(width(), height() - ui.display->height() +
|
||||
QFontMetrics(ui.display->font(), ui.display).boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence).height()
|
||||
);
|
||||
}
|
||||
|
||||
void SetTopmost(bool topmost)
|
||||
{
|
||||
for (auto window : { winId(), dictionaryWindow.winId() })
|
||||
SetWindowPos((HWND)window, topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
settings.setValue(TOPMOST, topmost);
|
||||
};
|
||||
|
||||
void SetPositionLock(bool locked)
|
||||
{
|
||||
settings.setValue(POSITION_LOCK, posLock = locked);
|
||||
};
|
||||
|
||||
void SetSizeLock(bool locked)
|
||||
{
|
||||
setSizeGripEnabled(!locked);
|
||||
settings.setValue(SIZE_LOCK, sizeLock = locked);
|
||||
};
|
||||
|
||||
void SetCenteredText(bool centeredText)
|
||||
{
|
||||
ui.display->setAlignment(centeredText ? Qt::AlignHCenter : Qt::AlignLeft);
|
||||
settings.setValue(CENTERED_TEXT, this->centeredText = centeredText);
|
||||
};
|
||||
|
||||
void SetAutoResize(bool autoResize)
|
||||
{
|
||||
settings.setValue(AUTO_RESIZE_WINDOW_HEIGHT, this->autoResize = autoResize);
|
||||
DisplaySentence();
|
||||
};
|
||||
|
||||
void SetShowOriginal(bool showOriginal)
|
||||
{
|
||||
settings.setValue(SHOW_ORIGINAL, this->showOriginal = showOriginal);
|
||||
DisplaySentence();
|
||||
};
|
||||
|
||||
void SetShowOriginalAfterTranslation(bool showOriginalAfterTranslation)
|
||||
{
|
||||
settings.setValue(ORIGINAL_AFTER_TRANSLATION, this->showOriginalAfterTranslation = showOriginalAfterTranslation);
|
||||
DisplaySentence();
|
||||
};
|
||||
|
||||
void SetUseDictionary(bool useDictionary)
|
||||
{
|
||||
if (useDictionary)
|
||||
{
|
||||
dictionaryWindow.UpdateDictionary();
|
||||
if (dictionaryWindow.dictionary.empty())
|
||||
{
|
||||
std::ofstream(DICTIONARY_SAVE_FILE) << u8"\ufeff" << DICTIONARY_INSTRUCTIONS;
|
||||
_spawnlp(_P_DETACH, "notepad", "notepad", DICTIONARY_SAVE_FILE, NULL); // show file to user
|
||||
}
|
||||
}
|
||||
settings.setValue(DICTIONARY, this->useDictionary = useDictionary);
|
||||
}
|
||||
|
||||
void ToggleClickThrough()
|
||||
{
|
||||
clickThrough = !clickThrough;
|
||||
for (auto window : { winId(), dictionaryWindow.winId() })
|
||||
{
|
||||
unsigned exStyle = GetWindowLongPtrW((HWND)window, GWL_EXSTYLE);
|
||||
if (clickThrough) exStyle |= WS_EX_TRANSPARENT;
|
||||
else exStyle &= ~WS_EX_TRANSPARENT;
|
||||
SetWindowLongPtrW((HWND)window, GWL_EXSTYLE, exStyle);
|
||||
}
|
||||
};
|
||||
|
||||
void ShowDictionary(QPoint mouse)
|
||||
{
|
||||
QString sentence = ui.display->text();
|
||||
const QFont& font = ui.display->font();
|
||||
if (cachedDisplayInfo.CompareExchange(ui.display))
|
||||
{
|
||||
QFontMetrics fontMetrics(font, ui.display);
|
||||
int flags = Qt::TextWordWrap | (ui.display->alignment() & (Qt::AlignLeft | Qt::AlignHCenter));
|
||||
textPositionMap.clear();
|
||||
for (int i = 0, height = 0, lineBreak = 0; i < sentence.size(); ++i)
|
||||
{
|
||||
int block = 1;
|
||||
for (int charHeight = fontMetrics.boundingRect(0, 0, 1, INT_MAX, flags, sentence.mid(i, 1)).height();
|
||||
i + block < sentence.size() && fontMetrics.boundingRect(0, 0, 1, INT_MAX, flags, sentence.mid(i, block + 1)).height() < charHeight * 1.5; ++block);
|
||||
auto boundingRect = fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, flags, sentence.left(i + block));
|
||||
if (boundingRect.height() > height)
|
||||
{
|
||||
height = boundingRect.height();
|
||||
lineBreak = i;
|
||||
}
|
||||
textPositionMap.push_back({
|
||||
fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, flags, sentence.mid(lineBreak, i - lineBreak + 1)).right() + 1,
|
||||
height
|
||||
});
|
||||
}
|
||||
}
|
||||
int i;
|
||||
for (i = 0; i < textPositionMap.size(); ++i) if (textPositionMap[i].y() > mouse.y() && textPositionMap[i].x() > mouse.x()) break;
|
||||
if (i == textPositionMap.size() || (mouse - textPositionMap[i]).manhattanLength() > font.pointSize() * 3) return dictionaryWindow.hide();
|
||||
if (sentence.mid(i) == dictionaryWindow.term) return dictionaryWindow.ShowDefinition();
|
||||
dictionaryWindow.ui.display->setFixedWidth(ui.display->width() * 3 / 4);
|
||||
dictionaryWindow.SetTerm(sentence.mid(i));
|
||||
int left = i == 0 ? 0 : textPositionMap[i - 1].x(), right = textPositionMap[i].x(),
|
||||
x = textPositionMap[i].x() > ui.display->width() / 2 ? -dictionaryWindow.width() + (right * 3 + left) / 4 : (left * 3 + right) / 4, y = 0;
|
||||
for (auto point : textPositionMap) if (point.y() > y && point.y() < textPositionMap[i].y()) y = point.y();
|
||||
dictionaryWindow.move(ui.display->mapToGlobal(QPoint(x, y - dictionaryWindow.height())));
|
||||
}
|
||||
|
||||
bool nativeEventFilter(const QByteArray&, void* message, long* result) override
|
||||
{
|
||||
auto msg = (MSG*)message;
|
||||
if (msg->message == WM_HOTKEY)
|
||||
if (msg->wParam == CLICK_THROUGH_HOTKEY) return ToggleClickThrough(), true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool eventFilter(QObject*, QEvent* event) override
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonPress) mousePressEvent((QMouseEvent*)event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void timerEvent(QTimerEvent* event) override
|
||||
{
|
||||
if (useDictionary && QCursor::pos() != oldPos && (!dictionaryWindow.isVisible() || !dictionaryWindow.geometry().contains(QCursor::pos())))
|
||||
ShowDictionary(ui.display->mapFromGlobal(QCursor::pos()));
|
||||
PrettyWindow::timerEvent(event);
|
||||
}
|
||||
|
||||
void mousePressEvent(QMouseEvent* event) override
|
||||
{
|
||||
dictionaryWindow.hide();
|
||||
oldPos = event->globalPos();
|
||||
}
|
||||
|
||||
void mouseMoveEvent(QMouseEvent* event) override
|
||||
{
|
||||
if (!posLock) move(pos() + event->globalPos() - oldPos);
|
||||
oldPos = event->globalPos();
|
||||
}
|
||||
|
||||
void wheelEvent(QWheelEvent* event) override
|
||||
{
|
||||
int scroll = event->angleDelta().y();
|
||||
if (scroll > 0 && historyIndex > 0) --historyIndex;
|
||||
if (scroll < 0 && historyIndex + 1 < sentenceHistory.size()) ++historyIndex;
|
||||
DisplaySentence();
|
||||
}
|
||||
|
||||
bool sizeLock, posLock, centeredText, autoResize, showOriginal, showOriginalAfterTranslation, useDictionary, clickThrough;
|
||||
QPoint oldPos;
|
||||
|
||||
class
|
||||
{
|
||||
public:
|
||||
bool CompareExchange(QLabel* display)
|
||||
{
|
||||
if (display->text() == text && display->font() == font && display->width() == width && display->alignment() == alignment) return false;
|
||||
text = display->text();
|
||||
font = display->font();
|
||||
width = display->width();
|
||||
alignment = display->alignment();
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
QString text;
|
||||
QFont font;
|
||||
int width;
|
||||
Qt::Alignment alignment;
|
||||
} cachedDisplayInfo;
|
||||
std::vector<QPoint> textPositionMap;
|
||||
|
||||
std::vector<QString> sentenceHistory;
|
||||
int historyIndex = 0;
|
||||
|
||||
class DictionaryWindow : public PrettyWindow
|
||||
{
|
||||
public:
|
||||
DictionaryWindow() : PrettyWindow("Dictionary Window")
|
||||
{
|
||||
ui.display->setSizePolicy({ QSizePolicy::Fixed, QSizePolicy::Minimum });
|
||||
}
|
||||
|
||||
void UpdateDictionary()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (dictionaryFileLastWrite == std::filesystem::last_write_time(DICTIONARY_SAVE_FILE)) return;
|
||||
dictionaryFileLastWrite = std::filesystem::last_write_time(DICTIONARY_SAVE_FILE);
|
||||
}
|
||||
catch (std::filesystem::filesystem_error) { return; }
|
||||
|
||||
dictionary.clear();
|
||||
charStorage.clear();
|
||||
|
||||
auto StoreCopy = [&](std::string_view string)
|
||||
{
|
||||
auto location = &*charStorage.insert(charStorage.end(), string.begin(), string.end());
|
||||
charStorage.push_back(0);
|
||||
return location;
|
||||
};
|
||||
|
||||
charStorage.reserve(std::filesystem::file_size(DICTIONARY_SAVE_FILE));
|
||||
std::ifstream stream(DICTIONARY_SAVE_FILE);
|
||||
BlockMarkupIterator savedDictionary(stream, Array<std::string_view>{ "|TERM|", "|DEFINITION|" });
|
||||
while (auto read = savedDictionary.Next())
|
||||
{
|
||||
const auto& [terms, definition] = read.value();
|
||||
auto storedDefinition = StoreCopy(definition);
|
||||
std::string_view termsView = terms;
|
||||
size_t start = 0, end = termsView.find("|TERM|");
|
||||
while (end != std::string::npos)
|
||||
{
|
||||
dictionary.push_back(DictionaryEntry{ StoreCopy(termsView.substr(start, end - start)), storedDefinition });
|
||||
start = end + 6;
|
||||
end = termsView.find("|TERM|", start);
|
||||
}
|
||||
dictionary.push_back(DictionaryEntry{ StoreCopy(termsView.substr(start)), storedDefinition });
|
||||
}
|
||||
std::stable_sort(dictionary.begin(), dictionary.end());
|
||||
|
||||
inflections.clear();
|
||||
stream.seekg(0);
|
||||
BlockMarkupIterator savedInflections(stream, Array<std::string_view>{ "|ROOT|", "|INFLECTS TO|", "|NAME|" });
|
||||
while (auto read = savedInflections.Next())
|
||||
{
|
||||
const auto& [root, inflectsTo, name] = read.value();
|
||||
if (!inflections.emplace_back(Inflection{
|
||||
S(root),
|
||||
QRegularExpression(QRegularExpression::anchoredPattern(S(inflectsTo)), QRegularExpression::UseUnicodePropertiesOption),
|
||||
S(name)
|
||||
}).inflectsTo.isValid()) TEXTRACTOR_MESSAGE(L"Invalid regex: %s", StringToWideString(inflectsTo));
|
||||
}
|
||||
}
|
||||
|
||||
void SetTerm(QString term)
|
||||
{
|
||||
this->term = term;
|
||||
UpdateDictionary();
|
||||
definitions.clear();
|
||||
definitionIndex = 0;
|
||||
std::unordered_set<const char*> foundDefinitions;
|
||||
for (term = term.left(100); !term.isEmpty(); term.chop(1))
|
||||
for (const auto& [rootTerm, definition, inflections] : LookupDefinitions(term, foundDefinitions))
|
||||
definitions.push_back(
|
||||
QStringLiteral("<h3>%1 (%5/%6)</h3><small>%2%3</small>%4").arg(
|
||||
term.split("<<")[0].toHtmlEscaped(),
|
||||
rootTerm.split("<<")[0].toHtmlEscaped(),
|
||||
inflections.join(""),
|
||||
definition
|
||||
)
|
||||
);
|
||||
for (int i = 0; i < definitions.size(); ++i) definitions[i] = definitions[i].arg(i + 1).arg(definitions.size());
|
||||
ShowDefinition();
|
||||
}
|
||||
|
||||
void ShowDefinition()
|
||||
{
|
||||
if (definitions.empty()) return hide();
|
||||
ui.display->setText(definitions[definitionIndex]);
|
||||
adjustSize();
|
||||
resize(width(), 1);
|
||||
show();
|
||||
}
|
||||
|
||||
struct DictionaryEntry
|
||||
{
|
||||
const char* term;
|
||||
const char* definition;
|
||||
bool operator<(DictionaryEntry other) const { return strcmp(term, other.term) < 0; }
|
||||
};
|
||||
std::vector<DictionaryEntry> dictionary;
|
||||
QString term;
|
||||
|
||||
private:
|
||||
struct LookupResult
|
||||
{
|
||||
QString term;
|
||||
QString definition;
|
||||
QStringList inflectionsUsed;
|
||||
};
|
||||
std::vector<LookupResult> LookupDefinitions(QString term, std::unordered_set<const char*>& foundDefinitions, QStringList inflectionsUsed = {})
|
||||
{
|
||||
std::vector<LookupResult> results;
|
||||
for (auto [it, end] = std::equal_range(dictionary.begin(), dictionary.end(), DictionaryEntry{ term.toUtf8() }); it != end; ++it)
|
||||
if (foundDefinitions.emplace(it->definition).second) results.push_back({ term, it->definition, inflectionsUsed });
|
||||
for (const auto& inflection : inflections) if (auto match = inflection.inflectsTo.match(term); match.hasMatch())
|
||||
{
|
||||
QStringList currentInflectionsUsed = inflectionsUsed;
|
||||
currentInflectionsUsed.push_front(inflection.name);
|
||||
QString root;
|
||||
for (const auto& ch : inflection.root) root += ch.isDigit() ? match.captured(ch.digitValue()) : ch;
|
||||
for (const auto& definition : LookupDefinitions(root, foundDefinitions, currentInflectionsUsed)) results.push_back(definition);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
void wheelEvent(QWheelEvent* event) override
|
||||
{
|
||||
int scroll = event->angleDelta().y();
|
||||
if (scroll > 0 && definitionIndex > 0) definitionIndex -= 1;
|
||||
if (scroll < 0 && definitionIndex + 1 < definitions.size()) definitionIndex += 1;
|
||||
int oldHeight = height();
|
||||
ShowDefinition();
|
||||
move(x(), y() + oldHeight - height());
|
||||
}
|
||||
|
||||
struct Inflection
|
||||
{
|
||||
QString root;
|
||||
QRegularExpression inflectsTo;
|
||||
QString name;
|
||||
};
|
||||
std::vector<Inflection> inflections;
|
||||
|
||||
std::filesystem::file_time_type dictionaryFileLastWrite;
|
||||
std::vector<char> charStorage;
|
||||
std::vector<QString> definitions;
|
||||
int definitionIndex;
|
||||
} dictionaryWindow;
|
||||
} extraWindow;
|
||||
#include<stdio.h>
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
wprintf(L"enter %s\n",sentence.c_str());
|
||||
if (sentenceInfo["current select"] && sentenceInfo["text number"] != 0)
|
||||
QMetaObject::invokeMethod(&extraWindow, [sentence = S(sentence)] { extraWindow.AddSentence(sentence); });
|
||||
wprintf(L"leave %s\n",sentence.c_str());
|
||||
return false;
|
||||
}
|
51
LunaHost/GUI/Plugin/extensions/extrawindow.ui
Normal file
51
LunaHost/GUI/Plugin/extensions/extrawindow.ui
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ExtraWindow</class>
|
||||
<widget class="QDialog" name="ExtraWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="display">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>16</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
267
LunaHost/GUI/Plugin/extensions/googletranslate.cpp
Normal file
267
LunaHost/GUI/Plugin/extensions/googletranslate.cpp
Normal file
@ -0,0 +1,267 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "network.h"
|
||||
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "Google Translate";
|
||||
const char* GET_API_KEY_FROM = "https://console.cloud.google.com/marketplace/product/google/translate.googleapis.com";
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Afrikaans",
|
||||
"Albanian",
|
||||
"Amharic",
|
||||
"Arabic",
|
||||
"Armenian",
|
||||
"Azerbaijani",
|
||||
"Basque",
|
||||
"Belarusian",
|
||||
"Bengali",
|
||||
"Bosnian",
|
||||
"Bulgarian",
|
||||
"Catalan",
|
||||
"Cebuano",
|
||||
"Chichewa",
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"Corsican",
|
||||
"Croatian",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Esperanto",
|
||||
"Estonian",
|
||||
"Filipino",
|
||||
"Finnish",
|
||||
"French",
|
||||
"Frisian",
|
||||
"Galician",
|
||||
"Georgian",
|
||||
"German",
|
||||
"Greek",
|
||||
"Gujarati",
|
||||
"Haitian Creole",
|
||||
"Hausa",
|
||||
"Hawaiian",
|
||||
"Hebrew",
|
||||
"Hindi",
|
||||
"Hmong",
|
||||
"Hungarian",
|
||||
"Icelandic",
|
||||
"Igbo",
|
||||
"Indonesian",
|
||||
"Irish",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Javanese",
|
||||
"Kannada",
|
||||
"Kazakh",
|
||||
"Khmer",
|
||||
"Kinyarwanda",
|
||||
"Korean",
|
||||
"Kurdish (Kurmanji)",
|
||||
"Kyrgyz",
|
||||
"Lao",
|
||||
"Latin",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Luxembourgish",
|
||||
"Macedonian",
|
||||
"Malagasy",
|
||||
"Malay",
|
||||
"Malayalam",
|
||||
"Maltese",
|
||||
"Maori",
|
||||
"Marathi",
|
||||
"Mongolian",
|
||||
"Myanmar (Burmese)",
|
||||
"Nepali",
|
||||
"Norwegian",
|
||||
"Odia (Oriya)",
|
||||
"Pashto",
|
||||
"Persian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Punjabi",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Samoan",
|
||||
"Scots Gaelic",
|
||||
"Serbian",
|
||||
"Sesotho",
|
||||
"Shona",
|
||||
"Sindhi",
|
||||
"Sinhala",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Somali",
|
||||
"Spanish",
|
||||
"Sundanese",
|
||||
"Swahili",
|
||||
"Swedish",
|
||||
"Tajik",
|
||||
"Tamil",
|
||||
"Tatar",
|
||||
"Telugu",
|
||||
"Thai",
|
||||
"Turkish",
|
||||
"Turkmen",
|
||||
"Ukrainian",
|
||||
"Urdu",
|
||||
"Uyghur",
|
||||
"Uzbek",
|
||||
"Vietnamese",
|
||||
"Welsh",
|
||||
"Xhosa",
|
||||
"Yiddish",
|
||||
"Yoruba",
|
||||
"Zulu",
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Afrikaans" }, { L"af" } },
|
||||
{ { L"Albanian" }, { L"sq" } },
|
||||
{ { L"Amharic" }, { L"am" } },
|
||||
{ { L"Arabic" }, { L"ar" } },
|
||||
{ { L"Armenian" }, { L"hy" } },
|
||||
{ { L"Azerbaijani" }, { L"az" } },
|
||||
{ { L"Basque" }, { L"eu" } },
|
||||
{ { L"Belarusian" }, { L"be" } },
|
||||
{ { L"Bengali" }, { L"bn" } },
|
||||
{ { L"Bosnian" }, { L"bs" } },
|
||||
{ { L"Bulgarian" }, { L"bg" } },
|
||||
{ { L"Catalan" }, { L"ca" } },
|
||||
{ { L"Cebuano" }, { L"ceb" } },
|
||||
{ { L"Chichewa" }, { L"ny" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"zh-CN" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zh-TW" } },
|
||||
{ { L"Corsican" }, { L"co" } },
|
||||
{ { L"Croatian" }, { L"hr" } },
|
||||
{ { L"Czech" }, { L"cs" } },
|
||||
{ { L"Danish" }, { L"da" } },
|
||||
{ { L"Dutch" }, { L"nl" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"Esperanto" }, { L"eo" } },
|
||||
{ { L"Estonian" }, { L"et" } },
|
||||
{ { L"Filipino" }, { L"tl" } },
|
||||
{ { L"Finnish" }, { L"fi" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"Frisian" }, { L"fy" } },
|
||||
{ { L"Galician" }, { L"gl" } },
|
||||
{ { L"Georgian" }, { L"ka" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Greek" }, { L"el" } },
|
||||
{ { L"Gujarati" }, { L"gu" } },
|
||||
{ { L"Haitian Creole" }, { L"ht" } },
|
||||
{ { L"Hausa" }, { L"ha" } },
|
||||
{ { L"Hawaiian" }, { L"haw" } },
|
||||
{ { L"Hebrew" }, { L"iw" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Hmong" }, { L"hmn" } },
|
||||
{ { L"Hungarian" }, { L"hu" } },
|
||||
{ { L"Icelandic" }, { L"is" } },
|
||||
{ { L"Igbo" }, { L"ig" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Irish" }, { L"ga" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Javanese" }, { L"jw" } },
|
||||
{ { L"Kannada" }, { L"kn" } },
|
||||
{ { L"Kazakh" }, { L"kk" } },
|
||||
{ { L"Khmer" }, { L"km" } },
|
||||
{ { L"Kinyarwanda" }, { L"rw" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Kurdish (Kurmanji)" }, { L"ku" } },
|
||||
{ { L"Kyrgyz" }, { L"ky" } },
|
||||
{ { L"Lao" }, { L"lo" } },
|
||||
{ { L"Latin" }, { L"la" } },
|
||||
{ { L"Latvian" }, { L"lv" } },
|
||||
{ { L"Lithuanian" }, { L"lt" } },
|
||||
{ { L"Luxembourgish" }, { L"lb" } },
|
||||
{ { L"Macedonian" }, { L"mk" } },
|
||||
{ { L"Malagasy" }, { L"mg" } },
|
||||
{ { L"Malay" }, { L"ms" } },
|
||||
{ { L"Malayalam" }, { L"ml" } },
|
||||
{ { L"Maltese" }, { L"mt" } },
|
||||
{ { L"Maori" }, { L"mi" } },
|
||||
{ { L"Marathi" }, { L"mr" } },
|
||||
{ { L"Mongolian" }, { L"mn" } },
|
||||
{ { L"Myanmar (Burmese)" }, { L"my" } },
|
||||
{ { L"Nepali" }, { L"ne" } },
|
||||
{ { L"Norwegian" }, { L"no" } },
|
||||
{ { L"Odia (Oriya)" }, { L"or" } },
|
||||
{ { L"Pashto" }, { L"ps" } },
|
||||
{ { L"Persian" }, { L"fa" } },
|
||||
{ { L"Polish" }, { L"pl" } },
|
||||
{ { L"Portuguese" }, { L"pt" } },
|
||||
{ { L"Punjabi" }, { L"pa" } },
|
||||
{ { L"Romanian" }, { L"ro" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Samoan" }, { L"sm" } },
|
||||
{ { L"Scots Gaelic" }, { L"gd" } },
|
||||
{ { L"Serbian" }, { L"sr" } },
|
||||
{ { L"Sesotho" }, { L"st" } },
|
||||
{ { L"Shona" }, { L"sn" } },
|
||||
{ { L"Sindhi" }, { L"sd" } },
|
||||
{ { L"Sinhala" }, { L"si" } },
|
||||
{ { L"Slovak" }, { L"sk" } },
|
||||
{ { L"Slovenian" }, { L"sl" } },
|
||||
{ { L"Somali" }, { L"so" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Sundanese" }, { L"su" } },
|
||||
{ { L"Swahili" }, { L"sw" } },
|
||||
{ { L"Swedish" }, { L"sv" } },
|
||||
{ { L"Tajik" }, { L"tg" } },
|
||||
{ { L"Tamil" }, { L"ta" } },
|
||||
{ { L"Tatar" }, { L"tt" } },
|
||||
{ { L"Telugu" }, { L"te" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Turkish" }, { L"tr" } },
|
||||
{ { L"Turkmen" }, { L"tk" } },
|
||||
{ { L"Ukrainian" }, { L"uk" } },
|
||||
{ { L"Urdu" }, { L"ur" } },
|
||||
{ { L"Uyghur" }, { L"ug" } },
|
||||
{ { L"Uzbek" }, { L"uz" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"Welsh" }, { L"cy" } },
|
||||
{ { L"Xhosa" }, { L"xh" } },
|
||||
{ { L"Yiddish" }, { L"yi" } },
|
||||
{ { L"Yoruba" }, { L"yo" } },
|
||||
{ { L"Zulu" }, { L"zu" } },
|
||||
{ { L"?" }, { L"auto" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = false, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 1000;
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!tlp.authKey.empty())
|
||||
{
|
||||
std::wstring translateFromComponent = tlp.translateFrom == L"?" ? L"" : L"&source=" + codes.at(tlp.translateFrom);
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
L"translation.googleapis.com",
|
||||
L"POST",
|
||||
FormatString(L"/language/translate/v2?format=text&target=%s&key=%s%s", codes.at(tlp.translateTo), tlp.authKey, translateFromComponent).c_str(),
|
||||
FormatString(R"({"q":["%s"]})", JSON::Escape(WideStringToString(text)))
|
||||
})
|
||||
if (auto translation = Copy(JSON::Parse(httpRequest.response)[L"data"][L"translations"][0][L"translatedText"].String())) return { true, translation.value() };
|
||||
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
|
||||
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
|
||||
}
|
||||
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
L"translate.google.com",
|
||||
L"GET",
|
||||
FormatString(L"/m?sl=%s&tl=%s&q=%s", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text)).c_str()
|
||||
})
|
||||
{
|
||||
auto start = httpRequest.response.find(L"result-container\">"), end = httpRequest.response.find(L'<', start);
|
||||
if (end != std::string::npos) return { true, HTML::Unescape(httpRequest.response.substr(start + 18, end - start - 18)) };
|
||||
return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
|
||||
}
|
||||
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
|
||||
}
|
65
LunaHost/GUI/Plugin/extensions/network.cpp
Normal file
65
LunaHost/GUI/Plugin/extensions/network.cpp
Normal file
@ -0,0 +1,65 @@
|
||||
#include "network.h"
|
||||
|
||||
HttpRequest::HttpRequest(
|
||||
const wchar_t* agentName,
|
||||
const wchar_t* serverName,
|
||||
const wchar_t* action,
|
||||
const wchar_t* objectName,
|
||||
std::string body,
|
||||
const wchar_t* headers,
|
||||
DWORD port,
|
||||
const wchar_t* referrer,
|
||||
DWORD requestFlags,
|
||||
const wchar_t* httpVersion,
|
||||
const wchar_t** acceptTypes
|
||||
)
|
||||
{
|
||||
static std::atomic<HINTERNET> internet = NULL;
|
||||
if (!internet) internet = WinHttpOpen(agentName, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0);
|
||||
if (internet)
|
||||
if (InternetHandle connection = WinHttpConnect(internet, serverName, port, 0))
|
||||
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))
|
||||
{
|
||||
WinHttpReceiveResponse(request, NULL);
|
||||
|
||||
//DWORD size = 0;
|
||||
//WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &size, WINHTTP_NO_HEADER_INDEX);
|
||||
//this->headers.resize(size);
|
||||
//WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, this->headers.data(), &size, WINHTTP_NO_HEADER_INDEX);
|
||||
std::string data;
|
||||
DWORD availableSize, downloadedSize;
|
||||
do
|
||||
{
|
||||
availableSize = 0;
|
||||
WinHttpQueryDataAvailable(request, &availableSize);
|
||||
if (!availableSize) break;
|
||||
std::vector<char> buffer(availableSize);
|
||||
WinHttpReadData(request, buffer.data(), availableSize, &downloadedSize);
|
||||
data.append(buffer.data(), downloadedSize);
|
||||
} while (availableSize > 0);
|
||||
response = StringToWideString(data);
|
||||
this->connection = std::move(connection);
|
||||
this->request = std::move(request);
|
||||
}
|
||||
else errorCode = GetLastError();
|
||||
else errorCode = GetLastError();
|
||||
else errorCode = GetLastError();
|
||||
else errorCode = GetLastError();
|
||||
}
|
||||
|
||||
std::wstring Escape(const std::wstring& text)
|
||||
{
|
||||
std::wstring escaped;
|
||||
for (unsigned char ch : WideStringToString(text)) escaped += FormatString(L"%%%02X", (int)ch);
|
||||
return escaped;
|
||||
}
|
||||
|
||||
std::string Escape(const std::string& text)
|
||||
{
|
||||
std::string escaped;
|
||||
for (unsigned char ch : text) escaped += FormatString("%%%02X", (int)ch);
|
||||
return escaped;
|
||||
}
|
||||
|
||||
TEST(assert(JSON::Parse<wchar_t>(LR"([{"string":"hello world","boolean":false,"number":1.67e+4,"null":null,"array":[]},"hello world"])")));
|
233
LunaHost/GUI/Plugin/extensions/network.h
Normal file
233
LunaHost/GUI/Plugin/extensions/network.h
Normal file
@ -0,0 +1,233 @@
|
||||
#pragma once
|
||||
|
||||
#include <winhttp.h>
|
||||
#include <variant>
|
||||
|
||||
using InternetHandle = AutoHandle<Functor<WinHttpCloseHandle>>;
|
||||
|
||||
struct HttpRequest
|
||||
{
|
||||
HttpRequest(
|
||||
const wchar_t* agentName,
|
||||
const wchar_t* serverName,
|
||||
const wchar_t* action,
|
||||
const wchar_t* objectName,
|
||||
std::string body = "",
|
||||
const wchar_t* headers = NULL,
|
||||
DWORD port = INTERNET_DEFAULT_PORT,
|
||||
const wchar_t* referrer = NULL,
|
||||
DWORD requestFlags = WINHTTP_FLAG_SECURE | WINHTTP_FLAG_ESCAPE_DISABLE,
|
||||
const wchar_t* httpVersion = NULL,
|
||||
const wchar_t** acceptTypes = NULL
|
||||
);
|
||||
operator bool() { return errorCode == ERROR_SUCCESS; }
|
||||
|
||||
std::wstring response;
|
||||
std::wstring headers;
|
||||
InternetHandle connection = NULL;
|
||||
InternetHandle request = NULL;
|
||||
DWORD errorCode = ERROR_SUCCESS;
|
||||
};
|
||||
|
||||
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>
|
||||
std::basic_string<C> Escape(std::basic_string<C> text)
|
||||
{
|
||||
int oldSize = text.size();
|
||||
text.resize(text.size() + std::count_if(text.begin(), text.end(), [](C ch) { return ch == '\n' || ch == '\r' || ch == '\t' || ch == '\\' || ch == '"'; }));
|
||||
auto out = text.rbegin();
|
||||
for (int i = oldSize - 1; i >= 0; --i)
|
||||
{
|
||||
if (text[i] == '\n') *out++ = 'n';
|
||||
else if (text[i] == '\t') *out++ = 't';
|
||||
else if (text[i] == '\r') *out++ = 'r';
|
||||
else if (text[i] == '\\' || text[i] == '"') *out++ = text[i];
|
||||
else
|
||||
{
|
||||
*out++ = text[i];
|
||||
continue;
|
||||
}
|
||||
*out++ = '\\';
|
||||
}
|
||||
text.erase(std::remove_if(text.begin(), text.end(), [](uint64_t ch) { return ch < 0x20 || ch == 0x7f; }), text.end());
|
||||
return text;
|
||||
}
|
||||
|
||||
template <typename C> struct UTF {};
|
||||
template <> struct UTF<wchar_t>
|
||||
{
|
||||
inline static std::wstring FromCodepoint(unsigned codepoint) { return { (wchar_t)codepoint }; } // TODO: surrogate pairs
|
||||
};
|
||||
|
||||
template <typename C>
|
||||
struct Value : private std::variant<std::monostate, std::nullptr_t, bool, double, std::basic_string<C>, std::vector<Value<C>>, std::unordered_map<std::basic_string<C>, Value<C>>>
|
||||
{
|
||||
using std::variant<std::monostate, std::nullptr_t, bool, double, std::basic_string<C>, std::vector<Value<C>>, std::unordered_map<std::basic_string<C>, Value<C>>>::variant;
|
||||
|
||||
explicit operator bool() const { return index(); }
|
||||
bool IsNull() const { return index() == 1; }
|
||||
auto Boolean() const { return std::get_if<bool>(this); }
|
||||
auto Number() const { return std::get_if<double>(this); }
|
||||
auto String() const { return std::get_if<std::basic_string<C>>(this); }
|
||||
auto Array() const { return std::get_if<std::vector<Value<C>>>(this); }
|
||||
auto Object() const { return std::get_if<std::unordered_map<std::basic_string<C>, Value<C>>>(this); }
|
||||
|
||||
const Value<C>& operator[](std::basic_string<C> key) const
|
||||
{
|
||||
if (auto object = Object()) if (auto it = object->find(key); it != object->end()) return it->second;
|
||||
return failure;
|
||||
}
|
||||
const Value<C>& operator[](int i) const
|
||||
{
|
||||
if (auto array = Array()) if (i < array->size()) return array->at(i);
|
||||
return failure;
|
||||
}
|
||||
|
||||
static const Value<C> failure;
|
||||
};
|
||||
template <typename C> const Value<C> Value<C>::failure;
|
||||
|
||||
template <typename C, int maxDepth = 25>
|
||||
Value<C> Parse(const std::basic_string<C>& text, int64_t& i, int depth)
|
||||
{
|
||||
if (depth > maxDepth) return {};
|
||||
C ch;
|
||||
auto SkipWhitespace = [&]
|
||||
{
|
||||
while (i < text.size() && (text[i] == ' ' || text[i] == '\n' || text[i] == '\r' || text[i] == '\t')) ++i;
|
||||
if (i >= text.size()) return true;
|
||||
ch = text[i];
|
||||
return false;
|
||||
};
|
||||
auto ExtractString = [&]
|
||||
{
|
||||
std::basic_string<C> unescaped;
|
||||
i += 1;
|
||||
for (; i < text.size(); ++i)
|
||||
{
|
||||
auto ch = text[i];
|
||||
if (ch == '"') return i += 1, unescaped;
|
||||
if (ch == '\\')
|
||||
{
|
||||
ch = text[i + 1];
|
||||
if (ch == 'u' && isxdigit(text[i + 2]) && isxdigit(text[i + 3]) && isxdigit(text[i + 4]) && isxdigit(text[i + 5]))
|
||||
{
|
||||
char charCode[] = { (char)text[i + 2], (char)text[i + 3], (char)text[i + 4], (char)text[i + 5], 0 };
|
||||
unescaped += UTF<C>::FromCodepoint(strtoul(charCode, nullptr, 16));
|
||||
i += 5;
|
||||
continue;
|
||||
}
|
||||
for (auto [original, value] : Array<char, char>{ { 'b', '\b' }, {'f', '\f'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'} }) if (ch == original)
|
||||
{
|
||||
unescaped.push_back(value);
|
||||
goto replaced;
|
||||
}
|
||||
unescaped.push_back(ch);
|
||||
replaced: i += 1;
|
||||
}
|
||||
else unescaped.push_back(ch);
|
||||
}
|
||||
return unescaped;
|
||||
};
|
||||
|
||||
if (SkipWhitespace()) return {};
|
||||
|
||||
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 {};
|
||||
if (ch == trueStr[0])
|
||||
if (std::char_traits<C>::compare(text.data() + i, trueStr, std::size(trueStr)) == 0) return i += std::size(trueStr), true;
|
||||
else return {};
|
||||
if (ch == falseStr[0])
|
||||
if (std::char_traits<C>::compare(text.data() + i, falseStr, std::size(falseStr)) == 0) return i += std::size(falseStr), false;
|
||||
else return {};
|
||||
|
||||
if (ch == '-' || (ch >= '0' && ch <= '9'))
|
||||
{
|
||||
std::string number;
|
||||
for (; i < text.size() && ((text[i] >= '0' && text[i] <= '9') || text[i] == '-' || text[i] == '+' || text[i] == 'e' || text[i] == 'E' || text[i] == '.'); ++i)
|
||||
number.push_back(text[i]);
|
||||
return strtod(number.c_str(), NULL);
|
||||
}
|
||||
|
||||
if (ch == '"') return ExtractString();
|
||||
|
||||
if (ch == '[')
|
||||
{
|
||||
std::vector<Value<C>> array;
|
||||
while (true)
|
||||
{
|
||||
i += 1;
|
||||
if (SkipWhitespace()) return {};
|
||||
if (ch == ']') return i += 1, Value<C>(array);
|
||||
if (!array.emplace_back(Parse<C, maxDepth>(text, i, depth + 1))) return {};
|
||||
if (SkipWhitespace()) return {};
|
||||
if (ch == ']') return i += 1, Value<C>(array);
|
||||
if (ch != ',') return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (ch == '{')
|
||||
{
|
||||
std::unordered_map<std::basic_string<C>, Value<C>> object;
|
||||
while (true)
|
||||
{
|
||||
i += 1;
|
||||
if (SkipWhitespace()) return {};
|
||||
if (ch == '}') return i += 1, Value<C>(object);
|
||||
if (ch != '"') return {};
|
||||
auto key = ExtractString();
|
||||
if (SkipWhitespace() || ch != ':') return {};
|
||||
i += 1;
|
||||
if (!(object[std::move(key)] = Parse<C, maxDepth>(text, i, depth + 1))) return {};
|
||||
if (SkipWhitespace()) return {};
|
||||
if (ch == '}') return i += 1, Value<C>(object);
|
||||
if (ch != ',') return {};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
template <typename C>
|
||||
Value<C> Parse(const std::basic_string<C>& text)
|
||||
{
|
||||
int64_t start = 0;
|
||||
return Parse(text, start, 0);
|
||||
}
|
||||
}
|
38
LunaHost/GUI/Plugin/extensions/qtcommon.h
Normal file
38
LunaHost/GUI/Plugin/extensions/qtcommon.h
Normal file
@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QHash>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QMainWindow>
|
||||
#include <QDialog>
|
||||
#include <QApplication>
|
||||
#include <QLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QSpinBox>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QInputDialog>
|
||||
|
||||
static thread_local bool ok;
|
||||
|
||||
constexpr auto CONFIG_FILE = u8"Textractor.ini";
|
||||
constexpr auto WINDOW = u8"Window";
|
||||
|
||||
|
||||
struct Settings : QSettings { Settings(QObject* parent = nullptr) : QSettings(CONFIG_FILE, QSettings::IniFormat, parent) {} };
|
||||
struct QTextFile : QFile { QTextFile(QString name, QIODevice::OpenMode mode) : QFile(name) { open(mode | QIODevice::Text); } };
|
||||
struct Localizer { Localizer() { Localize(); } };
|
||||
inline std::wstring S(const QString& s) { return { s.toStdWString() }; }
|
||||
inline QString S(const std::string& s) { return QString::fromStdString(s); }
|
||||
inline QString S(const std::wstring& s) { return QString::fromStdWString(s); }
|
||||
// TODO: allow paired surrogates
|
||||
inline void sanitize(QString& s) { s.chop(std::distance(std::remove_if(s.begin(), s.end(), [](QChar ch) { return ch.isSurrogate(); }), s.end())); }
|
||||
inline QString sanitize(QString&& s) { sanitize(s); return std::move(s); }
|
||||
|
71
LunaHost/GUI/Plugin/extensions/regexfilter.cpp
Normal file
71
LunaHost/GUI/Plugin/extensions/regexfilter.cpp
Normal file
@ -0,0 +1,71 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "ui_regexfilter.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <fstream>
|
||||
|
||||
extern const char* REGEX_FILTER;
|
||||
extern const char* INVALID_REGEX;
|
||||
extern const char* CURRENT_FILTER;
|
||||
|
||||
const char* REGEX_SAVE_FILE = "SavedRegexFilters.txt";
|
||||
|
||||
std::optional<std::wregex> regex;
|
||||
std::wstring replace = L"$1";
|
||||
concurrency::reader_writer_lock m;
|
||||
DWORD (*GetSelectedProcessId)() = [] { return 0UL; };
|
||||
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
|
||||
connect(ui.regexEdit, &QLineEdit::textEdited, this, &Window::SetRegex);
|
||||
connect(ui.saveButton, &QPushButton::clicked, this, &Window::Save);
|
||||
|
||||
setWindowTitle(REGEX_FILTER);
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void SetRegex(QString regex)
|
||||
{
|
||||
ui.regexEdit->setText(regex);
|
||||
std::scoped_lock lock(m);
|
||||
if (!regex.isEmpty()) try { ::regex = S(regex); }
|
||||
catch (std::regex_error) { return ui.output->setText(INVALID_REGEX); }
|
||||
else ::regex = std::nullopt;
|
||||
ui.output->setText(QString(CURRENT_FILTER).arg(regex));
|
||||
}
|
||||
|
||||
private:
|
||||
void Save()
|
||||
{
|
||||
auto formatted = FormatString(
|
||||
L"\xfeff|PROCESS|%s|FILTER|%s|END|\r\n",
|
||||
GetModuleFilename(GetSelectedProcessId()).value_or(FormatString(L"Error getting name of process 0x%X", GetSelectedProcessId())),
|
||||
S(ui.regexEdit->text())
|
||||
);
|
||||
std::ofstream(REGEX_SAVE_FILE, std::ios::binary | std::ios::app).write((const char*)formatted.c_str(), formatted.size() * sizeof(wchar_t));
|
||||
}
|
||||
|
||||
Ui::FilterWindow ui;
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
static auto _ = GetSelectedProcessId = (DWORD(*)())sentenceInfo["get selected process id"];
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
if (/*sentenceInfo["current select"] && */!regex) if (auto processName = GetModuleFilename(sentenceInfo["process id"]))
|
||||
{
|
||||
std::ifstream stream(REGEX_SAVE_FILE, std::ios::binary);
|
||||
BlockMarkupIterator savedFilters(stream, Array<std::wstring_view>{ L"|PROCESS|", L"|FILTER|" });
|
||||
std::vector<std::wstring> regexes;
|
||||
while (auto read = savedFilters.Next()) if (read->at(0) == processName) regexes.push_back(std::move(read->at(1)));
|
||||
if (!regexes.empty()) QMetaObject::invokeMethod(&window, std::bind(&Window::SetRegex, &window, S(regexes.back())), Qt::BlockingQueuedConnection);
|
||||
}
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
if (regex) sentence = std::regex_replace(sentence, regex.value(), replace);
|
||||
return true;
|
||||
}
|
61
LunaHost/GUI/Plugin/extensions/regexfilter.ui
Normal file
61
LunaHost/GUI/Plugin/extensions/regexfilter.ui
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FilterWindow</class>
|
||||
<widget class="QDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>80</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="regexEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="saveButton">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="output">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel">
|
||||
<property name="text">
|
||||
<string><a href="https://regexr.com">regexr.com</a></string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextBrowserInteraction</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
71
LunaHost/GUI/Plugin/extensions/regexreplacer.cpp
Normal file
71
LunaHost/GUI/Plugin/extensions/regexreplacer.cpp
Normal file
@ -0,0 +1,71 @@
|
||||
#include "extension.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <fstream>
|
||||
#include <process.h>
|
||||
|
||||
extern const wchar_t* REGEX_REPLACER_INSTRUCTIONS;
|
||||
|
||||
const char* REPLACE_SAVE_FILE = "SavedRegexReplacements.txt";
|
||||
|
||||
std::atomic<std::filesystem::file_time_type> replaceFileLastWrite = {};
|
||||
concurrency::reader_writer_lock m;
|
||||
std::vector<std::tuple<std::wregex, std::wstring, std::regex_constants::match_flag_type>> replacements;
|
||||
|
||||
void UpdateReplacements()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (replaceFileLastWrite.exchange(std::filesystem::last_write_time(REPLACE_SAVE_FILE)) == std::filesystem::last_write_time(REPLACE_SAVE_FILE)) return;
|
||||
std::scoped_lock lock(m);
|
||||
replacements.clear();
|
||||
std::ifstream stream(REPLACE_SAVE_FILE, std::ios::binary);
|
||||
BlockMarkupIterator savedFilters(stream, Array<std::wstring_view>{ L"|REGEX|", L"|BECOMES|", L"|MODIFIER|" });
|
||||
while (auto read = savedFilters.Next())
|
||||
{
|
||||
const auto& [regex, replacement, modifier] = read.value();
|
||||
try
|
||||
{
|
||||
replacements.emplace_back(
|
||||
std::wregex(regex, modifier.find(L'i') == std::string::npos ? std::regex::ECMAScript : std::regex::icase),
|
||||
replacement,
|
||||
modifier.find(L'g') == std::string::npos ? std::regex_constants::format_first_only : std::regex_constants::format_default
|
||||
);
|
||||
}
|
||||
catch (std::regex_error) {}
|
||||
}
|
||||
}
|
||||
catch (std::filesystem::filesystem_error) { replaceFileLastWrite.store({}); }
|
||||
}
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
UpdateReplacements();
|
||||
if (replacements.empty())
|
||||
{
|
||||
auto file = std::ofstream(REPLACE_SAVE_FILE, std::ios::binary) << "\xff\xfe";
|
||||
for (auto ch : std::wstring_view(REGEX_REPLACER_INSTRUCTIONS))
|
||||
file << (ch == L'\n' ? std::string_view("\r\0\n", 4) : std::string_view((char*)&ch, 2));
|
||||
SpawnThread([] { _spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); }); // show file to user
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
UpdateReplacements();
|
||||
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
for (const auto& [regex, replacement, flags] : replacements) sentence = std::regex_replace(sentence, regex, replacement, flags);
|
||||
return true;
|
||||
}
|
54
LunaHost/GUI/Plugin/extensions/removerepeatchar.cpp
Normal file
54
LunaHost/GUI/Plugin/extensions/removerepeatchar.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
std::vector<int> repeatNumbers(sentence.size() + 1, 0);
|
||||
for (int i = 0; i < sentence.size(); ++i)
|
||||
{
|
||||
if (sentence[i] != sentence[i + 1])
|
||||
{
|
||||
int j = i;
|
||||
while (sentence[j] == sentence[i] && --j >= 0);
|
||||
repeatNumbers[i - j] += 1;
|
||||
}
|
||||
}
|
||||
int repeatNumber = std::distance(repeatNumbers.begin(), std::max_element(repeatNumbers.rbegin(), repeatNumbers.rend()).base() - 1);
|
||||
if (repeatNumber < 2) return false;
|
||||
|
||||
std::wstring newSentence;
|
||||
for (int i = 0; i < sentence.size();)
|
||||
{
|
||||
newSentence.push_back(sentence[i]);
|
||||
for (int j = i; j <= sentence.size(); ++j)
|
||||
{
|
||||
if (j == sentence.size() || sentence[i] != sentence[j])
|
||||
{
|
||||
i += (j - i) % repeatNumber == 0 ? repeatNumber : 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sentence = newSentence;
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(
|
||||
{
|
||||
InfoForExtension nonConsole[] = { { "text number", 1 }, {} };
|
||||
|
||||
std::wstring repeatedChars = L"aaaaaaaaaaaabbbbbbcccdddaabbbcccddd";
|
||||
std::wstring someRepeatedChars = L"abcdefaabbccddeeff";
|
||||
ProcessSentence(repeatedChars, { nonConsole });
|
||||
ProcessSentence(someRepeatedChars, { nonConsole });
|
||||
assert(repeatedChars.find(L"aaaabbcd") == 0);
|
||||
assert(someRepeatedChars == L"abcdefabcdef");
|
||||
|
||||
std::wstring empty = L"", one = L" ", normal = L"This is a normal sentence. はい";
|
||||
ProcessSentence(empty, { nonConsole });
|
||||
ProcessSentence(one, { nonConsole });
|
||||
ProcessSentence(normal, { nonConsole });
|
||||
assert(empty == L"" && one == L" " && normal == L"This is a normal sentence. はい");
|
||||
}
|
||||
);
|
97
LunaHost/GUI/Plugin/extensions/removerepeatphrase.cpp
Normal file
97
LunaHost/GUI/Plugin/extensions/removerepeatphrase.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
#include "extension.h"
|
||||
|
||||
std::vector<int> GenerateSuffixArray(const std::wstring& text)
|
||||
{
|
||||
std::vector<int> suffixArray(text.size());
|
||||
for (int i = 0; i < text.size(); ++i) suffixArray[i] = i;
|
||||
// The below code is a more efficient way of doing this:
|
||||
// std::sort(suffixArray.begin(), suffixArray.end(), [&](int a, int b) { return wcscmp(text.c_str() + a, text.c_str() + b) > 0; });
|
||||
std::stable_sort(suffixArray.begin(), suffixArray.end(), [&](int a, int b) { return text[a] > text[b]; });
|
||||
std::vector<int> eqClasses(text.begin(), text.end());
|
||||
std::vector<int> count(text.size());
|
||||
for (int length = 1; length < text.size(); length *= 2)
|
||||
{
|
||||
// Determine equivalence class up to length, by checking length / 2 equivalence of suffixes and their following length / 2 suffixes
|
||||
std::vector<int> prevEqClasses = eqClasses;
|
||||
eqClasses[suffixArray[0]] = 0;
|
||||
for (int i = 1; i < text.size(); ++i)
|
||||
{
|
||||
int currentSuffix = suffixArray[i], lastSuffix = suffixArray[i - 1];
|
||||
if (currentSuffix + length < text.size() && prevEqClasses[currentSuffix] == prevEqClasses[lastSuffix] &&
|
||||
prevEqClasses[currentSuffix + length / 2] == prevEqClasses[lastSuffix + length / 2]
|
||||
)
|
||||
eqClasses[currentSuffix] = eqClasses[lastSuffix];
|
||||
else eqClasses[currentSuffix] = i;
|
||||
}
|
||||
|
||||
// Sort within equivalence class based on order of following suffix after length (orders up to length * 2)
|
||||
for (int i = 0; i < text.size(); ++i) count[i] = i;
|
||||
for (auto suffix : std::vector(suffixArray))
|
||||
{
|
||||
int precedingSuffix = suffix - length;
|
||||
if (precedingSuffix >= 0) suffixArray[count[eqClasses[precedingSuffix]]++] = precedingSuffix;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i + 1 < text.size(); ++i)
|
||||
assert(wcscmp(text.c_str() + suffixArray[i], text.c_str() + suffixArray[i + 1]) > 0);
|
||||
return suffixArray;
|
||||
}
|
||||
|
||||
constexpr wchar_t ERASED = 0xf246; // inside Unicode private use area
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
// This algorithm looks for repeating substrings (in other words, common prefixes among the set of suffixes) of the sentence with length > 6
|
||||
// It then looks for any regions of characters at least twice as long as the substring made up only of characters in the substring, and erases them
|
||||
// If this results in the substring being completely erased from the string, the substring is copied to the last location where it was located in the original string
|
||||
auto timeout = GetTickCount64() + 30'000; // give up if taking over 30 seconds
|
||||
std::vector<int> suffixArray = GenerateSuffixArray(sentence);
|
||||
for (int i = 0; i + 1 < sentence.size() && GetTickCount64() < timeout; ++i)
|
||||
{
|
||||
int commonPrefixLength = 0;
|
||||
for (int j = suffixArray[i], k = suffixArray[i + 1]; j < sentence.size() && k < sentence.size(); ++j, ++k)
|
||||
if (sentence[j] != ERASED && sentence[j] == sentence[k]) commonPrefixLength += 1;
|
||||
else break;
|
||||
|
||||
if (commonPrefixLength > 6)
|
||||
{
|
||||
std::wstring substring(sentence, suffixArray[i], commonPrefixLength);
|
||||
bool substringCharMap[0x10000] = {};
|
||||
for (auto ch : substring) substringCharMap[ch] = true;
|
||||
|
||||
for (int regionSize = 0, j = 0; j <= sentence.size(); ++j)
|
||||
if (substringCharMap[sentence[j]]) regionSize += 1;
|
||||
else if (regionSize >= commonPrefixLength * 2)
|
||||
while (regionSize > 0) sentence[j - regionSize--] = ERASED;
|
||||
else regionSize = 0;
|
||||
|
||||
if (!wcsstr(sentence.c_str(), substring.c_str())) std::copy(substring.begin(), substring.end(), sentence.begin() + max(suffixArray[i], suffixArray[i + 1]));
|
||||
}
|
||||
}
|
||||
sentence.erase(std::remove(sentence.begin(), sentence.end(), ERASED), sentence.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(
|
||||
{
|
||||
InfoForExtension nonConsole[] = { { "text number", 1 }, {} };
|
||||
|
||||
std::wstring cyclicRepeats = L"Name: '_abcdefg_abcdefg_abcdefg_abcdefg_abcdefg'";
|
||||
std::wstring buildupRepeats = L"Name: '__a_ab_abc_abcd_abcde_abcdef_abcdefg'";
|
||||
std::wstring breakdownRepeats = L"Name: '_abcdefg_abcdef_abcde_abcd_abc_ab_a_'";
|
||||
ProcessSentence(cyclicRepeats, { nonConsole });
|
||||
ProcessSentence(buildupRepeats, { nonConsole });
|
||||
ProcessSentence(breakdownRepeats, { nonConsole });
|
||||
assert(cyclicRepeats == L"Name: '_abcdefg'");
|
||||
assert(buildupRepeats == L"Name: '_abcdefg'");
|
||||
assert(breakdownRepeats == L"Name: '_abcdefg'");
|
||||
|
||||
std::wstring empty = L"", one = L" ", normal = L"This is a normal sentence. はい";
|
||||
ProcessSentence(empty, { nonConsole });
|
||||
ProcessSentence(one, { nonConsole });
|
||||
ProcessSentence(normal, { nonConsole });
|
||||
assert(empty == L"" && one == L" " && normal == L"This is a normal sentence. はい");
|
||||
}
|
||||
);
|
52
LunaHost/GUI/Plugin/extensions/removerepeatphrase2.cpp
Normal file
52
LunaHost/GUI/Plugin/extensions/removerepeatphrase2.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
// This algorithm looks at all the prefixes of the sentence: if a prefix is found later in the sentence, it is removed from the beginning and the process is repeated
|
||||
auto timeout = GetTickCount64() + 30'000; // give up if taking over 30 seconds
|
||||
auto data = std::make_unique<wchar_t[]>(sentence.size() + 1);
|
||||
wcscpy_s(data.get(), sentence.size() + 1, sentence.c_str());
|
||||
wchar_t* dataEnd = data.get() + sentence.size();
|
||||
int skip = 0, count = 0;
|
||||
for (wchar_t* end = dataEnd; end - data.get() > skip && GetTickCount64() < timeout; --end)
|
||||
{
|
||||
std::swap(*end, *dataEnd);
|
||||
int junkLength = end - data.get() - skip;
|
||||
auto junkFound = wcsstr(sentence.c_str() + skip + junkLength, data.get() + skip);
|
||||
std::swap(*end, *dataEnd);
|
||||
if (junkFound)
|
||||
{
|
||||
if (count && junkLength < min(skip / count, 4)) break;
|
||||
skip += junkLength;
|
||||
count += 1;
|
||||
end = dataEnd;
|
||||
}
|
||||
}
|
||||
if (count && skip / count >= 3)
|
||||
{
|
||||
sentence = data.get() + skip;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
TEST(
|
||||
{
|
||||
InfoForExtension nonConsole[] = { { "text number", 1 }, {} };
|
||||
|
||||
std::wstring cyclicRepeats = L"_abcde_abcdef_abcdefg_abcdefg_abcdefg_abcdefg_abcdefg";
|
||||
std::wstring buildupRepeats = L"__a_ab_abc_abcd_abcde_abcdef_abcdefg";
|
||||
ProcessSentence(cyclicRepeats, { nonConsole });
|
||||
ProcessSentence(buildupRepeats, { nonConsole });
|
||||
assert(cyclicRepeats == L"_abcdefg");
|
||||
assert(buildupRepeats == L"_abcdefg");
|
||||
|
||||
std::wstring empty = L"", one = L" ", normal = L"This is a normal sentence. はい";
|
||||
ProcessSentence(empty, { nonConsole });
|
||||
ProcessSentence(one, { nonConsole });
|
||||
ProcessSentence(normal, { nonConsole });
|
||||
assert(empty == L"" && one == L" " && normal == L"This is a normal sentence. はい");
|
||||
}
|
||||
);
|
44
LunaHost/GUI/Plugin/extensions/removerepeatsentence.cpp
Normal file
44
LunaHost/GUI/Plugin/extensions/removerepeatsentence.cpp
Normal file
@ -0,0 +1,44 @@
|
||||
#include "extension.h"
|
||||
|
||||
int sentenceCacheSize = 30;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
wchar_t filePath[MAX_PATH];
|
||||
GetModuleFileNameW(hModule, filePath, MAX_PATH);
|
||||
if (wchar_t* fileName = wcsrchr(filePath, L'\\')) swscanf_s(fileName, L"\\Remove %d Repeated Sentences.xdll", &sentenceCacheSize);
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
uint64_t textNumber = sentenceInfo["text number"];
|
||||
if (textNumber == 0) return false;
|
||||
|
||||
static std::deque<Synchronized<std::vector<std::wstring>>> cache;
|
||||
static std::mutex m;
|
||||
m.lock();
|
||||
if (textNumber + 1 > cache.size()) cache.resize(textNumber + 1);
|
||||
auto prevSentences = cache[textNumber].Acquire();
|
||||
m.unlock();
|
||||
auto& inserted = prevSentences->emplace_back(sentence);
|
||||
auto firstLocation = std::find(prevSentences->begin(), prevSentences->end(), sentence);
|
||||
if (&*firstLocation != &inserted)
|
||||
{
|
||||
prevSentences->erase(firstLocation);
|
||||
sentence.clear();
|
||||
}
|
||||
if (prevSentences->size() > sentenceCacheSize) prevSentences->erase(prevSentences->begin());
|
||||
return sentence.empty();
|
||||
}
|
142
LunaHost/GUI/Plugin/extensions/replacer.cpp
Normal file
142
LunaHost/GUI/Plugin/extensions/replacer.cpp
Normal file
@ -0,0 +1,142 @@
|
||||
#include "extension.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <cwctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <process.h>
|
||||
|
||||
extern const wchar_t* REPLACER_INSTRUCTIONS;
|
||||
|
||||
constexpr auto REPLACE_SAVE_FILE = u8"SavedReplacements.txt";
|
||||
|
||||
std::atomic<std::filesystem::file_time_type> replaceFileLastWrite = {};
|
||||
concurrency::reader_writer_lock m;
|
||||
|
||||
class Trie
|
||||
{
|
||||
public:
|
||||
Trie(const std::istream& replacementScript)
|
||||
{
|
||||
BlockMarkupIterator replacementScriptParser(replacementScript, Array<std::wstring_view>{ L"|ORIG|", L"|BECOMES|" });
|
||||
while (auto read = replacementScriptParser.Next())
|
||||
{
|
||||
const auto& [original, replacement] = read.value();
|
||||
Node* current = &root;
|
||||
for (auto ch : original) if (!Ignore(ch)) current = Next(current, ch);
|
||||
if (current != &root)
|
||||
current->value = charStorage.insert(charStorage.end(), replacement.c_str(), replacement.c_str() + replacement.size() + 1) - charStorage.begin();
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring Replace(const std::wstring& sentence) const
|
||||
{
|
||||
std::wstring result;
|
||||
for (int i = 0; i < sentence.size();)
|
||||
{
|
||||
std::wstring_view replacement(sentence.c_str() + i, 1);
|
||||
int originalLength = 1;
|
||||
|
||||
const Node* current = &root;
|
||||
for (int j = i; current && j <= sentence.size(); ++j)
|
||||
{
|
||||
if (current->value >= 0)
|
||||
{
|
||||
replacement = charStorage.data() + current->value;
|
||||
originalLength = j - i;
|
||||
}
|
||||
if (!Ignore(sentence[j])) current = Next(current, sentence[j]) ? Next(current, sentence[j]) : Next(current, L'^');
|
||||
}
|
||||
|
||||
result += replacement;
|
||||
i += originalLength;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Empty()
|
||||
{
|
||||
return root.charMap.empty();
|
||||
}
|
||||
|
||||
private:
|
||||
static bool Ignore(wchar_t ch)
|
||||
{
|
||||
return ch <= 0x20 || iswspace(ch);
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
static Node* Next(Node* node, wchar_t ch)
|
||||
{
|
||||
auto it = std::lower_bound(node->charMap.begin(), node->charMap.end(), ch, [](const auto& one, auto two) { return one.first < two; });
|
||||
if (it != node->charMap.end() && it->first == ch) return it->second.get();
|
||||
if constexpr (!std::is_const_v<Node>) return node->charMap.insert(it, { ch, std::make_unique<Node>() })->second.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct Node
|
||||
{
|
||||
std::vector<std::pair<wchar_t, std::unique_ptr<Node>>> charMap;
|
||||
ptrdiff_t value = -1;
|
||||
} root;
|
||||
|
||||
std::vector<wchar_t> charStorage;
|
||||
} trie = { std::istringstream("") };
|
||||
|
||||
void UpdateReplacements()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (replaceFileLastWrite.exchange(std::filesystem::last_write_time(REPLACE_SAVE_FILE)) == std::filesystem::last_write_time(REPLACE_SAVE_FILE)) return;
|
||||
std::scoped_lock lock(m);
|
||||
trie = Trie(std::ifstream(REPLACE_SAVE_FILE, std::ios::binary));
|
||||
}
|
||||
catch (std::filesystem::filesystem_error) { replaceFileLastWrite.store({}); }
|
||||
}
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
UpdateReplacements();
|
||||
if (trie.Empty())
|
||||
{
|
||||
auto file = std::ofstream(REPLACE_SAVE_FILE, std::ios::binary) << "\xff\xfe";
|
||||
for (auto ch : std::wstring_view(REPLACER_INSTRUCTIONS))
|
||||
file << (ch == L'\n' ? std::string_view("\r\0\n", 4) : std::string_view((char*)&ch, 2));
|
||||
SpawnThread([] { _spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); }); // show file to user
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo)
|
||||
{
|
||||
UpdateReplacements();
|
||||
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
sentence = trie.Replace(sentence);
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(
|
||||
{
|
||||
std::wstring replacementScript = LR"(
|
||||
|ORIG|さよなら|BECOMES|goodbye |END|Ignore this text
|
||||
And this text ツ
|
||||
|ORIG|バカ|BECOMES|idiot|END|
|
||||
|ORIG|こんにちは |BECOMES| hello|END||ORIG|delet^this|BECOMES||END|)";
|
||||
Trie replacements(std::istringstream(std::string{ (const char*)replacementScript.c_str(), replacementScript.size() * sizeof(wchar_t) }));
|
||||
std::wstring original = LR"(Don't replace this
|
||||
さよなら バカ こんにちは delete this)";
|
||||
std::wstring replaced = Trie(std::move(replacements)).Replace(original);
|
||||
assert(replaced == L"Don't replace thisgoodbye idiot hello");
|
||||
}
|
||||
);
|
54
LunaHost/GUI/Plugin/extensions/styler.cpp
Normal file
54
LunaHost/GUI/Plugin/extensions/styler.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
extern const char* LOAD_SCRIPT;
|
||||
|
||||
constexpr auto STYLE_SAVE_FILE = u8"Textractor.qss";
|
||||
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
connect(&loadButton, &QPushButton::clicked, this, &Window::LoadScript);
|
||||
|
||||
if (scriptEditor.toPlainText().isEmpty())
|
||||
scriptEditor.setPlainText("/*\nhttps://www.google.com/search?q=Qt+stylesheet+gallery\nhttps://doc.qt.io/qt-5/stylesheet-syntax.html\n*/");
|
||||
layout.addWidget(&scriptEditor);
|
||||
layout.addWidget(&loadButton);
|
||||
|
||||
resize(800, 600);
|
||||
setWindowTitle("Styler");
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
|
||||
LoadScript();
|
||||
}
|
||||
|
||||
~Window()
|
||||
{
|
||||
qApp->setStyleSheet("");
|
||||
Save();
|
||||
}
|
||||
|
||||
private:
|
||||
void LoadScript()
|
||||
{
|
||||
qApp->setStyleSheet(scriptEditor.toPlainText());
|
||||
Save();
|
||||
}
|
||||
|
||||
void Save()
|
||||
{
|
||||
QTextFile(STYLE_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Truncate).write(scriptEditor.toPlainText().toUtf8());
|
||||
}
|
||||
|
||||
QHBoxLayout layout{ this };
|
||||
QPlainTextEdit scriptEditor{ QTextFile(STYLE_SAVE_FILE, QIODevice::ReadOnly).readAll(), this };
|
||||
QPushButton loadButton{ LOAD_SCRIPT, this };
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
return false;
|
||||
}
|
1417
LunaHost/GUI/Plugin/extensions/text.cpp
Normal file
1417
LunaHost/GUI/Plugin/extensions/text.cpp
Normal file
File diff suppressed because it is too large
Load Diff
79
LunaHost/GUI/Plugin/extensions/threadlinker.cpp
Normal file
79
LunaHost/GUI/Plugin/extensions/threadlinker.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "ui_threadlinker.h"
|
||||
#include <QKeyEvent>
|
||||
|
||||
extern const char* THREAD_LINKER;
|
||||
extern const char* LINK;
|
||||
extern const char* UNLINK;
|
||||
extern const char* THREAD_LINK_FROM;
|
||||
extern const char* THREAD_LINK_TO;
|
||||
extern const char* HEXADECIMAL;
|
||||
|
||||
std::unordered_map<int64_t, std::unordered_set<int64_t>> links;
|
||||
std::unordered_set<int64_t> universalLinks, empty;
|
||||
bool separateSentences = false; // allow user to change?
|
||||
concurrency::reader_writer_lock m;
|
||||
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
ui.linkButton->setText(LINK);
|
||||
ui.unlinkButton->setText(UNLINK);
|
||||
connect(ui.linkButton, &QPushButton::clicked, this, &Window::Link);
|
||||
connect(ui.unlinkButton, &QPushButton::clicked, this, &Window::Unlink);
|
||||
|
||||
setWindowTitle(THREAD_LINKER);
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
private:
|
||||
void Link()
|
||||
{
|
||||
bool ok1, ok2, ok3, ok4;
|
||||
QString fromInput = QInputDialog::getText(this, THREAD_LINK_FROM, HEXADECIMAL, QLineEdit::Normal, "All", &ok1, Qt::WindowCloseButtonHint);
|
||||
int from = fromInput.toInt(&ok2, 16);
|
||||
if (ok1 && (fromInput == "All" || ok2))
|
||||
{
|
||||
int to = QInputDialog::getText(this, THREAD_LINK_TO, HEXADECIMAL, QLineEdit::Normal, "", &ok3, Qt::WindowCloseButtonHint).toInt(&ok4, 16);
|
||||
if (ok3 && ok4)
|
||||
{
|
||||
std::scoped_lock lock(m);
|
||||
if ((ok2 ? links[from] : universalLinks).insert(to).second)
|
||||
ui.linkList->addItem((ok2 ? QString::number(from, 16) : "All") + "->" + QString::number(to, 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Unlink()
|
||||
{
|
||||
if (ui.linkList->currentItem())
|
||||
{
|
||||
QStringList link = ui.linkList->currentItem()->text().split("->");
|
||||
ui.linkList->takeItem(ui.linkList->currentRow());
|
||||
std::scoped_lock lock(m);
|
||||
(link[0] == "All" ? universalLinks : links[link[0].toInt(nullptr, 16)]).erase(link[1].toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
|
||||
void keyPressEvent(QKeyEvent* event) override
|
||||
{
|
||||
if (event->key() == Qt::Key_Delete) Unlink();
|
||||
}
|
||||
|
||||
Ui::LinkWindow ui;
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
auto action = separateSentences ? sentenceInfo["add sentence"] : sentenceInfo["add text"];
|
||||
auto it = links.find(sentenceInfo["text number"]);
|
||||
for (const auto& linkSet : { it != links.end() ? it->second : empty, sentenceInfo["text number"] > 1 ? universalLinks : empty })
|
||||
for (auto link : linkSet)
|
||||
((void(*)(int64_t, const wchar_t*))action)(link, sentence.c_str());
|
||||
return false;
|
||||
}
|
47
LunaHost/GUI/Plugin/extensions/threadlinker.ui
Normal file
47
LunaHost/GUI/Plugin/extensions/threadlinker.ui
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>LinkWindow</class>
|
||||
<widget class="QDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="linkList"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="linkButton">
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="unlinkButton">
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
212
LunaHost/GUI/Plugin/extensions/translatewrapper.cpp
Normal file
212
LunaHost/GUI/Plugin/extensions/translatewrapper.cpp
Normal file
@ -0,0 +1,212 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <concurrent_priority_queue.h>
|
||||
#include <fstream>
|
||||
#include <QComboBox>
|
||||
|
||||
extern const char* NATIVE_LANGUAGE;
|
||||
extern const char* TRANSLATE_TO;
|
||||
extern const char* TRANSLATE_FROM;
|
||||
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* MAX_TRANSLATIONS_IN_TIMESPAN;
|
||||
extern const char* TIMESPAN;
|
||||
extern const char* MAX_SENTENCE_SIZE;
|
||||
extern const char* API_KEY;
|
||||
extern const wchar_t* SENTENCE_TOO_LARGE_TO_TRANS;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
extern const wchar_t* TOO_MANY_TRANS_REQUESTS;
|
||||
|
||||
extern const char* TRANSLATION_PROVIDER;
|
||||
extern const char* GET_API_KEY_FROM;
|
||||
extern const QStringList languagesTo, languagesFrom;
|
||||
extern bool translateSelectedOnly, useRateLimiter, rateLimitSelected, useCache, useFilter;
|
||||
extern int tokenCount, rateLimitTimespan, maxSentenceSize;
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp);
|
||||
|
||||
QFormLayout* display;
|
||||
Settings settings;
|
||||
|
||||
namespace
|
||||
{
|
||||
Synchronized<TranslationParam> tlp;
|
||||
Synchronized<std::unordered_map<std::wstring, std::wstring>> translationCache;
|
||||
|
||||
std::string CacheFile()
|
||||
{
|
||||
return FormatString("%s Cache (%S).txt", TRANSLATION_PROVIDER, tlp->translateTo);
|
||||
}
|
||||
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(CacheFile(), std::ios::binary | std::ios::trunc).write((const char*)allTranslations.c_str(), allTranslations.size() * sizeof(wchar_t));
|
||||
}
|
||||
void LoadCache()
|
||||
{
|
||||
translationCache->clear();
|
||||
std::ifstream stream(CacheFile(), 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
display = new QFormLayout(this);
|
||||
|
||||
settings.beginGroup(TRANSLATION_PROVIDER);
|
||||
|
||||
auto translateToCombo = new QComboBox(this);
|
||||
translateToCombo->addItems(languagesTo);
|
||||
int i = -1;
|
||||
if (settings.contains(TRANSLATE_TO)) i = translateToCombo->findText(settings.value(TRANSLATE_TO).toString());
|
||||
if (i < 0) i = translateToCombo->findText(NATIVE_LANGUAGE, Qt::MatchStartsWith);
|
||||
if (i < 0) i = translateToCombo->findText("English", Qt::MatchStartsWith);
|
||||
translateToCombo->setCurrentIndex(i);
|
||||
SaveTranslateTo(translateToCombo->currentText());
|
||||
display->addRow(TRANSLATE_TO, translateToCombo);
|
||||
connect(translateToCombo, &QComboBox::currentTextChanged, this, &Window::SaveTranslateTo);
|
||||
auto translateFromCombo = new QComboBox(this);
|
||||
translateFromCombo->addItem("?");
|
||||
translateFromCombo->addItems(languagesFrom);
|
||||
i = -1;
|
||||
if (settings.contains(TRANSLATE_FROM)) i = translateFromCombo->findText(settings.value(TRANSLATE_FROM).toString());
|
||||
if (i < 0) i = 0;
|
||||
translateFromCombo->setCurrentIndex(i);
|
||||
SaveTranslateFrom(translateFromCombo->currentText());
|
||||
display->addRow(TRANSLATE_FROM, translateFromCombo);
|
||||
connect(translateFromCombo, &QComboBox::currentTextChanged, this, &Window::SaveTranslateFrom);
|
||||
for (auto [value, label] : Array<bool&, const char*>{
|
||||
{ translateSelectedOnly, TRANSLATE_SELECTED_THREAD_ONLY },
|
||||
{ useRateLimiter, RATE_LIMIT_ALL_THREADS },
|
||||
{ rateLimitSelected, RATE_LIMIT_SELECTED_THREAD },
|
||||
{ useCache, USE_TRANS_CACHE },
|
||||
{ useFilter, FILTER_GARBAGE }
|
||||
})
|
||||
{
|
||||
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*>{
|
||||
{ tokenCount, MAX_TRANSLATIONS_IN_TIMESPAN },
|
||||
{ rateLimitTimespan, TIMESPAN },
|
||||
{ maxSentenceSize, MAX_SENTENCE_SIZE },
|
||||
})
|
||||
{
|
||||
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); });
|
||||
}
|
||||
if (GET_API_KEY_FROM)
|
||||
{
|
||||
auto keyEdit = new QLineEdit(settings.value(API_KEY).toString(), this);
|
||||
tlp->authKey = S(keyEdit->text());
|
||||
QObject::connect(keyEdit, &QLineEdit::textChanged, [](QString key) { settings.setValue(API_KEY, S(tlp->authKey = S(key))); });
|
||||
auto keyLabel = new QLabel(QString("<a href=\"%1\">%2</a>").arg(GET_API_KEY_FROM, API_KEY), this);
|
||||
keyLabel->setOpenExternalLinks(true);
|
||||
display->addRow(keyLabel, keyEdit);
|
||||
}
|
||||
|
||||
setWindowTitle(TRANSLATION_PROVIDER);
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
~Window()
|
||||
{
|
||||
SaveCache();
|
||||
}
|
||||
|
||||
private:
|
||||
void SaveTranslateTo(QString language)
|
||||
{
|
||||
SaveCache();
|
||||
settings.setValue(TRANSLATE_TO, S(tlp->translateTo = S(language)));
|
||||
LoadCache();
|
||||
}
|
||||
void SaveTranslateFrom(QString language)
|
||||
{
|
||||
settings.setValue(TRANSLATE_FROM, S(tlp->translateFrom = S(language)));
|
||||
}
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
static class
|
||||
{
|
||||
public:
|
||||
bool Request()
|
||||
{
|
||||
DWORD64 current = GetTickCount64(), token;
|
||||
while (tokens.try_pop(token)) if (token > current - rateLimitTimespan)
|
||||
{
|
||||
tokens.push(token); // popped one too many
|
||||
break;
|
||||
}
|
||||
bool available = tokens.size() < tokenCount;
|
||||
if (available) tokens.push(current);
|
||||
return available;
|
||||
}
|
||||
|
||||
private:
|
||||
concurrency::concurrent_priority_queue<DWORD64, std::greater<DWORD64>> tokens;
|
||||
} rateLimiter;
|
||||
|
||||
bool cache = false;
|
||||
std::wstring translation;
|
||||
if (useFilter)
|
||||
{
|
||||
Trim(sentence);
|
||||
sentence.erase(std::remove_if(sentence.begin(), sentence.end(), [](wchar_t ch) { return ch < ' ' && ch != '\n'; }), sentence.end());
|
||||
}
|
||||
if (sentence.empty()) return true;
|
||||
if (sentence.size() > maxSentenceSize) translation = SENTENCE_TOO_LARGE_TO_TRANS;
|
||||
if (useCache)
|
||||
{
|
||||
auto translationCache = ::translationCache.Acquire();
|
||||
if (auto it = translationCache->find(sentence); it != translationCache->end()) translation = it->second;
|
||||
}
|
||||
if (translation.empty() && (!translateSelectedOnly || sentenceInfo["current select"]))
|
||||
if (rateLimiter.Request() || !useRateLimiter || (!rateLimitSelected && sentenceInfo["current select"])) std::tie(cache, translation) = Translate(sentence, tlp.Copy());
|
||||
else translation = TOO_MANY_TRANS_REQUESTS;
|
||||
if (cache) translationCache->operator[](sentence) = translation;
|
||||
|
||||
if (useFilter) Trim(translation);
|
||||
for (int i = 0; i < translation.size(); ++i) if (translation[i] == '\r' && translation[i + 1] == '\n') translation[i] = 0x200b; // for some reason \r appears as newline - no need to double
|
||||
if (translation.empty()) translation = TRANSLATION_ERROR;
|
||||
(sentence += L"\x200b \n") += translation;
|
||||
return true;
|
||||
}
|
||||
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes;
|
||||
TEST(
|
||||
{
|
||||
assert(Translate(L"こんにちは", { L"English", L"?", L"" }).second.find(L"ello") == 1 || strstr(TRANSLATION_PROVIDER, "DevTools"));
|
||||
|
||||
for (auto languages : { languagesFrom, languagesTo }) for (auto language : languages)
|
||||
assert(codes.count(S(language)));
|
||||
assert(codes.count(L"?"));
|
||||
}
|
||||
);
|
6
LunaHost/GUI/Plugin/extensions/translatewrapper.h
Normal file
6
LunaHost/GUI/Plugin/extensions/translatewrapper.h
Normal file
@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
struct TranslationParam
|
||||
{
|
||||
std::wstring translateTo, translateFrom, authKey;
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
#include"plugindef.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo);
|
||||
|
||||
extern "C" __declspec(dllexport) wchar_t* OnNewSentence(wchar_t* sentence, const InfoForExtension* sentenceInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::wstring sentenceCopy(sentence);
|
||||
int oldSize = sentenceCopy.size();
|
||||
if (ProcessSentence(sentenceCopy, SentenceInfo{ sentenceInfo }))
|
||||
{
|
||||
if (sentenceCopy.size() > oldSize) sentence = (wchar_t*)HeapReAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sentence, (sentenceCopy.size() + 1) * sizeof(wchar_t));
|
||||
wcscpy_s(sentence, sentenceCopy.size() + 1, sentenceCopy.c_str());
|
||||
}
|
||||
}
|
||||
catch (std::exception &e)
|
||||
{
|
||||
*sentence = L'\0';
|
||||
}
|
||||
return sentence;
|
||||
}
|
||||
bool sendclipboarddata(const std::wstring&text,HWND hwnd);
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["current select"] && sentenceInfo["process id"] != 0 &&sentenceInfo["toclipboard"])
|
||||
{
|
||||
sendclipboarddata(sentence,(HWND)sentenceInfo["HostHWND"]);
|
||||
}
|
||||
return false;
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
#include"pluginmanager.h"
|
||||
#include<filesystem>
|
||||
#include"Plugin/plugindef.h"
|
||||
#include"Plugin/extension.h"
|
||||
#include<fstream>
|
||||
#include <commdlg.h>
|
||||
#include"LunaHost.h"
|
||||
#include"Lang/Lang.h"
|
||||
#include"host.h"
|
||||
typedef wchar_t* (*OnNewSentence_t)(wchar_t*, const InfoForExtension*);
|
||||
|
||||
std::optional<std::wstring>SelectFile(HWND hwnd,LPCWSTR lpstrFilter){
|
||||
OPENFILENAME ofn;
|
||||
|
@ -1,6 +1,6 @@
|
||||
#ifndef LUNA_PLUGINMANAGER_H
|
||||
#define LUNA_PLUGINMANAGER_H
|
||||
#include"Plugin/plugindef.h"
|
||||
#include"Plugin/extension.h"
|
||||
#include"textthread.h"
|
||||
#include<nlohmann/json.hpp>
|
||||
class LunaHost;
|
||||
|
1
scripts/pack.bat
Normal file
1
scripts/pack.bat
Normal file
@ -0,0 +1 @@
|
||||
python pack.py
|
14
scripts/pack.py
Normal file
14
scripts/pack.py
Normal file
@ -0,0 +1,14 @@
|
||||
import os, shutil
|
||||
for f in os.listdir('../builds'):
|
||||
if os.path.isdir('../builds/'+f)==False:continue
|
||||
|
||||
for dirname,_,fs in os.walk('../builds/'+f):
|
||||
if dirname.endswith('translations') or dirname.endswith('translations') or dirname.endswith('imageformats') or dirname.endswith('iconengines') or dirname.endswith('bearer'):
|
||||
shutil.rmtree(dirname)
|
||||
continue
|
||||
for ff in fs:
|
||||
path=os.path.join(dirname,ff)
|
||||
if ff in ['Qt5Svg.dll','libEGL.dll','libGLESv2.dll','opengl32sw.dll','D3Dcompiler_47.dll']:os.remove(path)
|
||||
targetdir='../builds/'+f
|
||||
target='../builds/'+f+'.zip'
|
||||
os.system(rf'"C:\Program Files\7-Zip\7z.exe" a -m0=LZMA -mx9 {target} {targetdir}')
|
Loading…
Reference in New Issue
Block a user