Add resource downloader/updater
This commit is contained in:
		
							parent
							
								
									c5889e1374
								
							
						
					
					
						commit
						40824e60de
					
				| @ -1,7 +1,4 @@ | ||||
| <RCC> | ||||
|     <qresource prefix="/"> | ||||
|         <file>version.json</file> | ||||
|     </qresource> | ||||
|     <qresource prefix="/icons"> | ||||
|         <file alias="app.png">share/images/STIconBlue.png</file> | ||||
|         <file alias="st_success.png">share/images/STIconGreen.png</file> | ||||
|  | ||||
| @ -44,6 +44,7 @@ HEADERS += \ | ||||
|   src/service/debug.h \ | ||||
|   src/service/globalaction.h \ | ||||
|   src/service/singleapplication.h \ | ||||
|   src/service/updates.h \ | ||||
|   src/service/widgetstate.h \ | ||||
|   src/settings.h \ | ||||
|   src/settingseditor.h \ | ||||
| @ -71,6 +72,7 @@ SOURCES += \ | ||||
|   src/service/debug.cpp \ | ||||
|   src/service/globalaction.cpp \ | ||||
|   src/service/singleapplication.cpp \ | ||||
|   src/service/updates.cpp \ | ||||
|   src/service/widgetstate.cpp \ | ||||
|   src/settings.cpp \ | ||||
|   src/settingseditor.cpp \ | ||||
|  | ||||
							
								
								
									
										36
									
								
								share/updates/tessdata.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								share/updates/tessdata.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| import sys | ||||
| import os | ||||
| import subprocess | ||||
| 
 | ||||
| if len(sys.argv) < 2: | ||||
|     print("Usage:", sys.argv[0], "<tessdata_dir> [<download_url>]") | ||||
|     exit(1) | ||||
| 
 | ||||
| tessdata_dir = sys.argv[1] | ||||
| 
 | ||||
| 
 | ||||
| download_url = "https://github.com/tesseract-ocr/tessdata_best/raw/master" | ||||
| if len(sys.argv) > 2: | ||||
|     download_url = sys.argv[2] | ||||
| 
 | ||||
| files = {} | ||||
| with os.scandir(tessdata_dir) as it: | ||||
|     for f in it: | ||||
|         if not f.is_file() or f.name in ["LICENSE", "README.md"]: | ||||
|             continue | ||||
|         name = f.name[:f.name.index('.')] | ||||
|         files.setdefault(name, []).append(f.name) | ||||
| 
 | ||||
| print(',"recognizers": {') | ||||
| comma = '' | ||||
| for name, file_names in files.items(): | ||||
|     print(' {}"{}":{{"files":['.format(comma, name)) | ||||
|     comma = ', ' | ||||
|     for file_name in file_names: | ||||
|         git_cmd = ['git', 'log', '-1', '--pretty=format:%cI', file_name] | ||||
|         date = subprocess.run(git_cmd, cwd=tessdata_dir, universal_newlines=True, | ||||
|                               stdout=subprocess.PIPE, check=True).stdout | ||||
|         print('  {{"url":"{}/{}", "path":"$tessdata$/{}", "date":"{}"}}'.format( | ||||
|             download_url, file_name, file_name, date)) | ||||
|     print(' ]}') | ||||
| print('}') | ||||
							
								
								
									
										33
									
								
								share/updates/translators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								share/updates/translators.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| import sys | ||||
| import os | ||||
| import hashlib | ||||
| 
 | ||||
| download_url = "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master" | ||||
| 
 | ||||
| if len(sys.argv) > 1: | ||||
|     download_url = sys.argv[1] | ||||
| 
 | ||||
| subdir = 'translators' | ||||
| root = os.path.abspath(os.path.basename(__file__) + '/..') | ||||
| translators_dir = root + '/' + subdir | ||||
| 
 | ||||
| files = {} | ||||
| with os.scandir(translators_dir) as it: | ||||
|     for f in it: | ||||
|         if not f.is_file() or not f.name.endswith('.js'): | ||||
|             continue | ||||
|         name = f.name[:f.name.index('.')] | ||||
|         files[name] = f.name | ||||
| 
 | ||||
| print(',"translators":{') | ||||
| comma = '' | ||||
| for name, file_name in files.items(): | ||||
|     print(' {}"{}": {{"files":['.format(comma, name)) | ||||
|     comma = ',' | ||||
|     md5 = hashlib.md5() | ||||
|     with open(os.path.join(translators_dir, file_name), 'rb') as f: | ||||
|         md5.update(f.read()) | ||||
|     print('  {{"url":"{}/{}", "path":"$translators$/{}", "md5":"{}"}}'.format( | ||||
|         download_url, subdir + '/' + file_name, file_name, md5.hexdigest())) | ||||
|     print(' ]}') | ||||
| print('}') | ||||
| @ -8,13 +8,26 @@ | ||||
| #include "task.h" | ||||
| #include "translator.h" | ||||
| #include "trayicon.h" | ||||
| #include "updates.h" | ||||
| 
 | ||||
| #include <QApplication> | ||||
| #include <QClipboard> | ||||
| #include <QMessageBox> | ||||
| #include <QNetworkProxy> | ||||
| 
 | ||||
| namespace | ||||
| { | ||||
| #ifdef DEVELOP | ||||
| const auto updatesUrl = "http://localhost:8081/updates.json"; | ||||
| #else | ||||
| const auto updatesUrl = | ||||
|     "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/" | ||||
|     "updates.json"; | ||||
| #endif | ||||
| }  // namespace
 | ||||
| 
 | ||||
| Manager::Manager() | ||||
|   : updater_(std::make_unique<update::Loader>(QUrl(updatesUrl))) | ||||
| { | ||||
|   tray_ = std::make_unique<TrayIcon>(*this); | ||||
|   capturer_ = std::make_unique<Capturer>(*this); | ||||
| @ -31,6 +44,16 @@ Manager::Manager() | ||||
| 
 | ||||
|   if (settings.showMessageOnStart) | ||||
|     tray_->showInformation(QObject::tr("Screen translator started")); | ||||
| 
 | ||||
|   QObject::connect(updater_.get(), &update::Loader::error,  //
 | ||||
|                    tray_.get(), &TrayIcon::showError); | ||||
|   QObject::connect(updater_.get(), &update::Loader::updated,  //
 | ||||
|                    tray_.get(), [this] { | ||||
|                      tray_->showInformation(QObject::tr("Update completed")); | ||||
|                    }); | ||||
| #ifdef DEVELOP | ||||
|   updater_->checkForUpdates(); | ||||
| #endif | ||||
| } | ||||
| 
 | ||||
| Manager::~Manager() = default; | ||||
| @ -40,6 +63,11 @@ void Manager::updateSettings(const Settings &settings) | ||||
|   LTRACE() << "updateSettings"; | ||||
|   setupProxy(settings); | ||||
| 
 | ||||
|   updater_->model()->setExpansions({ | ||||
|       {"$translators$", settings.translatorsDir}, | ||||
|       {"$tessdata$", settings.tessdataPath}, | ||||
|   }); | ||||
| 
 | ||||
|   tray_->updateSettings(settings); | ||||
|   capturer_->updateSettings(settings); | ||||
|   recognizer_->updateSettings(settings); | ||||
| @ -188,7 +216,7 @@ void Manager::showLast() | ||||
| 
 | ||||
