diff --git a/CMakeLists.txt b/CMakeLists.txt index 676e2e7..e886054 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/LunaHook/CMakeLists.txt b/LunaHook/CMakeLists.txt index 6b43e87..78360ae 100644 --- a/LunaHook/CMakeLists.txt +++ b/LunaHook/CMakeLists.txt @@ -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 diff --git a/LunaHost/GUI/CMakeLists.txt b/LunaHost/GUI/CMakeLists.txt index 2d3338d..b25183f 100644 --- a/LunaHost/GUI/CMakeLists.txt +++ b/LunaHost/GUI/CMakeLists.txt @@ -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}) diff --git a/LunaHost/GUI/Plugin/CMakeLists.txt b/LunaHost/GUI/Plugin/CMakeLists.txt index 8b13412..6c7fff6 100644 --- a/LunaHost/GUI/Plugin/CMakeLists.txt +++ b/LunaHost/GUI/Plugin/CMakeLists.txt @@ -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() @@ -16,4 +10,13 @@ if(Qt5_DIR) target_link_libraries(QtLoader Qt5::Widgets Qt5::Core) 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() \ No newline at end of file diff --git a/LunaHost/GUI/Plugin/copyclipboard.cpp b/LunaHost/GUI/Plugin/copyclipboard.cpp new file mode 100644 index 0000000..6c70ad6 --- /dev/null +++ b/LunaHost/GUI/Plugin/copyclipboard.cpp @@ -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; +} \ No newline at end of file diff --git a/LunaHost/GUI/Plugin/plugindef.h b/LunaHost/GUI/Plugin/extension.h similarity index 72% rename from LunaHost/GUI/Plugin/plugindef.h rename to LunaHost/GUI/Plugin/extension.h index 5a67f3e..6a15ae2 100644 --- a/LunaHost/GUI/Plugin/plugindef.h +++ b/LunaHost/GUI/Plugin/extension.h @@ -1,6 +1,5 @@ -#include -#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 \ No newline at end of file +struct SKIP {}; +inline void Skip() { throw SKIP(); } diff --git a/LunaHost/GUI/Plugin/extensionimpl.cpp b/LunaHost/GUI/Plugin/extensionimpl.cpp new file mode 100644 index 0000000..c33a42d --- /dev/null +++ b/LunaHost/GUI/Plugin/extensionimpl.cpp @@ -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; +} diff --git a/LunaHost/GUI/Plugin/extensions/CMakeLists.txt b/LunaHost/GUI/Plugin/extensions/CMakeLists.txt new file mode 100644 index 0000000..7ed516f --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/CMakeLists.txt @@ -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%$${qt5_install_prefix}/bin + COMMAND Qt5::windeployqt --dir ${disttarget} "${disttarget}/DevTools\ DeepL\ Translate.dll" +) +endif() diff --git a/LunaHost/GUI/Plugin/extensions/bingtranslate.cpp b/LunaHost/GUI/Plugin/extensions/bingtranslate.cpp new file mode 100644 index 0000000..2722b85 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/bingtranslate.cpp @@ -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 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 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 i = 0; + static Synchronized 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) }; +} diff --git a/LunaHost/GUI/Plugin/extensions/blockmarkup.h b/LunaHost/GUI/Plugin/extensions/blockmarkup.h new file mode 100644 index 0000000..091a044 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/blockmarkup.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +template // windows file block size +class BlockMarkupIterator +{ +public: + BlockMarkupIterator(const std::istream& stream, const std::basic_string_view(&delimiters)[delimiterCount]) : streambuf(*stream.rdbuf()) + { + std::copy_n(delimiters, delimiterCount, this->delimiters.begin()); + } + + std::optional, delimiterCount>> Next() + { + std::array, 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> Find(std::basic_string_view 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 end{ endImpl, 5 }; + + std::basic_streambuf& streambuf; + std::basic_string buffer; + std::array, delimiterCount> delimiters; +}; diff --git a/LunaHost/GUI/Plugin/extensions/deepltranslate.cpp b/LunaHost/GUI/Plugin/extensions/deepltranslate.cpp new file mode 100644 index 0000000..a20fd88 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/deepltranslate.cpp @@ -0,0 +1,180 @@ +#include "qtcommon.h" +#include "translatewrapper.h" +#include "network.h" +#include + +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 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 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) }; +} diff --git a/LunaHost/GUI/Plugin/extensions/devtools.cpp b/LunaHost/GUI/Plugin/extensions/devtools.cpp new file mode 100644 index 0000000..e531f19 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/devtools.cpp @@ -0,0 +1,173 @@ +#include "devtools.h" +#include +#include +#include +#include +#include +#include + +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 idCounter = 0; + Synchronized>>> 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& 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().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(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 SendRequest(const char* method, const std::wstring& params) + { + concurrency::task_completion_event> 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 {}; + } +} diff --git a/LunaHost/GUI/Plugin/extensions/devtools.h b/LunaHost/GUI/Plugin/extensions/devtools.h new file mode 100644 index 0000000..83a004b --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/devtools.h @@ -0,0 +1,10 @@ +#include "qtcommon.h" +#include "network.h" + +namespace DevTools +{ + void Initialize(); + void Close(); + bool Connected(); + JSON::Value SendRequest(const char* method, const std::wstring& params = L"{}"); +} diff --git a/LunaHost/GUI/Plugin/extensions/devtoolsdeepltranslate.cpp b/LunaHost/GUI/Plugin/extensions/devtoolsdeepltranslate.cpp new file mode 100644 index 0000000..16a79ad --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/devtoolsdeepltranslate.cpp @@ -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 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 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 }; +} diff --git a/LunaHost/GUI/Plugin/extensions/devtoolspapagotranslate.cpp b/LunaHost/GUI/Plugin/extensions/devtoolspapagotranslate.cpp new file mode 100644 index 0000000..0934008 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/devtoolspapagotranslate.cpp @@ -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 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 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 }; +} diff --git a/LunaHost/GUI/Plugin/extensions/devtoolssystrantranslate.cpp b/LunaHost/GUI/Plugin/extensions/devtoolssystrantranslate.cpp new file mode 100644 index 0000000..ab7184b --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/devtoolssystrantranslate.cpp @@ -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 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 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 }; +} diff --git a/LunaHost/GUI/Plugin/extensions/extpch.h b/LunaHost/GUI/Plugin/extensions/extpch.h new file mode 100644 index 0000000..8f41959 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/extpch.h @@ -0,0 +1,191 @@ + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +template struct ArrayImpl { using Type = std::tuple[]; }; +template struct ArrayImpl { using Type = T[]; }; +template using Array = typename ArrayImpl::Type; + +template using Functor = std::integral_constant, F>; // shouldn't need remove_reference_t but MSVC is bugged + +struct PermissivePointer +{ + template operator T*() { return (T*)p; } + void* p; +}; + +template > +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 h; +}; + +template +class Synchronized +{ +public: + template + Synchronized(Args&&... args) : contents(std::forward(args)...) {} + + struct Locker + { + T* operator->() { return &contents; } + std::unique_lock 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 +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 operator T*() { static_assert(sizeof(T) < sizeof(DUMMY)); return (T*)DUMMY; } +} DUMMY; + +inline auto Swallow = [](auto&&...) {}; + +template std::optional> Copy(T* ptr) { if (ptr) return *ptr; return {}; } + +template inline auto FormatArg(T arg) { return arg; } +template inline auto FormatArg(const std::basic_string& arg) { return arg.c_str(); } + +#pragma warning(push) +#pragma warning(disable: 4996) +template +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 +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 StringToWideString(const std::string& text, UINT encoding) +{ + std::vector 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 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 buffer((text.size() + 1) * 4); + WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, buffer.data(), buffer.size(), nullptr, nullptr); + return buffer.data(); +} + +template +inline void TEXTRACTOR_MESSAGE(const wchar_t* format, const Args&... args) { MessageBoxW(NULL, FormatString(format, args...).c_str(), L"Textractor", MB_OK); } + +template +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 GetModuleFilename(DWORD processId, HMODULE module = NULL) +{ + std::vector 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 GetModuleFilename(HMODULE module = NULL) +{ + std::vector buffer(MAX_PATH); + if (GetModuleFileNameW(module, buffer.data(), MAX_PATH)) return buffer.data(); + return {}; +} + +inline std::vector>> GetAllProcesses() +{ + std::vector processIds(10000); + DWORD spaceUsed = 0; + EnumProcesses(processIds.data(), 10000 * sizeof(DWORD), &spaceUsed); + std::vector>> processes; + for (int i = 0; i < spaceUsed / sizeof(DWORD); ++i) processes.push_back({ processIds[i], GetModuleFilename(processIds[i]) }); + return processes; +} diff --git a/LunaHost/GUI/Plugin/extensions/extranewlines.cpp b/LunaHost/GUI/Plugin/extensions/extranewlines.cpp new file mode 100644 index 0000000..35236cf --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/extranewlines.cpp @@ -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; +} diff --git a/LunaHost/GUI/Plugin/extensions/extrawindow.cpp b/LunaHost/GUI/Plugin/extensions/extrawindow.cpp new file mode 100644 index 0000000..e4e894f --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/extrawindow.cpp @@ -0,0 +1,606 @@ +#include "qtcommon.h" +#include "extension.h" +#include "ui_extrawindow.h" +#include "blockmarkup.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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()); + SetTextColor(settings.value(TEXT_COLOR, TextColor()).value()); + outliner->color = settings.value(OUTLINE_COLOR, outliner->color).value(); + 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().alpha()); + outliner->color.setAlpha(settings.value(OUTLINE_COLOR).value().alpha()); + ui.display->setPalette(QPalette(settings.value(TEXT_COLOR).value(), {}, {}, {}, {}, {}, {})); + 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{ { 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{ + { 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 textPositionMap; + + std::vector 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{ "|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{ "|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 foundDefinitions; + for (term = term.left(100); !term.isEmpty(); term.chop(1)) + for (const auto& [rootTerm, definition, inflections] : LookupDefinitions(term, foundDefinitions)) + definitions.push_back( + QStringLiteral("