| void Manager::settings() | ||||
| { | ||||
|   SettingsEditor editor; | ||||
|   SettingsEditor editor(*updater_); | ||||
| 
 | ||||
|   Settings settings; | ||||
|   settings.load(); | ||||
|  | ||||
| @ -36,6 +36,7 @@ private: | ||||
|   std::unique_ptr<Corrector> corrector_; | ||||
|   std::unique_ptr<Translator> translator_; | ||||
|   std::unique_ptr<Representer> representer_; | ||||
|   std::unique_ptr<update::Loader> updater_; | ||||
|   TaskPtr last_; | ||||
|   int activeTaskCount_{0}; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										624
									
								
								src/service/updates.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										624
									
								
								src/service/updates.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,624 @@ | ||||
| #include "updates.h" | ||||
| #include "debug.h" | ||||
| 
 | ||||
| #include <QComboBox> | ||||
| #include <QDir> | ||||
| #include <QJsonArray> | ||||
| #include <QJsonDocument> | ||||
| #include <QJsonObject> | ||||
| #include <QNetworkReply> | ||||
| #include <QStandardPaths> | ||||
| 
 | ||||
| namespace update | ||||
| { | ||||
| namespace | ||||
| { | ||||
| const auto versionKey = "version"; | ||||
| const auto filesKey = "files"; | ||||
| 
 | ||||
| QString toString(State state) | ||||
| { | ||||
|   const QMap<State, QString> names{ | ||||
|       {State::NotAvailable, {}}, | ||||
|       {State::NotInstalled, QObject::tr("Not installed")}, | ||||
|       {State::UpdateAvailable, QObject::tr("Update available")}, | ||||
|       {State::Actual, QObject::tr("Up to date")}, | ||||
|   }; | ||||
|   return names.value(state); | ||||
| } | ||||
| 
 | ||||
| QString toString(Action action) | ||||
| { | ||||
|   const QMap<Action, QString> names{ | ||||
|       {Action::NoAction, {}}, | ||||
|       {Action::Remove, QObject::tr("Remove")}, | ||||
|       {Action::Install, QObject::tr("Install/Update")}, | ||||
|   }; | ||||
|   return names.value(action); | ||||
| } | ||||
| 
 | ||||
| }  // namespace
 | ||||
| 
 | ||||
| Loader::Loader(const QUrl &updateUrl, QObject *parent) | ||||
|   : QObject(parent) | ||||
|   , network_(new QNetworkAccessManager(this)) | ||||
|   , model_(new Model(this)) | ||||
|   , updateUrl_(updateUrl) | ||||
|   , downloadPath_( | ||||
|         QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + | ||||
|         "/updates") | ||||
| { | ||||
|   connect(network_, &QNetworkAccessManager::finished,  //
 | ||||
|           this, &Loader::handleReply); | ||||
| } | ||||
| 
 | ||||
| void Loader::checkForUpdates() | ||||
| { | ||||
|   auto reply = network_->get(QNetworkRequest(updateUrl_)); | ||||
|   if (reply->error() == QNetworkReply::NoError) | ||||
|     return; | ||||
| 
 | ||||
|   reply->deleteLater(); | ||||
|   emit error(toError(*reply)); | ||||
| } | ||||
| 
 | ||||
| QString Loader::toError(QNetworkReply &reply) const | ||||
| { | ||||
|   return tr("Failed to download file %1. Error %2") | ||||
|       .arg(reply.url().toString(), reply.errorString()); | ||||
| } | ||||
| 
 | ||||
| void Loader::applyUserActions() | ||||
| { | ||||
|   SOFT_ASSERT(model_, return ); | ||||
|   if (installer_ || !componentReplyToPath_.empty()) { | ||||
|     emit error(tr("Update already in process")); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   auto actions = model_->userActions(); | ||||
|   if (actions.empty()) { | ||||
|     emit error(tr("No actions to apply")); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   for (auto &action : actions) { | ||||
|     if (action.first != Action::Install) | ||||
|       continue; | ||||
| 
 | ||||
|     auto &file = action.second; | ||||
| 
 | ||||
|     auto reply = network_->get(QNetworkRequest(file.url)); | ||||
|     if (reply->error() != QNetworkReply::NoError) { | ||||
|       finishUpdate(toError(*reply)); | ||||
|       break; | ||||
|     } | ||||
| 
 | ||||
|     file.downloadPath = downloadPath_ + '/' + file.rawPath; | ||||
|     componentReplyToPath_.emplace(reply, file.downloadPath); | ||||
|   } | ||||
| 
 | ||||
|   installer_ = std::make_unique<Installer>(actions); | ||||
| 
 | ||||
|   if (componentReplyToPath_.empty())  // no downloads
 | ||||
|     commitUpdate(); | ||||
| } | ||||
| 
 | ||||
| void Loader::finishUpdate(const QString &error) | ||||
| { | ||||
|   installer_.reset(); | ||||
|   for (const auto &i : componentReplyToPath_) i.first->deleteLater(); | ||||
|   componentReplyToPath_.clear(); | ||||
|   if (!error.isEmpty()) | ||||
|     emit this->error(error); | ||||
|   SOFT_ASSERT(model_, return ); | ||||
|   model_->updateStates(); | ||||
| } | ||||
| 
 | ||||
| void Loader::handleReply(QNetworkReply *reply) | ||||
| { | ||||
|   reply->deleteLater(); | ||||
| 
 | ||||
|   const auto isUpdatesReply = reply->url() == updateUrl_; | ||||
| 
 | ||||
|   if (reply->error() != QNetworkReply::NoError) { | ||||
|     emit error(toError(*reply)); | ||||
|     if (!isUpdatesReply) | ||||
|       finishUpdate(); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   const auto replyData = reply->readAll(); | ||||
| 
 | ||||
|   if (isUpdatesReply) { | ||||
|     SOFT_ASSERT(model_, return ); | ||||
|     model_->parse(replyData); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   SOFT_ASSERT(componentReplyToPath_.count(reply) == 1, return ); | ||||
| 
 | ||||
|   auto replyIt = componentReplyToPath_.find(reply); | ||||
|   const auto &fileName = replyIt->second; | ||||
| 
 | ||||
|   auto dir = QFileInfo(fileName).absoluteDir(); | ||||
|   if (!dir.exists()) | ||||
|     dir.mkpath("."); | ||||
| 
 | ||||
|   QFile f(fileName); | ||||
|   if (!f.open(QFile::WriteOnly)) { | ||||
|     const auto error = | ||||
|         tr("Failed to save downloaded file %1 to %2. Error %3") | ||||
|             .arg(reply->url().toString(), f.fileName(), f.errorString()); | ||||
|     finishUpdate(error); | ||||
|     return; | ||||
|   } | ||||
|   f.write(replyData); | ||||
|   f.close(); | ||||
| 
 | ||||
|   componentReplyToPath_.erase(replyIt); | ||||
| 
 | ||||
|   if (componentReplyToPath_.empty()) | ||||
|     commitUpdate(); | ||||
| } | ||||
| 
 | ||||
| void Loader::commitUpdate() | ||||
| { | ||||
|   SOFT_ASSERT(installer_, return ); | ||||
|   if (installer_->commit()) { | ||||
|     emit updated(); | ||||
|   } else { | ||||
|     emit error(tr("Update failed: %1").arg(installer_->errorString())); | ||||
|   } | ||||
|   finishUpdate(); | ||||
| } | ||||
| 
 | ||||
| Model *Loader::model() const | ||||
| { | ||||
|   return model_; | ||||
| } | ||||
| 
 | ||||
| Model::Model(QObject *parent) | ||||
|   : QAbstractItemModel(parent) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| void Model::parse(const QByteArray &data) | ||||
| { | ||||
|   QJsonParseError error; | ||||
|   const auto doc = QJsonDocument::fromJson(data, &error); | ||||
|   if (doc.isNull()) { | ||||
|     LERROR() << error.errorString(); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   const auto json = doc.object(); | ||||
|   const auto version = json[versionKey].toInt(); | ||||
|   if (version != 1) { | ||||
|     LERROR() << "Wrong updates.json version" << version; | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   beginResetModel(); | ||||
| 
 | ||||
|   root_ = parse(json); | ||||
|   if (root_) | ||||
|     updateState(*root_); | ||||
| 
 | ||||
|   endResetModel(); | ||||
| } | ||||
| 
 | ||||
| std::unique_ptr<Model::Component> Model::parse(const QJsonObject &json) const | ||||
| { | ||||
|   auto result = std::make_unique<Component>(); | ||||
| 
 | ||||
|   if (json[filesKey].isArray()) {  // concrete component
 | ||||
|     const auto host = json["host"].toString().toLower(); | ||||
|     if (!host.isEmpty()) { | ||||
| #if defined(Q_OS_LINUX) && defined(Q_PROCESSOR_X86_64) | ||||
|       if (host != "linux") | ||||
|         return {}; | ||||
| #elif defined(Q_OS_WINDOWS) && defined(Q_PROCESSOR_X86_64) | ||||
|       if (host != "win64") | ||||
|         return {}; | ||||
| #elif defined(Q_OS_WINDOWS) && defined(Q_PROCESSOR_X86) | ||||
|       if (host != "win32") | ||||
|         return {}; | ||||
| #elif defined(Q_OS_MACOS) | ||||
|       if (host != "macos") | ||||
|         return {}; | ||||
| #else | ||||
|       return {}; | ||||
| #endif | ||||
|     } | ||||
| 
 | ||||
|     result->version = json["version"].toString(); | ||||
| 
 | ||||
|     const auto files = json[filesKey].toArray(); | ||||
|     result->files.reserve(files.size()); | ||||
| 
 | ||||
|     for (const auto &fileInfo : files) { | ||||
|       const auto object = fileInfo.toObject(); | ||||
|       File file; | ||||
|       file.url = object["url"].toString(); | ||||
|       file.rawPath = object["path"].toString(); | ||||
|       file.md5 = object["md5"].toString(); | ||||
|       file.versionDate = | ||||
|           QDateTime::fromString(object["date"].toString(), Qt::ISODate); | ||||
|       result->files.push_back(file); | ||||
|     } | ||||
| 
 | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   result->children.reserve(json.size()); | ||||
|   auto index = -1; | ||||
|   for (const auto &name : json.keys()) { | ||||
|     if (name == versionKey) | ||||
|       continue; | ||||
| 
 | ||||
|     auto child = parse(json[name].toObject()); | ||||
|     if (!child) | ||||
|       continue; | ||||
|     child->name = name; | ||||
|     child->index = ++index; | ||||
|     child->parent = result.get(); | ||||
|     result->children.push_back(std::move(child)); | ||||
|   } | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| void Model::setExpansions(const std::map<QString, QString> &expansions) | ||||
| { | ||||
|   expansions_ = expansions; | ||||
|   updateStates(); | ||||
| } | ||||
| 
 | ||||
| UserActions Model::userActions() const | ||||
| { | ||||
|   if (!root_) | ||||
|     return {}; | ||||
| 
 | ||||
|   UserActions result; | ||||
|   fillUserActions(result, *root_); | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| void Model::fillUserActions(UserActions &actions, Component &component) const | ||||
| { | ||||
|   if (!component.files.empty()) { | ||||
|     if (component.action == Action::NoAction) | ||||
|       return; | ||||
| 
 | ||||
|     for (auto &file : component.files) actions.emplace(component.action, file); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (!component.children.empty()) { | ||||
|     for (auto &child : component.children) fillUserActions(actions, *child); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| void Model::updateStates() | ||||
| { | ||||
|   if (!root_) | ||||
|     return; | ||||
| 
 | ||||
|   updateState(*root_); | ||||
|   emitColumnsChanged(QModelIndex()); | ||||
| } | ||||
| 
 | ||||
| void Model::updateState(Model::Component &component) | ||||
| { | ||||
|   if (!component.files.empty()) { | ||||
|     std::vector<State> states; | ||||
|     states.reserve(component.files.size()); | ||||
| 
 | ||||
|     for (auto &file : component.files) { | ||||
|       file.expandedPath = expanded(file.rawPath); | ||||
|       states.push_back(currentState(file)); | ||||
|     } | ||||
| 
 | ||||
|     component.state = *std::min_element(states.cbegin(), states.cend()); | ||||
|     component.action = Action::NoAction; | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (!component.children.empty()) { | ||||
|     for (auto &child : component.children) updateState(*child); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| State Model::currentState(const File &file) const | ||||
| { | ||||
|   if (!file.url.isValid() || file.expandedPath.isEmpty() || | ||||
|       (file.md5.isEmpty() && !file.versionDate.isValid())) | ||||
|     return State::NotAvailable; | ||||
| 
 | ||||
|   if (!QFile::exists(file.expandedPath)) | ||||
|     return State::NotInstalled; | ||||
| 
 | ||||
|   if (file.versionDate.isValid()) { | ||||
|     QFileInfo info(file.expandedPath); | ||||
|     const auto date = info.fileTime(QFile::FileModificationTime); | ||||
|     if (!date.isValid()) | ||||
|       return State::NotInstalled; | ||||
|     return date >= file.versionDate ? State::Actual : State::UpdateAvailable; | ||||
|   } | ||||
| 
 | ||||
|   QFile f(file.expandedPath); | ||||
|   if (!f.open(QFile::ReadOnly)) | ||||
|     return State::NotInstalled; | ||||
| 
 | ||||
|   const auto data = f.readAll(); | ||||
|   const auto md5 = | ||||
|       QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex(); | ||||
|   if (md5 != file.md5) | ||||
|     return State::UpdateAvailable; | ||||
|   return State::Actual; | ||||
| } | ||||
| 
 | ||||
| QString Model::expanded(const QString &source) const | ||||
| { | ||||
|   auto result = source; | ||||
| 
 | ||||
|   for (const auto &expansion : expansions_) { | ||||
|     if (!result.contains(expansion.first)) | ||||
|       continue; | ||||
|     result.replace(expansion.first, expansion.second); | ||||
|   } | ||||
| 
 | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| void Model::emitColumnsChanged(const QModelIndex &parent) | ||||
| { | ||||
|   const auto count = rowCount(parent); | ||||
|   if (count == 0) | ||||
|     return; | ||||
| 
 | ||||
|   emit dataChanged(index(0, int(Column::State), parent), | ||||
|                    index(count - 1, int(Column::Action), parent), | ||||
|                    {Qt::DisplayRole, Qt::EditRole}); | ||||
| 
 | ||||
|   for (auto i = 0; i < count; ++i) emitColumnsChanged(index(0, 0, parent)); | ||||
| } | ||||
| 
 | ||||
| QModelIndex Model::index(int row, int column, const QModelIndex &parent) const | ||||
| { | ||||
|   if (!root_) | ||||
|     return {}; | ||||
|   if (auto ptr = static_cast<Component *>(parent.internalPointer())) { | ||||
|     SOFT_ASSERT(row >= 0 && row < int(ptr->children.size()), return {}); | ||||
|     return createIndex(row, column, ptr->children[row].get()); | ||||
|   } | ||||
|   if (row < 0 && row >= int(root_->children.size())) | ||||
|     return {}; | ||||
|   return createIndex(row, column, root_->children[row].get()); | ||||
| } | ||||
| 
 | ||||
| QModelIndex Model::parent(const QModelIndex &child) const | ||||
| { | ||||
|   auto ptr = static_cast<Component *>(child.internalPointer()); | ||||
|   if (auto parent = ptr->parent) | ||||
|     return createIndex(parent->index, 0, parent); | ||||
|   return {}; | ||||
| } | ||||
| 
 | ||||
| int Model::rowCount(const QModelIndex &parent) const | ||||
| { | ||||
|   if (auto ptr = static_cast<Component *>(parent.internalPointer())) { | ||||
|     return ptr->children.size(); | ||||
|   } | ||||
|   return root_ ? root_->children.size() : 0; | ||||
| } | ||||
| 
 | ||||
| int Model::columnCount(const QModelIndex & /*parent*/) const | ||||
| { | ||||
|   return int(Column::Count); | ||||
| } | ||||
| 
 | ||||
| QVariant Model::headerData(int section, Qt::Orientation orientation, | ||||
|                            int role) const | ||||
| { | ||||
|   if (role != Qt::DisplayRole) | ||||
|     return {}; | ||||
| 
 | ||||
|   if (orientation == Qt::Vertical) | ||||
|     return section + 1; | ||||
| 
 | ||||
|   const QMap<Column, QString> names{ | ||||
|       {Column::Name, tr("Name")},     {Column::State, tr("State")}, | ||||
|       {Column::Action, tr("Action")}, {Column::Version, tr("Version")}, | ||||
|       {Column::Files, tr("Files")}, | ||||
|   }; | ||||
|   return names.value(Column(section)); | ||||
| } | ||||
| 
 | ||||
| QVariant Model::data(const QModelIndex &index, int role) const | ||||
| { | ||||
|   if ((role != Qt::DisplayRole && role != Qt::EditRole) || !index.isValid()) | ||||
|     return {}; | ||||
| 
 | ||||
|   auto ptr = static_cast<Component *>(index.internalPointer()); | ||||
|   SOFT_ASSERT(ptr, return {}); | ||||
| 
 | ||||
|   switch (index.column()) { | ||||
|     case int(Column::Name): return ptr->name; | ||||
|     case int(Column::State): return toString(ptr->state); | ||||
|     case int(Column::Action): return toString(ptr->action); | ||||
|     case int(Column::Version): return ptr->version; | ||||
|     case int(Column::Files): { | ||||
|       QStringList files; | ||||
|       files.reserve(ptr->files.size()); | ||||
|       for (const auto &f : ptr->files) files.append(f.expandedPath); | ||||
|       return files.join(','); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return {}; | ||||
| } | ||||
| 
 | ||||
| bool Model::setData(const QModelIndex &index, const QVariant &value, int role) | ||||
| { | ||||
|   if (!index.isValid() || role != Qt::EditRole) | ||||
|     return false; | ||||
| 
 | ||||
|   auto ptr = static_cast<Component *>(index.internalPointer()); | ||||
|   SOFT_ASSERT(ptr, return false); | ||||
| 
 | ||||
|   if (index.column() != int(Column::Action)) | ||||
|     return false; | ||||
| 
 | ||||
|   const auto newAction = Action( | ||||
|       std::clamp(value.toInt(), int(Action::NoAction), int(Action::Install))); | ||||
|   if (ptr->action == newAction) | ||||
|     return false; | ||||
| 
 | ||||
|   ptr->action = newAction; | ||||
|   emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); | ||||
| 
 | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| Qt::ItemFlags Model::flags(const QModelIndex &index) const | ||||
| { | ||||
|   auto ptr = static_cast<Component *>(index.internalPointer()); | ||||
|   SOFT_ASSERT(ptr, return {}); | ||||
|   auto result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; | ||||
|   if (index.column() != int(Column::Action)) | ||||
|     return result; | ||||
| 
 | ||||
|   if (ptr->state == State::NotAvailable) | ||||
|     return result; | ||||
| 
 | ||||
|   result |= Qt::ItemIsEditable; | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| ActionDelegate::ActionDelegate(QObject *parent) | ||||
|   : QStyledItemDelegate(parent) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| QWidget *ActionDelegate::createEditor(QWidget *parent, | ||||
|                                       const QStyleOptionViewItem & /*option*/, | ||||
|                                       const QModelIndex & /*index*/) const | ||||
| { | ||||
|   auto combo = new QComboBox(parent); | ||||
|   combo->setEditable(false); | ||||
|   combo->addItems({toString(Action::NoAction), toString(Action::Remove), | ||||
|                    toString(Action::Install)}); | ||||
|   return combo; | ||||
| } | ||||
| 
 | ||||
| void ActionDelegate::setEditorData(QWidget *editor, | ||||
|                                    const QModelIndex &index) const | ||||
| { | ||||
|   auto combo = qobject_cast<QComboBox *>(editor); | ||||
|   SOFT_ASSERT(combo, return ); | ||||
|   combo->setCurrentText(index.data(Qt::EditRole).toString()); | ||||
| } | ||||
| 
 | ||||
| void ActionDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, | ||||
|                                   const QModelIndex &index) const | ||||
| { | ||||
|   auto combo = qobject_cast<QComboBox *>(editor); | ||||
|   SOFT_ASSERT(combo, return ); | ||||
|   model->setData(index, combo->currentIndex()); | ||||
| } | ||||
| 
 | ||||
| Installer::Installer(const UserActions &actions) | ||||
|   : actions_(actions) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| bool Installer::commit() | ||||
| { | ||||
|   if (!checkIsPossible()) | ||||
|     return false; | ||||
| 
 | ||||
|   for (const auto &action : actions_) { | ||||
|     const auto &file = action.second; | ||||
| 
 | ||||
|     if (action.first == Action::Remove) { | ||||
|       QFile f(file.expandedPath); | ||||
|       if (!f.exists()) | ||||
|         continue; | ||||
|       if (!f.remove()) { | ||||
|         errors_.append(QObject::tr("Failed to remove file %1. Error %2") | ||||
|                            .arg(f.fileName(), f.errorString())); | ||||
|       } | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
|     if (action.first == Action::Install) { | ||||
|       auto installDir = QFileInfo(file.expandedPath).absoluteDir(); | ||||
|       if (!installDir.exists() && !installDir.mkpath(".")) { | ||||
|         errors_.append(QObject::tr("Failed to create path %1") | ||||
|                            .arg(installDir.absolutePath())); | ||||
|         continue; | ||||
|       } | ||||
|       { | ||||
|         QFile existing(file.expandedPath); | ||||
|         if (existing.exists() && !existing.remove()) { | ||||
|           errors_.append(QObject::tr("Failed to remove file %1. Error %2") | ||||
|                              .arg(existing.fileName(), existing.errorString())); | ||||
|           continue; | ||||
|         } | ||||
|       } | ||||
|       QFile f(file.downloadPath); | ||||
|       if (!f.rename(file.expandedPath)) { | ||||
|         errors_.append( | ||||
|             QObject::tr("Failed to move file %1 to %2. Error %3") | ||||
|                 .arg(f.fileName(), file.expandedPath, f.errorString())); | ||||
|         continue; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return errors_.isEmpty(); | ||||
| } | ||||
| 
 | ||||
| bool Installer::checkIsPossible() | ||||
| { | ||||
|   errors_.clear(); | ||||
| 
 | ||||
|   for (const auto &action : actions_) { | ||||
|     const auto &file = action.second; | ||||
|     QFileInfo installDir(QFileInfo(file.expandedPath).absolutePath()); | ||||
| 
 | ||||
|     if (action.first == Action::Remove) { | ||||
|       if (!QFile::exists(file.expandedPath)) | ||||
|         continue; | ||||
|       if (installDir.exists() && !installDir.isWritable()) { | ||||
|         errors_.append(QObject::tr("Directory is not writable %1") | ||||
|                            .arg(installDir.absolutePath())); | ||||
|       } | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
|     if (action.first == Action::Install) { | ||||
|       if (!QFileInfo::exists(file.downloadPath)) { | ||||
|         errors_.append(QObject::tr("Downloaded file not exists %1") | ||||
|                            .arg(file.downloadPath)); | ||||
|       } | ||||
|       if (installDir.exists() && !installDir.isWritable()) { | ||||
|         errors_.append(QObject::tr("Directory is not writable %1") | ||||
|                            .arg(installDir.absolutePath())); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   errors_.removeDuplicates(); | ||||
| 
 | ||||
|   return errors_.isEmpty(); | ||||
| } | ||||
| 
 | ||||
| QString Installer::errorString() const | ||||
| { | ||||
|   return errors_.join('\n'); | ||||
| } | ||||
| 
 | ||||
| }  // namespace update
 | ||||
							
								
								
									
										128
									
								
								src/service/updates.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/service/updates.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,128 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <QDate> | ||||
| #include <QStyledItemDelegate> | ||||
| #include <QUrl> | ||||
| 
 | ||||
| class QNetworkAccessManager; | ||||
| class QNetworkReply; | ||||
| 
 | ||||
| namespace update | ||||
| { | ||||
| enum class State { NotAvailable, NotInstalled, UpdateAvailable, Actual }; | ||||
| enum class Action { NoAction, Remove, Install }; | ||||
| 
 | ||||
| struct File { | ||||
|   QUrl url; | ||||
|   QString rawPath; | ||||
|   QString expandedPath; | ||||
|   QString downloadPath; | ||||
|   QString md5; | ||||
|   QDateTime versionDate; | ||||
| }; | ||||
| 
 | ||||
| using UserActions = std::multimap<Action, File>; | ||||
| 
 | ||||
| class ActionDelegate : public QStyledItemDelegate | ||||
| { | ||||
|   Q_OBJECT | ||||
| public: | ||||
|   explicit ActionDelegate(QObject* parent = nullptr); | ||||
|   QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, | ||||
|                         const QModelIndex& index) const override; | ||||
|   void setEditorData(QWidget* editor, const QModelIndex& index) const override; | ||||
|   void setModelData(QWidget* editor, QAbstractItemModel* model, | ||||
|                     const QModelIndex& index) const override; | ||||
| }; | ||||
| 
 | ||||
| class Model : public QAbstractItemModel | ||||
| { | ||||
|   Q_OBJECT | ||||
| public: | ||||
|   enum class Column { Name, State, Action, Version, Files, Count }; | ||||
| 
 | ||||
|   explicit Model(QObject* parent = nullptr); | ||||
| 
 | ||||
|   void parse(const QByteArray& data); | ||||
|   void setExpansions(const std::map<QString, QString>& expansions); | ||||
|   UserActions userActions() const; | ||||
|   void updateStates(); | ||||
| 
 | ||||
|   QModelIndex index(int row, int column, | ||||
|                     const QModelIndex& parent) const override; | ||||
|   QModelIndex parent(const QModelIndex& child) const override; | ||||
|   int rowCount(const QModelIndex& parent) const override; | ||||
|   int columnCount(const QModelIndex& parent) const override; | ||||
|   QVariant headerData(int section, Qt::Orientation orientation, | ||||
|                       int role) const override; | ||||
|   QVariant data(const QModelIndex& index, int role) const override; | ||||
|   Qt::ItemFlags flags(const QModelIndex& index) const override; | ||||
|   bool setData(const QModelIndex& index, const QVariant& value, | ||||
|                int role) override; | ||||
| 
 | ||||
| private: | ||||
|   struct Component { | ||||
|     QString name; | ||||
|     State state{State::NotAvailable}; | ||||
|     Action action{Action::NoAction}; | ||||
|     QString version; | ||||
|     std::vector<File> files; | ||||
|     std::vector<std::unique_ptr<Component>> children; | ||||
|     Component* parent{nullptr}; | ||||
|     int index{-1}; | ||||
|   }; | ||||
| 
 | ||||
|   std::unique_ptr<Component> parse(const QJsonObject& json) const; | ||||
|   void updateState(Component& component); | ||||
|   void fillUserActions(UserActions& actions, Component& component) const; | ||||
|   State currentState(const File& file) const; | ||||
|   QString expanded(const QString& source) const; | ||||
|   void emitColumnsChanged(const QModelIndex& parent); | ||||
| 
 | ||||
|   std::unique_ptr<Component> root_; | ||||
|   std::map<QString, QString> expansions_; | ||||
| }; | ||||
| 
 | ||||
| class Installer | ||||
| { | ||||
| public: | ||||
|   explicit Installer(const UserActions& actions); | ||||
|   bool commit(); | ||||
|   QString errorString() const; | ||||
| 
 | ||||
| private: | ||||
|   bool checkIsPossible(); | ||||
| 
 | ||||
|   UserActions actions_; | ||||
|   QStringList errors_; | ||||
| }; | ||||
| 
 | ||||
| class Loader : public QObject | ||||
| { | ||||
|   Q_OBJECT | ||||
| public: | ||||
|   explicit Loader(const QUrl& updateUrl, QObject* parent = nullptr); | ||||
| 
 | ||||
|   void checkForUpdates(); | ||||
|   void applyUserActions(); | ||||
|   Model* model() const; | ||||
| 
 | ||||
| signals: | ||||
|   void updated(); | ||||
|   void error(const QString& error); | ||||
| 
 | ||||
| private: | ||||
|   void handleReply(QNetworkReply* reply); | ||||
|   QString toError(QNetworkReply& reply) const; | ||||
|   void finishUpdate(const QString& error = {}); | ||||
|   void commitUpdate(); | ||||
| 
 | ||||
|   QNetworkAccessManager* network_; | ||||
|   Model* model_; | ||||
|   QUrl updateUrl_; | ||||
|   QString downloadPath_; | ||||
|   std::map<QNetworkReply*, QString> componentReplyToPath_; | ||||
|   std::unique_ptr<Installer> installer_; | ||||
| }; | ||||
| 
 | ||||
| }  // namespace update
 | ||||
| @ -118,7 +118,7 @@ void Settings::save() | ||||
|     settings.remove(qs_proxyPassword); | ||||
|   } | ||||
| 
 | ||||
|   settings.setValue(qs_autoUpdateType, autoUpdateType); | ||||
|   settings.setValue(qs_autoUpdateType, int(autoUpdateType)); | ||||
| 
 | ||||
|   settings.endGroup(); | ||||
| 
 | ||||
| @ -178,7 +178,9 @@ void Settings::load() | ||||
|       settings.value(qs_proxySavePassword, proxySavePassword).toBool(); | ||||
|   proxyPassword = shuffle(settings.value(qs_proxyPassword).toString()); | ||||
| 
 | ||||
|   autoUpdateType = settings.value(qs_autoUpdateType, autoUpdateType).toInt(); | ||||
|   autoUpdateType = AutoUpdate( | ||||
|       std::clamp(settings.value(qs_autoUpdateType, int(autoUpdateType)).toInt(), | ||||
|                  int(AutoUpdate::Disabled), int(AutoUpdate::Monthly))); | ||||
| 
 | ||||
|   settings.endGroup(); | ||||
| 
 | ||||
|  | ||||
| @ -16,6 +16,8 @@ using Substitutions = std::unordered_multimap<LanguageId, Substitution>; | ||||
| 
 | ||||
| enum class ProxyType { Disabled, System, Socks5, Http }; | ||||
| 
 | ||||
| enum class AutoUpdate { Disabled, Daily, Weekly, Monthly }; | ||||
| 
 | ||||
| class Settings | ||||
| { | ||||
| public: | ||||
| @ -36,7 +38,7 @@ public: | ||||
|   QString proxyPassword; | ||||
|   bool proxySavePassword{false}; | ||||
| 
 | ||||
|   int autoUpdateType{0};  // Never
 | ||||
|   AutoUpdate autoUpdateType{AutoUpdate::Disabled}; | ||||
|   QString lastUpdateCheck{""}; | ||||
| 
 | ||||
|   Substitutions userSubstitutions; | ||||
|  | ||||
| @ -1,14 +1,17 @@ | ||||
| #include "settingseditor.h" | ||||
| #include "languagecodes.h" | ||||
| #include "ui_settingseditor.h" | ||||
| #include "updates.h" | ||||
| #include "widgetstate.h" | ||||
| 
 | ||||
| #include <QFileDialog> | ||||
| #include <QNetworkProxy> | ||||
| #include <QSortFilterProxyModel> | ||||
| #include <QStringListModel> | ||||
| 
 | ||||
| SettingsEditor::SettingsEditor() | ||||
| SettingsEditor::SettingsEditor(update::Loader &updater) | ||||
|   : ui(new Ui::SettingsEditor) | ||||
|   , updater_(updater) | ||||
| { | ||||
|   ui->setupUi(this); | ||||
| 
 | ||||
| @ -55,8 +58,30 @@ SettingsEditor::SettingsEditor() | ||||
|   updateTranslationLanguages(); | ||||
| 
 | ||||
|   // updates
 | ||||
|   ui->updateCombo->addItems( | ||||
|       {tr("Never"), tr("Daily"), tr("Weekly"), tr("Monthly")}); | ||||
|   QMap<AutoUpdate, QString> updateTypes; | ||||
|   updateTypes.insert(AutoUpdate::Disabled, tr("Disabled")); | ||||
|   updateTypes.insert(AutoUpdate::Daily, tr("Daily")); | ||||
|   updateTypes.insert(AutoUpdate::Weekly, tr("Weekly")); | ||||
|   updateTypes.insert(AutoUpdate::Monthly, tr("Monthly")); | ||||
|   ui->updateCombo->addItems(updateTypes.values()); | ||||
| 
 | ||||
|   auto updatesProxy = new QSortFilterProxyModel(this); | ||||
|   updatesProxy->setSourceModel(updater_.model()); | ||||
|   ui->updatesView->setModel(updatesProxy); | ||||
|   ui->updatesView->setItemDelegateForColumn(int(update::Model::Column::Action), | ||||
|                                             new update::ActionDelegate(this)); | ||||
| #ifndef DEVELOP | ||||
|   ui->updatesView->hideColumn(int(update::Model::Column::Files)); | ||||
| #endif | ||||
|   adjustUpdatesView(); | ||||
|   connect(updater_.model(), &QAbstractItemModel::modelReset,  //
 | ||||
|           this, &SettingsEditor::adjustUpdatesView); | ||||
|   connect(&updater_, &update::Loader::updated,  //
 | ||||
|           this, &SettingsEditor::adjustUpdatesView); | ||||
|   connect(ui->checkUpdates, &QPushButton::clicked,  //
 | ||||
|           &updater_, &update::Loader::checkForUpdates); | ||||
|   connect(ui->applyUpdates, &QPushButton::clicked,  //
 | ||||
|           &updater_, &update::Loader::applyUserActions); | ||||
| 
 | ||||
|   new WidgetState(this); | ||||
| } | ||||
| @ -140,7 +165,9 @@ void SettingsEditor::setSettings(const Settings &settings) | ||||
|   ui->ignoreSslCheck->setChecked(settings.ignoreSslErrors); | ||||
|   ui->translatorDebugCheck->setChecked(settings.debugMode); | ||||
|   ui->translateTimeoutSpin->setValue(settings.translationTimeout.count()); | ||||
|   updateTranslators(settings.translatorsDir, settings.translators); | ||||
|   translatorsPath_ = settings.translatorsDir; | ||||
|   enabledTranslators_ = settings.translators; | ||||
|   updateTranslators(); | ||||
|   if (auto lang = langs.findById(settings.targetLanguage)) | ||||
|     ui->translateLangCombo->setCurrentText(lang->name); | ||||
| 
 | ||||
| @ -193,12 +220,11 @@ void SettingsEditor::updateTesseractLanguages() | ||||
|   ui->tesseractLangCombo->addItems(names); | ||||
| } | ||||
| 
 | ||||
| void SettingsEditor::updateTranslators(const QString &path, | ||||
|                                        const QStringList &enabled) | ||||
| void SettingsEditor::updateTranslators() | ||||
| { | ||||
|   ui->translatorList->clear(); | ||||
| 
 | ||||
|   QDir dir(path); | ||||
|   QDir dir(translatorsPath_); | ||||
|   if (!dir.exists()) | ||||
|     return; | ||||
| 
 | ||||
| @ -208,8 +234,9 @@ void SettingsEditor::updateTranslators(const QString &path, | ||||
| 
 | ||||
|   for (auto i = 0, end = ui->translatorList->count(); i < end; ++i) { | ||||
|     auto item = ui->translatorList->item(i); | ||||
|     item->setCheckState(enabled.contains(item->text()) ? Qt::Checked | ||||
|                                                        : Qt::Unchecked); | ||||
|     item->setCheckState(enabledTranslators_.contains(item->text()) | ||||
|                             ? Qt::Checked | ||||
|                             : Qt::Unchecked); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -227,3 +254,10 @@ void SettingsEditor::updateTranslationLanguages() | ||||
|   std::sort(names.begin(), names.end()); | ||||
|   ui->translateLangCombo->addItems(names); | ||||
| } | ||||
| 
 | ||||
| void SettingsEditor::adjustUpdatesView() | ||||
| { | ||||
|   ui->updatesView->resizeColumnToContents(int(update::Model::Column::Name)); | ||||
|   updateTesseractLanguages(); | ||||
|   updateTranslators(); | ||||
| } | ||||
|  | ||||
| @ -14,7 +14,7 @@ class SettingsEditor : public QDialog | ||||
|   Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|   explicit SettingsEditor(); | ||||
|   explicit SettingsEditor(update::Loader &updater); | ||||
|   ~SettingsEditor(); | ||||
| 
 | ||||
|   Settings settings() const; | ||||
| @ -24,8 +24,12 @@ private: | ||||
|   void updateCurrentPage(); | ||||
|   void openTessdataDialog(); | ||||
|   void updateTesseractLanguages(); | ||||
|   void updateTranslators(const QString &path, const QStringList &enabled); | ||||
|   void updateTranslators(); | ||||
|   void updateTranslationLanguages(); | ||||
|   void adjustUpdatesView(); | ||||
| 
 | ||||
|   Ui::SettingsEditor *ui; | ||||
|   update::Loader &updater_; | ||||
|   QString translatorsPath_; | ||||
|   QStringList enabledTranslators_; | ||||
| }; | ||||
|  | ||||
| @ -14,16 +14,6 @@ | ||||
|    <string>Настройки</string> | ||||
|   </property> | ||||
|   <layout class="QGridLayout" name="gridLayout_6"> | ||||
|    <item row="0" column="0"> | ||||
|     <widget class="QListView" name="pagesList"> | ||||
|      <property name="sizePolicy"> | ||||
|       <sizepolicy hsizetype="Maximum" vsizetype="Expanding"> | ||||
|        <horstretch>0</horstretch> | ||||
|        <verstretch>0</verstretch> | ||||
|       </sizepolicy> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item row="1" column="0" colspan="4"> | ||||
|     <widget class="QDialogButtonBox" name="buttonBox"> | ||||
|      <property name="orientation"> | ||||
| @ -34,6 +24,16 @@ | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item row="0" column="0"> | ||||
|     <widget class="QListView" name="pagesList"> | ||||
|      <property name="sizePolicy"> | ||||
|       <sizepolicy hsizetype="Maximum" vsizetype="Expanding"> | ||||
|        <horstretch>0</horstretch> | ||||
|        <verstretch>0</verstretch> | ||||
|       </sizepolicy> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item row="0" column="1" colspan="3"> | ||||
|     <widget class="QStackedWidget" name="pagesView"> | ||||
|      <property name="sizePolicy"> | ||||
| @ -459,22 +459,48 @@ | ||||
|       </layout> | ||||
|      </widget> | ||||
|      <widget class="QWidget" name="pageUpdate"> | ||||
|       <layout class="QVBoxLayout" name="verticalLayout_2"> | ||||
|        <item> | ||||
|       <layout class="QGridLayout" name="gridLayout_5"> | ||||
|        <item row="2" column="0"> | ||||
|         <spacer name="horizontalSpacer_2"> | ||||
|          <property name="orientation"> | ||||
|           <enum>Qt::Horizontal</enum> | ||||
|          </property> | ||||
|          <property name="sizeHint" stdset="0"> | ||||
|           <size> | ||||
|            <width>204</width> | ||||
|            <height>20</height> | ||||
|           </size> | ||||
|          </property> | ||||
|         </spacer> | ||||
|        </item> | ||||
|        <item row="2" column="2"> | ||||
|         <spacer name="horizontalSpacer_3"> | ||||
|          <property name="orientation"> | ||||
|           <enum>Qt::Horizontal</enum> | ||||
|          </property> | ||||
|          <property name="sizeHint" stdset="0"> | ||||
|           <size> | ||||
|            <width>203</width> | ||||
|            <height>20</height> | ||||
|           </size> | ||||
|          </property> | ||||
|         </spacer> | ||||
|        </item> | ||||
|        <item row="0" column="0" colspan="3"> | ||||
|         <widget class="QWidget" name="widget_3" native="true"> | ||||
|          <layout class="QGridLayout" name="gridLayout_5"> | ||||
|           <item row="0" column="0"> | ||||
|          <layout class="QHBoxLayout" name="horizontalLayout"> | ||||
|           <item> | ||||
|            <widget class="QLabel" name="label_17"> | ||||
|             <property name="text"> | ||||
|              <string>Check for updates:</string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="0" column="1"> | ||||
|           <item> | ||||
|            <widget class="QComboBox" name="updateCombo"/> | ||||
|           </item> | ||||
|           <item row="0" column="2"> | ||||
|            <widget class="QPushButton" name="updateButton"> | ||||
|           <item> | ||||
|            <widget class="QPushButton" name="checkUpdates"> | ||||
|             <property name="sizePolicy"> | ||||
|              <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> | ||||
|               <horstretch>0</horstretch> | ||||
| @ -489,18 +515,19 @@ | ||||
|          </layout> | ||||
|         </widget> | ||||
|        </item> | ||||
|        <item> | ||||
|         <spacer name="verticalSpacer"> | ||||
|          <property name="orientation"> | ||||
|           <enum>Qt::Vertical</enum> | ||||
|        <item row="1" column="0" colspan="3"> | ||||
|         <widget class="QTreeView" name="updatesView"> | ||||
|          <property name="sortingEnabled"> | ||||
|           <bool>true</bool> | ||||
|          </property> | ||||
|          <property name="sizeHint" stdset="0"> | ||||
|           <size> | ||||
|            <width>20</width> | ||||
|            <height>389</height> | ||||
|           </size> | ||||
|         </widget> | ||||
|        </item> | ||||
|        <item row="2" column="1"> | ||||
|         <widget class="QPushButton" name="applyUpdates"> | ||||
|          <property name="text"> | ||||
|           <string>Apply updates</string> | ||||
|          </property> | ||||
|         </spacer> | ||||
|         </widget> | ||||
|        </item> | ||||
|       </layout> | ||||
|      </widget> | ||||
|  | ||||
| @ -16,6 +16,11 @@ class Translator; | ||||
| class Corrector; | ||||
| class Recognizer; | ||||
| 
 | ||||
| namespace update | ||||
| { | ||||
| class Loader; | ||||
| } | ||||
| 
 | ||||
| using TaskPtr = std::shared_ptr<Task>; | ||||
| using LanguageId = QString; | ||||
| using LanguageIds = QStringList; | ||||
|  | ||||
							
								
								
									
										405
									
								
								updates.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										405
									
								
								updates.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,405 @@ | ||||
| { | ||||
| "version":1 | ||||
| 
 | ||||
| ,"app":{ | ||||
|  "win32":{"version":"3.0.0", "host":"win32", "files":[]} | ||||
|  ,"win64":{"version":"3.0.0", "host":"win64", "files":[]} | ||||
|  ,"linux":{"version":"3.0.0", "host":"linux", "files":[]} | ||||
| } | ||||
| 
 | ||||
| ,"recognizers": { | ||||
|   "mal":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/mal.traineddata", "path":"$tessdata$/mal.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "guj":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/guj.traineddata", "path":"$tessdata$/guj.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "isl":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/isl.traineddata", "path":"$tessdata$/isl.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "san":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/san.traineddata", "path":"$tessdata$/san.traineddata", "date":"2017-09-15T18:37:50+05:30"} | ||||
|   ]} | ||||
|   , "afr":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/afr.traineddata", "path":"$tessdata$/afr.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "pol":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/pol.traineddata", "path":"$tessdata$/pol.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "fil":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/fil.traineddata", "path":"$tessdata$/fil.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "por":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/por.traineddata", "path":"$tessdata$/por.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "bos":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/bos.traineddata", "path":"$tessdata$/bos.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "mlt":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/mlt.traineddata", "path":"$tessdata$/mlt.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "sin":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/sin.traineddata", "path":"$tessdata$/sin.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "rus":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/rus.traineddata", "path":"$tessdata$/rus.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ori":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ori.traineddata", "path":"$tessdata$/ori.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "chi_tra_vert":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/chi_tra_vert.traineddata", "path":"$tessdata$/chi_tra_vert.traineddata", "date":"2019-05-21T17:48:52+02:00"} | ||||
|   ]} | ||||
|   , "kmr":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/kmr.traineddata", "path":"$tessdata$/kmr.traineddata", "date":"2018-04-25T22:47:45+05:30"} | ||||
|   ]} | ||||
|   , "osd":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/osd.traineddata", "path":"$tessdata$/osd.traineddata", "date":"2017-09-15T11:44:08-07:00"} | ||||
|   ]} | ||||
|   , "srp":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/srp.traineddata", "path":"$tessdata$/srp.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "dan":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/dan.traineddata", "path":"$tessdata$/dan.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "tam":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/tam.traineddata", "path":"$tessdata$/tam.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "syr":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/syr.traineddata", "path":"$tessdata$/syr.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "gle":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/gle.traineddata", "path":"$tessdata$/gle.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "vie":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/vie.traineddata", "path":"$tessdata$/vie.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "oci":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/oci.traineddata", "path":"$tessdata$/oci.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "tur":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/tur.traineddata", "path":"$tessdata$/tur.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "bel":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/bel.traineddata", "path":"$tessdata$/bel.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "bre":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/bre.traineddata", "path":"$tessdata$/bre.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "tir":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/tir.traineddata", "path":"$tessdata$/tir.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "bul":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/bul.traineddata", "path":"$tessdata$/bul.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "chi_sim":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/chi_sim.traineddata", "path":"$tessdata$/chi_sim.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "que":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/que.traineddata", "path":"$tessdata$/que.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "deu":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/deu.traineddata", "path":"$tessdata$/deu.traineddata", "date":"2018-02-01T15:29:03+01:00"} | ||||
|   ]} | ||||
|   , "swa":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/swa.traineddata", "path":"$tessdata$/swa.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "sqi":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/sqi.traineddata", "path":"$tessdata$/sqi.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "yid":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/yid.traineddata", "path":"$tessdata$/yid.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "aze_cyrl":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/aze_cyrl.traineddata", "path":"$tessdata$/aze_cyrl.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ben":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ben.traineddata", "path":"$tessdata$/ben.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "uig":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/uig.traineddata", "path":"$tessdata$/uig.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "tha":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/tha.traineddata", "path":"$tessdata$/tha.traineddata", "date":"2019-05-21T17:50:06+02:00"} | ||||
|   ]} | ||||
|   , "spa_old":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/spa_old.traineddata", "path":"$tessdata$/spa_old.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "div":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/div.traineddata", "path":"$tessdata$/div.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "fin":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/fin.traineddata", "path":"$tessdata$/fin.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "eng":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/eng.traineddata", "path":"$tessdata$/eng.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "slk":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/slk.traineddata", "path":"$tessdata$/slk.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ltz":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ltz.traineddata", "path":"$tessdata$/ltz.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "gla":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/gla.traineddata", "path":"$tessdata$/gla.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "nld":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/nld.traineddata", "path":"$tessdata$/nld.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "eus":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/eus.traineddata", "path":"$tessdata$/eus.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "cat":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/cat.traineddata", "path":"$tessdata$/cat.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "amh":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/amh.traineddata", "path":"$tessdata$/amh.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "lit":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/lit.traineddata", "path":"$tessdata$/lit.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "fao":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/fao.traineddata", "path":"$tessdata$/fao.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ind":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ind.traineddata", "path":"$tessdata$/ind.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "pus":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/pus.traineddata", "path":"$tessdata$/pus.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "aze":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/aze.traineddata", "path":"$tessdata$/aze.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "kat":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/kat.traineddata", "path":"$tessdata$/kat.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "tat":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/tat.traineddata", "path":"$tessdata$/tat.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "chi_sim_vert":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/chi_sim_vert.traineddata", "path":"$tessdata$/chi_sim_vert.traineddata", "date":"2019-05-21T17:48:52+02:00"} | ||||
|   ]} | ||||
|   , "ton":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ton.traineddata", "path":"$tessdata$/ton.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "mya":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/mya.traineddata", "path":"$tessdata$/mya.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "kor":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/kor.traineddata", "path":"$tessdata$/kor.traineddata", "date":"2018-04-09T19:58:26+05:30"} | ||||
|   ]} | ||||
|   , "nor":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/nor.traineddata", "path":"$tessdata$/nor.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "grc":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/grc.traineddata", "path":"$tessdata$/grc.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "jpn":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/jpn.traineddata", "path":"$tessdata$/jpn.traineddata", "date":"2019-05-21T17:49:35+02:00"} | ||||
|   ]} | ||||
|   , "fas":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/fas.traineddata", "path":"$tessdata$/fas.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "kan":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/kan.traineddata", "path":"$tessdata$/kan.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "mkd":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/mkd.traineddata", "path":"$tessdata$/mkd.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "tgk":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/tgk.traineddata", "path":"$tessdata$/tgk.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "hye":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/hye.traineddata", "path":"$tessdata$/hye.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "fra":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/fra.traineddata", "path":"$tessdata$/fra.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "urd":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/urd.traineddata", "path":"$tessdata$/urd.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "hin":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/hin.traineddata", "path":"$tessdata$/hin.traineddata", "date":"2017-09-15T18:37:50+05:30"} | ||||
|   ]} | ||||
|   , "heb":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/heb.traineddata", "path":"$tessdata$/heb.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "kat_old":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/kat_old.traineddata", "path":"$tessdata$/kat_old.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "yor":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/yor.traineddata", "path":"$tessdata$/yor.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "msa":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/msa.traineddata", "path":"$tessdata$/msa.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "hat":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/hat.traineddata", "path":"$tessdata$/hat.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ita_old":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ita_old.traineddata", "path":"$tessdata$/ita_old.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "swe":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/swe.traineddata", "path":"$tessdata$/swe.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "srp_latn":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/srp_latn.traineddata", "path":"$tessdata$/srp_latn.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "jav":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/jav.traineddata", "path":"$tessdata$/jav.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "bod":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/bod.traineddata", "path":"$tessdata$/bod.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "mar":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/mar.traineddata", "path":"$tessdata$/mar.traineddata", "date":"2017-09-15T21:22:28+05:30"} | ||||
|   ]} | ||||
|   , "chi_tra":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/chi_tra.traineddata", "path":"$tessdata$/chi_tra.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ita":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ita.traineddata", "path":"$tessdata$/ita.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "mon":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/mon.traineddata", "path":"$tessdata$/mon.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "fry":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/fry.traineddata", "path":"$tessdata$/fry.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ces":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ces.traineddata", "path":"$tessdata$/ces.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "asm":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/asm.traineddata", "path":"$tessdata$/asm.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "tel":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/tel.traineddata", "path":"$tessdata$/tel.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "kir":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/kir.traineddata", "path":"$tessdata$/kir.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "cym":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/cym.traineddata", "path":"$tessdata$/cym.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ceb":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ceb.traineddata", "path":"$tessdata$/ceb.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "sun":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/sun.traineddata", "path":"$tessdata$/sun.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "cos":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/cos.traineddata", "path":"$tessdata$/cos.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "hrv":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/hrv.traineddata", "path":"$tessdata$/hrv.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "iku":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/iku.traineddata", "path":"$tessdata$/iku.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "hun":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/hun.traineddata", "path":"$tessdata$/hun.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "snd":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/snd.traineddata", "path":"$tessdata$/snd.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "pan":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/pan.traineddata", "path":"$tessdata$/pan.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "dzo":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/dzo.traineddata", "path":"$tessdata$/dzo.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "nep":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/nep.traineddata", "path":"$tessdata$/nep.traineddata", "date":"2017-09-15T21:22:28+05:30"} | ||||
|   ]} | ||||
|   , "uzb_cyrl":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/uzb_cyrl.traineddata", "path":"$tessdata$/uzb_cyrl.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "kor_vert":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/kor_vert.traineddata", "path":"$tessdata$/kor_vert.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "slv":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/slv.traineddata", "path":"$tessdata$/slv.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "est":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/est.traineddata", "path":"$tessdata$/est.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "lat":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/lat.traineddata", "path":"$tessdata$/lat.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "epo":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/epo.traineddata", "path":"$tessdata$/epo.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "spa":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/spa.traineddata", "path":"$tessdata$/spa.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "jpn_vert":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/jpn_vert.traineddata", "path":"$tessdata$/jpn_vert.traineddata", "date":"2019-05-21T17:49:35+02:00"} | ||||
|   ]} | ||||
|   , "mri":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/mri.traineddata", "path":"$tessdata$/mri.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ron":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ron.traineddata", "path":"$tessdata$/ron.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "khm":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/khm.traineddata", "path":"$tessdata$/khm.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "frm":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/frm.traineddata", "path":"$tessdata$/frm.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ukr":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ukr.traineddata", "path":"$tessdata$/ukr.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "frk":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/frk.traineddata", "path":"$tessdata$/frk.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "glg":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/glg.traineddata", "path":"$tessdata$/glg.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "lav":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/lav.traineddata", "path":"$tessdata$/lav.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ara":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ara.traineddata", "path":"$tessdata$/ara.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "kaz":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/kaz.traineddata", "path":"$tessdata$/kaz.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "enm":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/enm.traineddata", "path":"$tessdata$/enm.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "uzb":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/uzb.traineddata", "path":"$tessdata$/uzb.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "ell":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/ell.traineddata", "path":"$tessdata$/ell.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "lao":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/lao.traineddata", "path":"$tessdata$/lao.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|   , "chr":{"files":[ | ||||
|    {"url":"https://github.com/tesseract-ocr/tessdata_best/raw/master/chr.traineddata", "path":"$tessdata$/chr.traineddata", "date":"2017-09-14T14:45:10-07:00"} | ||||
|   ]} | ||||
|  } | ||||
| 
 | ||||