%1 (%5/%6)

%2%3%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 dictionary; + QString term; + + private: + struct LookupResult + { + QString term; + QString definition; + QStringList inflectionsUsed; + }; + std::vector LookupDefinitions(QString term, std::unordered_set& foundDefinitions, QStringList inflectionsUsed = {}) + { + std::vector 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 inflections; + + std::filesystem::file_time_type dictionaryFileLastWrite; + std::vector charStorage; + std::vector definitions; + int definitionIndex; + } dictionaryWindow; +} extraWindow; +#include +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; +} diff --git a/LunaHost/GUI/Plugin/extensions/extrawindow.ui b/LunaHost/GUI/Plugin/extensions/extrawindow.ui new file mode 100644 index 0000000..643a9e0 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/extrawindow.ui @@ -0,0 +1,51 @@ + + + ExtraWindow + + + + 0 + 0 + 800 + 300 + + + + Qt::CustomContextMenu + + + + + + + 0 + 0 + + + + + 16 + + + + Qt::CustomContextMenu + + + 0 + + + Qt::AlignTop + + + true + + + Qt::TextSelectableByMouse + + + + + + + + diff --git a/LunaHost/GUI/Plugin/extensions/googletranslate.cpp b/LunaHost/GUI/Plugin/extensions/googletranslate.cpp new file mode 100644 index 0000000..26d8d41 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/googletranslate.cpp @@ -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 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 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) }; +} diff --git a/LunaHost/GUI/Plugin/extensions/network.cpp b/LunaHost/GUI/Plugin/extensions/network.cpp new file mode 100644 index 0000000..ec65e0a --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/network.cpp @@ -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 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 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(LR"([{"string":"hello world","boolean":false,"number":1.67e+4,"null":null,"array":[]},"hello world"])"))); diff --git a/LunaHost/GUI/Plugin/extensions/network.h b/LunaHost/GUI/Plugin/extensions/network.h new file mode 100644 index 0000000..061e90c --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/network.h @@ -0,0 +1,233 @@ +#pragma once + +#include +#include + +using InternetHandle = AutoHandle>; + +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 + std::basic_string Unescape(std::basic_string 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{ + { 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::compare(text.data() + i, original, length) == 0) text.replace(i, length, 1, replacement); + return text; + } +} + +namespace JSON +{ + template + std::basic_string Escape(std::basic_string 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 struct UTF {}; + template <> struct UTF + { + inline static std::wstring FromCodepoint(unsigned codepoint) { return { (wchar_t)codepoint }; } // TODO: surrogate pairs + }; + + template + struct Value : private std::variant, std::vector>, std::unordered_map, Value>> + { + using std::variant, std::vector>, std::unordered_map, Value>>::variant; + + explicit operator bool() const { return index(); } + bool IsNull() const { return index() == 1; } + auto Boolean() const { return std::get_if(this); } + auto Number() const { return std::get_if(this); } + auto String() const { return std::get_if>(this); } + auto Array() const { return std::get_if>>(this); } + auto Object() const { return std::get_if, Value>>(this); } + + const Value& operator[](std::basic_string key) const + { + if (auto object = Object()) if (auto it = object->find(key); it != object->end()) return it->second; + return failure; + } + const Value& operator[](int i) const + { + if (auto array = Array()) if (i < array->size()) return array->at(i); + return failure; + } + + static const Value failure; + }; + template const Value Value::failure; + + template + Value Parse(const std::basic_string& 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 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::FromCodepoint(strtoul(charCode, nullptr, 16)); + i += 5; + continue; + } + for (auto [original, value] : Array{ { '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::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::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::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> array; + while (true) + { + i += 1; + if (SkipWhitespace()) return {}; + if (ch == ']') return i += 1, Value(array); + if (!array.emplace_back(Parse(text, i, depth + 1))) return {}; + if (SkipWhitespace()) return {}; + if (ch == ']') return i += 1, Value(array); + if (ch != ',') return {}; + } + } + + if (ch == '{') + { + std::unordered_map, Value> object; + while (true) + { + i += 1; + if (SkipWhitespace()) return {}; + if (ch == '}') return i += 1, Value(object); + if (ch != '"') return {}; + auto key = ExtractString(); + if (SkipWhitespace() || ch != ':') return {}; + i += 1; + if (!(object[std::move(key)] = Parse(text, i, depth + 1))) return {}; + if (SkipWhitespace()) return {}; + if (ch == '}') return i += 1, Value(object); + if (ch != ',') return {}; + } + } + + return {}; + } + + template + Value Parse(const std::basic_string& text) + { + int64_t start = 0; + return Parse(text, start, 0); + } +} diff --git a/LunaHost/GUI/Plugin/extensions/qtcommon.h b/LunaHost/GUI/Plugin/extensions/qtcommon.h new file mode 100644 index 0000000..aa485c5 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/qtcommon.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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); } + diff --git a/LunaHost/GUI/Plugin/extensions/regexfilter.cpp b/LunaHost/GUI/Plugin/extensions/regexfilter.cpp new file mode 100644 index 0000000..6fbb413 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/regexfilter.cpp @@ -0,0 +1,71 @@ +#include "qtcommon.h" +#include "extension.h" +#include "ui_regexfilter.h" +#include "blockmarkup.h" +#include + +extern const char* REGEX_FILTER; +extern const char* INVALID_REGEX; +extern const char* CURRENT_FILTER; + +const char* REGEX_SAVE_FILE = "SavedRegexFilters.txt"; + +std::optional 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{ L"|PROCESS|", L"|FILTER|" }); + std::vector 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; +} diff --git a/LunaHost/GUI/Plugin/extensions/regexfilter.ui b/LunaHost/GUI/Plugin/extensions/regexfilter.ui new file mode 100644 index 0000000..6e98269 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/regexfilter.ui @@ -0,0 +1,61 @@ + + + FilterWindow + + + + 0 + 0 + 500 + 80 + + + + + + + + + + + + Save + + + + + + + + + + + + Qt::AlignCenter + + + + + + + <a href="https://regexr.com">regexr.com</a> + + + Qt::RichText + + + Qt::AlignCenter + + + true + + + Qt::TextBrowserInteraction + + + + + + + + diff --git a/LunaHost/GUI/Plugin/extensions/regexreplacer.cpp b/LunaHost/GUI/Plugin/extensions/regexreplacer.cpp new file mode 100644 index 0000000..1b6692d --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/regexreplacer.cpp @@ -0,0 +1,71 @@ +#include "extension.h" +#include "blockmarkup.h" +#include +#include + +extern const wchar_t* REGEX_REPLACER_INSTRUCTIONS; + +const char* REPLACE_SAVE_FILE = "SavedRegexReplacements.txt"; + +std::atomic replaceFileLastWrite = {}; +concurrency::reader_writer_lock m; +std::vector> 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{ 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; +} diff --git a/LunaHost/GUI/Plugin/extensions/removerepeatchar.cpp b/LunaHost/GUI/Plugin/extensions/removerepeatchar.cpp new file mode 100644 index 0000000..cd8a3e0 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/removerepeatchar.cpp @@ -0,0 +1,54 @@ +#include "extension.h" + +bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo) +{ + if (sentenceInfo["text number"] == 0) return false; + + std::vector 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. はい"); + } +); diff --git a/LunaHost/GUI/Plugin/extensions/removerepeatphrase.cpp b/LunaHost/GUI/Plugin/extensions/removerepeatphrase.cpp new file mode 100644 index 0000000..a23f4d1 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/removerepeatphrase.cpp @@ -0,0 +1,97 @@ +#include "extension.h" + +std::vector GenerateSuffixArray(const std::wstring& text) +{ + std::vector 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 eqClasses(text.begin(), text.end()); + std::vector 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 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 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. はい"); + } +); diff --git a/LunaHost/GUI/Plugin/extensions/removerepeatphrase2.cpp b/LunaHost/GUI/Plugin/extensions/removerepeatphrase2.cpp new file mode 100644 index 0000000..8065139 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/removerepeatphrase2.cpp @@ -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(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. はい"); + } +); diff --git a/LunaHost/GUI/Plugin/extensions/removerepeatsentence.cpp b/LunaHost/GUI/Plugin/extensions/removerepeatsentence.cpp new file mode 100644 index 0000000..cfa2d7e --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/removerepeatsentence.cpp @@ -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>> 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(); +} diff --git a/LunaHost/GUI/Plugin/extensions/replacer.cpp b/LunaHost/GUI/Plugin/extensions/replacer.cpp new file mode 100644 index 0000000..be3e1a0 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/replacer.cpp @@ -0,0 +1,142 @@ +#include "extension.h" +#include "blockmarkup.h" +#include +#include +#include +#include + +extern const wchar_t* REPLACER_INSTRUCTIONS; + +constexpr auto REPLACE_SAVE_FILE = u8"SavedReplacements.txt"; + +std::atomic replaceFileLastWrite = {}; +concurrency::reader_writer_lock m; + +class Trie +{ +public: + Trie(const std::istream& replacementScript) + { + BlockMarkupIterator replacementScriptParser(replacementScript, Array{ 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 + 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) return node->charMap.insert(it, { ch, std::make_unique() })->second.get(); + return nullptr; + } + + struct Node + { + std::vector>> charMap; + ptrdiff_t value = -1; + } root; + + std::vector 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"); + } +); diff --git a/LunaHost/GUI/Plugin/extensions/styler.cpp b/LunaHost/GUI/Plugin/extensions/styler.cpp new file mode 100644 index 0000000..304a8b3 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/styler.cpp @@ -0,0 +1,54 @@ +#include "qtcommon.h" +#include "extension.h" +#include + +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; +} diff --git a/LunaHost/GUI/Plugin/extensions/text.cpp b/LunaHost/GUI/Plugin/extensions/text.cpp new file mode 100644 index 0000000..cef6372 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/text.cpp @@ -0,0 +1,1417 @@ +#ifdef _WIN64 +#define ARCH "x64" +#else +#define ARCH "x86" +#endif + +#if 0 +#define TURKISH +#define SPANISH +#define SIMPLIFIED_CHINESE +#define RUSSIAN +#define INDONESIAN +#define ITALIAN +#define THAI +#define PORTUGUESE +#define KOREAN +#define FRENCH +#endif + +// If you are updating a previous translation see https://github.com/Artikash/Textractor/issues/313 + +const char* NATIVE_LANGUAGE = "English"; +const char* ATTACH = u8"Attach to game"; +const char* LAUNCH = u8"Launch game"; +const char* CONFIG = u8"Configure game"; +const char* DETACH = u8"Detach from game"; +const char* FORGET = u8"Forget game"; +const char* ADD_HOOK = u8"Add hook"; +const char* REMOVE_HOOKS = u8"Remove hook(s)"; +const char* SAVE_HOOKS = u8"Save hook(s)"; +const char* SEARCH_FOR_HOOKS = u8"Search for hooks"; +const char* SETTINGS = u8"Settings"; +const char* EXTENSIONS = u8"Extensions"; +const char* SELECT_PROCESS = u8"Select process"; +const char* ATTACH_INFO = u8R"(If you don't see the process you want to attach, try running with admin rights +You can also type in the process ID)"; +const char* SELECT_PROCESS_INFO = u8"If you manually type in the process file name, use the absolute path"; +const char* FROM_COMPUTER = u8"Select from computer"; +const char* PROCESSES = u8"Processes (*.exe)"; +const char* CODE_INFODUMP = u8R"(Enter read code +R{S|Q|V|M}[null_length<][codepage#]@addr +OR +Enter hook code +H{A|B|W|H|S|Q|V|M}[F][null_length<][N][codepage#][padding+]data_offset[*deref_offset][:split_offset[*deref_offset]]@addr[:module[:func]] +All numbers except codepage/null_length in hexadecimal +Default codepage is 932 (Shift-JIS) but this can be changed in settings +A/B: codepage char little/big endian +W: UTF-16 char +H: Two hex bytes +S/Q/V/M: codepage/UTF-16/UTF-8/hex string +F: treat strings as full lines of text +N: don't use context +null_length: length of null terminator used for string +padding: length of padding data before string (C struct { int64_t size; char string[500]; } needs padding = 8) +Negatives for data_offset/split_offset refer to registers +-4 for EAX, -8 for ECX, -C for EDX, -10 for EBX, -14 for ESP, -18 for EBP, -1C for ESI, -20 for EDI +-C for RAX, -14 for RBX, -1C for RCX, -24 for RDX, and so on for RSP, RBP, RSI, RDI, R8-R15 +* means dereference pointer+deref_offset)"; +const char* SAVE_SETTINGS = u8"Save settings"; +const char* EXTEN_WINDOW_INSTRUCTIONS = u8R"(Right click the list to add or remove extensions +Drag and drop extensions within the list to reorder them +(Extensions are used from top to bottom: order DOES matter))"; +const char* ADD_EXTENSION = u8"Add extension"; +const char* REMOVE_EXTENSION = u8"Remove extension"; +const char* INVALID_EXTENSION = u8"%1 is an invalid extension"; +const char* CONFIRM_EXTENSION_OVERWRITE = u8"Another version of this extension already exists, do you want to delete and overwrite it?"; +const char* EXTENSION_WRITE_ERROR = u8"Failed to save extension"; +const char* USE_JP_LOCALE = u8"Emulate japanese locale?"; +const char* FAILED_TO_CREATE_CONFIG_FILE = u8"Failed to create config file \"%1\""; +const char* HOOK_SEARCH_UNSTABLE_WARNING = u8"Searching for hooks is unstable! Be prepared for your game to crash!"; +const char* HOOK_SEARCH_STARTING_VIEW_CONSOLE = u8"Initializing hook search - please check console for further instructions"; +const char* SEARCH_CJK = u8"Search for Chinese/Japanese/Korean"; +const char* SEARCH_PATTERN = u8"Search pattern (hex byte array)"; +const char* SEARCH_DURATION = u8"Search duration (ms)"; +const char* SEARCH_MODULE = u8"Search within module"; +const char* PATTERN_OFFSET = u8"Offset from pattern start"; +const char* MAX_HOOK_SEARCH_RECORDS = u8"Search result cap"; +const char* MIN_ADDRESS = u8"Minimum address (hex)"; +const char* MAX_ADDRESS = u8"Maximum address (hex)"; +const char* STRING_OFFSET = u8"String offset (hex)"; +const char* HOOK_SEARCH_FILTER = u8"Results must match this regex"; +const char* TEXT = u8"Text"; +const char* CODEPAGE = u8"Codepage"; +const char* SEARCH_FOR_TEXT = u8"Search for specific text"; +const char* START_HOOK_SEARCH = u8"Start hook search"; +const char* SAVE_SEARCH_RESULTS = u8"Save search results"; +const char* TEXT_FILES = u8"Text (*.txt)"; +const char* DOUBLE_CLICK_TO_REMOVE_HOOK = u8"Double click a hook to remove it"; +const char* FILTER_REPETITION = u8"Filter repetition"; +const char* AUTO_ATTACH = u8"Auto attach"; +const char* ATTACH_SAVED_ONLY = u8"Auto attach (saved only)"; +const char* SHOW_SYSTEM_PROCESSES = u8"Show system processes"; +const char* DEFAULT_CODEPAGE = u8"Default codepage"; +const char* FLUSH_DELAY = u8"Flush delay"; +const char* MAX_BUFFER_SIZE = u8"Max buffer size"; +const char* MAX_HISTORY_SIZE = u8"Max history size"; +const char* CONFIG_JP_LOCALE = u8"Launch with JP locale"; +const wchar_t* CONSOLE = L"Console"; +const wchar_t* CLIPBOARD = L"Clipboard"; +const wchar_t* ABOUT = L"Textractor " ARCH L" v" VERSION LR"( made by Artikash (email: akashmozumdar@gmail.com) +Project homepage: https://github.com/Artikash/Textractor +Tutorial video: https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md +FAQ: https://github.com/Artikash/Textractor/wiki/FAQ +Please contact Artikash with any problems, feature requests, or questions relating to Textractor +You can do so via the project homepage (issues section) or via email +Source code available under GPLv3 at project homepage +If you like this project, please tell everyone about it! It's time to put AGTH down :))"; +const wchar_t* CL_OPTIONS = LR"(usage: Textractor [-p{process ID|"process name"}]... +example: Textractor -p4466 -p"My Game.exe" tries to inject processes with ID 4466 or with name My Game.exe)"; +const wchar_t* UPDATE_AVAILABLE = L"Update available: download it from https://github.com/Artikash/Textractor/releases"; +const wchar_t* ALREADY_INJECTED = L"Textractor: already injected"; +const wchar_t* NEED_32_BIT = L"Textractor: architecture mismatch: only Textractor x86 can inject this process"; +const wchar_t* NEED_64_BIT = L"Textractor: architecture mismatch: only Textractor x64 can inject this process"; +const wchar_t* INJECT_FAILED = L"Textractor: couldn't inject"; +const wchar_t* LAUNCH_FAILED = L"Textractor: couldn't launch"; +const wchar_t* INVALID_CODE = L"Textractor: invalid code"; +const wchar_t* INVALID_CODEPAGE = L"Textractor: couldn't convert text (invalid codepage?)"; +const char* PIPE_CONNECTED = u8"Textractor: pipe connected"; +const char* INSERTING_HOOK = u8"Textractor: inserting hook: %s"; +const char* REMOVING_HOOK = u8"Textractor: removing hook: %s"; +const char* HOOK_FAILED = u8"Textractor: failed to insert hook"; +const char* TOO_MANY_HOOKS = u8"Textractor: too many hooks: can't insert"; +const char* HOOK_SEARCH_STARTING = u8"Textractor: starting hook search"; +const char* HOOK_SEARCH_INITIALIZING = u8"Textractor: initializing hook search (%f%%)"; +const char* NOT_ENOUGH_TEXT = u8"Textractor: not enough text to search accurately"; +const char* HOOK_SEARCH_INITIALIZED = u8"Textractor: initialized hook search with %zd hooks"; +const char* MAKE_GAME_PROCESS_TEXT = u8"Textractor: please click around in the game to force it to process text during the next %d seconds"; +const char* HOOK_SEARCH_FINISHED = u8"Textractor: hook search finished, %d results found"; +const char* OUT_OF_RECORDS_RETRY = u8"Textractor: out of search records, please retry if results are poor (default record count increased)"; +const char* FUNC_MISSING = u8"Textractor: function not present"; +const char* MODULE_MISSING = u8"Textractor: module not present"; +const char* GARBAGE_MEMORY = u8"Textractor: memory constantly changing, useless to read"; +const char* SEND_ERROR = u8"Textractor: Send ERROR (likely an unstable/incorrect H-code)"; +const char* READ_ERROR = u8"Textractor: Reader ERROR (likely an incorrect R-code)"; +const char* HIJACK_ERROR = u8"Textractor: Hijack ERROR"; +const char* COULD_NOT_FIND = u8"Textractor: could not find text"; +const char* TRANSLATE_TO = u8"Translate to"; +const char* TRANSLATE_FROM = u8"Translate from"; +const char* FILTER_GARBAGE = u8"Filter garbage characters"; +const char* TRANSLATE_SELECTED_THREAD_ONLY = u8"Translate selected text thread only"; +const char* RATE_LIMIT_ALL_THREADS = u8"Use rate limiter"; +const char* RATE_LIMIT_SELECTED_THREAD = u8"Rate limit selected text thread"; +const char* USE_TRANS_CACHE = u8"Use translation cache"; +const char* MAX_TRANSLATIONS_IN_TIMESPAN = u8"Max translation requests in timespan"; +const char* TIMESPAN = u8"Timespan (ms)"; +const wchar_t* SENTENCE_TOO_LARGE_TO_TRANS = L"Sentence too large to translate"; +const wchar_t* TOO_MANY_TRANS_REQUESTS = L"Rate limit exceeded: refuse to make more translation requests"; +const wchar_t* TRANSLATION_ERROR = L"Error while translating"; +const char* USE_PREV_SENTENCE_CONTEXT = u8"Use previous sentence as context"; +const char* API_KEY = u8"API key"; +const char* CHROME_LOCATION = u8"Google Chrome file location"; +const char* START_DEVTOOLS = u8"Start DevTools"; +const char* STOP_DEVTOOLS = u8"Stop DevTools"; +const char* HIDE_CHROME = u8"Hide Chrome window"; +const char* DEVTOOLS_STATUS = u8"DevTools status"; +const char* AUTO_START = u8"Start automatically"; +const wchar_t* ERROR_START_CHROME = L"failed to start Chrome or to connect to it"; +const char* EXTRA_WINDOW_INFO = u8R"(Right click to change settings +Click and drag on window edges to move, or the bottom right corner to resize)"; +const char* MAX_SENTENCE_SIZE = u8"Max sentence size"; +const char* TOPMOST = u8"Always on top"; +const char* DICTIONARY = u8"Dictionary"; +const char* DICTIONARY_INSTRUCTIONS = u8R"(This file is used only for the "Dictionary" feature of the Extra Window extension. +It uses a custom format specific to Textractor and is not meant to be written manually. +You should look for a dictionary in this format online (https://github.com/Artikash/Textractor-Dictionaries/releases is a good place to start). +Alternatively, if you're a programmer, you can write a script to convert a dictionary from another format with the info below. +Once you have a dictionary, to look up some text in Extra Window, hover over it. You can scroll through all the matching definitions. +Definitions are formatted like this:|TERM|Hola< + +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> links; +std::unordered_set 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; +} diff --git a/LunaHost/GUI/Plugin/extensions/threadlinker.ui b/LunaHost/GUI/Plugin/extensions/threadlinker.ui new file mode 100644 index 0000000..4788efc --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/threadlinker.ui @@ -0,0 +1,47 @@ + + + LinkWindow + + + + 0 + 0 + 400 + 300 + + + + + + + + + + + + Qt::Vertical + + + + + + + + + + + + + + + Qt::Vertical + + + + + + + + + + diff --git a/LunaHost/GUI/Plugin/extensions/translatewrapper.cpp b/LunaHost/GUI/Plugin/extensions/translatewrapper.cpp new file mode 100644 index 0000000..0043c04 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/translatewrapper.cpp @@ -0,0 +1,212 @@ +#include "qtcommon.h" +#include "extension.h" +#include "translatewrapper.h" +#include "blockmarkup.h" +#include +#include +#include + +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 Translate(const std::wstring& text, TranslationParam tlp); + +QFormLayout* display; +Settings settings; + +namespace +{ + Synchronized tlp; + Synchronized> 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{ 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{ + { 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{ + { 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(&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("%2").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> 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 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"?")); + } +); diff --git a/LunaHost/GUI/Plugin/extensions/translatewrapper.h b/LunaHost/GUI/Plugin/extensions/translatewrapper.h new file mode 100644 index 0000000..9d34a38 --- /dev/null +++ b/LunaHost/GUI/Plugin/extensions/translatewrapper.h @@ -0,0 +1,6 @@ +#pragma once + +struct TranslationParam +{ + std::wstring translateTo, translateFrom, authKey; +}; diff --git a/LunaHost/GUI/Plugin/pluginexample.cpp b/LunaHost/GUI/Plugin/pluginexample.cpp deleted file mode 100644 index 1ec864f..0000000 --- a/LunaHost/GUI/Plugin/pluginexample.cpp +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/LunaHost/GUI/pluginmanager.cpp b/LunaHost/GUI/pluginmanager.cpp index 94a301a..949b42e 100644 --- a/LunaHost/GUI/pluginmanager.cpp +++ b/LunaHost/GUI/pluginmanager.cpp @@ -1,11 +1,12 @@ #include"pluginmanager.h" #include -#include"Plugin/plugindef.h" +#include"Plugin/extension.h" #include #include #include"LunaHost.h" #include"Lang/Lang.h" #include"host.h" +typedef wchar_t* (*OnNewSentence_t)(wchar_t*, const InfoForExtension*); std::optionalSelectFile(HWND hwnd,LPCWSTR lpstrFilter){ OPENFILENAME ofn; diff --git a/LunaHost/GUI/pluginmanager.h b/LunaHost/GUI/pluginmanager.h index 1f9948b..1918846 100644 --- a/LunaHost/GUI/pluginmanager.h +++ b/LunaHost/GUI/pluginmanager.h @@ -1,6 +1,6 @@ #ifndef LUNA_PLUGINMANAGER_H #define LUNA_PLUGINMANAGER_H -#include"Plugin/plugindef.h" +#include"Plugin/extension.h" #include"textthread.h" #include class LunaHost; diff --git a/scripts/pack.bat b/scripts/pack.bat new file mode 100644 index 0000000..ec27cab --- /dev/null +++ b/scripts/pack.bat @@ -0,0 +1 @@ +python pack.py \ No newline at end of file diff --git a/scripts/pack.py b/scripts/pack.py new file mode 100644 index 0000000..9e81863 --- /dev/null +++ b/scripts/pack.py @@ -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}') \ No newline at end of file