| ,"translators":{ | ||||
|  "bing": {"files":[ | ||||
|   {"url":"https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/bing.js", "path":"$translators$/bing.js", "md5":"23178a0a19f54c8dc6befd18c67decf7"} | ||||
|  ]} | ||||
|  ,"google": {"files":[ | ||||
|   {"url":"https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/google.js", "path":"$translators$/google.js", "md5":"232ffcfff1ab743d295e6c8a04695272"} | ||||
|  ]} | ||||
|  ,"google_api": {"files":[ | ||||
|   {"url":"https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/google_api.js", "path":"$translators$/google_api.js", "md5":"7fa9fb03d4cfc3e69cd0120c51d82c95"} | ||||
|  ]} | ||||
|  ,"yandex": {"files":[ | ||||
|   {"url":"https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/yandex.js", "path":"$translators$/yandex.js", "md5":"2c4d64c30fa74b875f48dc99f2745c1f"} | ||||
|  ]} | ||||
|  ,"deepl": {"files":[ | ||||
|   {"url":"https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/deepl.js", "path":"$translators$/deepl.js", "md5":"05ac33e02469710bc69fc20c55773c91"} | ||||
|  ]} | ||||
|  ,"papago": {"files":[ | ||||
|   {"url":"https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/papago.js", "path":"$translators$/papago.js", "md5":"c3c6bb456a48a47f50e634ee43862434"} | ||||
|  ]} | ||||
| } | ||||
| } | ||||
							
								
								
									
										36
									
								
								version.json
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								version.json
									
									
									
									
									
								
							| @ -2,44 +2,14 @@ | ||||
|     "version": 1, | ||||
|     "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/version.json", | ||||
|     "Application": { | ||||
|         "version": 2, | ||||
|         "version": 3, | ||||
|         "compatibleVersion": 2, | ||||
|         "built_in": true, | ||||
|         "versionString": "2.0.2", | ||||
|         "versionString": "3.0.0", | ||||
|         "permissions": "0x7755", | ||||
|         "url_win": "https://github.com/OneMoreGres/ScreenTranslator/releases/download/2.0.2/ScreenTranslator.exe", | ||||
|         "url_win": "disabled", | ||||
|         "path_win": "ScreenTranslator.exe", | ||||
|         "url_linux": "disabled", | ||||
|         "path_linux": "ScreenTranslator" | ||||
|     }, | ||||
|     "Bing translator": { | ||||
|         "version": 6, | ||||
|         "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/bing.js", | ||||
|         "path": "translators/bing.js" | ||||
|     }, | ||||
|     "Google translator": { | ||||
|         "version": 4, | ||||
|         "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/google.js", | ||||
|         "path": "translators/google.js" | ||||
|     }, | ||||
|     "Google API translator": { | ||||
|         "version": 1, | ||||
|         "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/google_api.js", | ||||
|         "path": "translators/google_api.js" | ||||
|     }, | ||||
|     "Yandex translator": { | ||||
|         "version": 4, | ||||
|         "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/yandex.js", | ||||
|         "path": "translators/yandex.js" | ||||
|     }, | ||||
|     "DeepL translator": { | ||||
|         "version": 1, | ||||
|         "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/deepl.js", | ||||
|         "path": "translators/deepl.js" | ||||
|     }, | ||||
|     "Papago translator": { | ||||
|         "version": 1, | ||||
|         "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/papago.js", | ||||
|         "path": "translators/papago.js" | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Gres
						Gres