From b7609d7d3e0b099094dde76b454661e1f9e40d4c Mon Sep 17 00:00:00 2001 From: Martchus Date: Thu, 25 Aug 2016 00:45:32 +0200 Subject: [PATCH] Initial import --- .gitignore | 44 ++ CMakeLists.txt | 102 +++ LICENSE | 339 ++++++++++ README.md | 29 + application/main.cpp | 68 ++ application/settings.cpp | 161 +++++ application/settings.h | 51 ++ data/syncthingconnection.cpp | 614 ++++++++++++++++++ data/syncthingconnection.h | 332 ++++++++++ data/syncthingdevicemodel.cpp | 195 ++++++ data/syncthingdevicemodel.h | 43 ++ data/syncthingdirectorymodel.cpp | 215 ++++++ data/syncthingdirectorymodel.h | 48 ++ gui/connectionoptionpage.ui | 192 ++++++ gui/devview.cpp | 48 ++ gui/devview.h | 22 + gui/dirbuttonsitemdelegate.cpp | 60 ++ gui/dirbuttonsitemdelegate.h | 25 + gui/dirview.cpp | 67 ++ gui/dirview.h | 29 + gui/launcheroptionpage.ui | 20 + gui/notificationsoptionpage.ui | 54 ++ gui/settingsdialog.cpp | 214 ++++++ gui/settingsdialog.h | 51 ++ gui/tray.cpp | 455 +++++++++++++ gui/tray.h | 125 ++++ gui/traywidget.ui | 248 +++++++ gui/webpage.cpp | 88 +++ gui/webpage.h | 44 ++ gui/webviewdialog.cpp | 63 ++ gui/webviewdialog.h | 33 + gui/webviewoptionpage.ui | 72 ++ gui/webviewprovider.h | 11 + resources/icons.qrc | 12 + .../hicolor/scalable/app/syncthingtray.svg | 32 + .../scalable/status/syncthing-default.svg | 20 + .../status/syncthing-disconnected.svg | 20 + .../scalable/status/syncthing-error.svg | 31 + .../scalable/status/syncthing-notify.svg | 31 + .../hicolor/scalable/status/syncthing-ok.svg | 20 + .../scalable/status/syncthing-pause.svg | 31 + .../scalable/status/syncthing-sync.svg | 31 + resources/screenshots/1.png | Bin 0 -> 118425 bytes translations/syncthingtray_de_DE.ts | 614 ++++++++++++++++++ translations/syncthingtray_en_US.ts | 614 ++++++++++++++++++ 45 files changed, 5618 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 application/main.cpp create mode 100644 application/settings.cpp create mode 100644 application/settings.h create mode 100644 data/syncthingconnection.cpp create mode 100644 data/syncthingconnection.h create mode 100644 data/syncthingdevicemodel.cpp create mode 100644 data/syncthingdevicemodel.h create mode 100644 data/syncthingdirectorymodel.cpp create mode 100644 data/syncthingdirectorymodel.h create mode 100644 gui/connectionoptionpage.ui create mode 100644 gui/devview.cpp create mode 100644 gui/devview.h create mode 100644 gui/dirbuttonsitemdelegate.cpp create mode 100644 gui/dirbuttonsitemdelegate.h create mode 100644 gui/dirview.cpp create mode 100644 gui/dirview.h create mode 100644 gui/launcheroptionpage.ui create mode 100644 gui/notificationsoptionpage.ui create mode 100644 gui/settingsdialog.cpp create mode 100644 gui/settingsdialog.h create mode 100644 gui/tray.cpp create mode 100644 gui/tray.h create mode 100644 gui/traywidget.ui create mode 100644 gui/webpage.cpp create mode 100644 gui/webpage.h create mode 100644 gui/webviewdialog.cpp create mode 100644 gui/webviewdialog.h create mode 100644 gui/webviewoptionpage.ui create mode 100644 gui/webviewprovider.h create mode 100644 resources/icons.qrc create mode 100644 resources/icons/hicolor/scalable/app/syncthingtray.svg create mode 100644 resources/icons/hicolor/scalable/status/syncthing-default.svg create mode 100644 resources/icons/hicolor/scalable/status/syncthing-disconnected.svg create mode 100644 resources/icons/hicolor/scalable/status/syncthing-error.svg create mode 100644 resources/icons/hicolor/scalable/status/syncthing-notify.svg create mode 100644 resources/icons/hicolor/scalable/status/syncthing-ok.svg create mode 100644 resources/icons/hicolor/scalable/status/syncthing-pause.svg create mode 100644 resources/icons/hicolor/scalable/status/syncthing-sync.svg create mode 100644 resources/screenshots/1.png create mode 100644 translations/syncthingtray_de_DE.ts create mode 100644 translations/syncthingtray_en_US.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a33e66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# C++ objects and libs + +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.dll +*.dylib + +# Qt-es + +/.qmake.cache +/.qmake.stash +*.pro.user +*.txt.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +qrc_*.cpp +ui_*.h +Makefile* +*-build-* + +# QtCreator + +*.autosave + +#QtCtreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# Dolphin +.directory + +# documentation +/doc + +# tests +testfiles/output.* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..23609be --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR) + +# metadata +set(META_PROJECT_NAME syncthingtray) +set(META_PROJECT_TYPE application) +set(META_APP_NAME "Syncthing Tray") +set(META_APP_AUTHOR "Martchus") +set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}") +set(META_APP_DESCRIPTION "Tray application for Syncthing") +set(META_APP_CATEGORIES "Utility;Network;") +set(META_GUI_OPTIONAL false) +set(META_VERSION_MAJOR 0) +set(META_VERSION_MINOR 0) +set(META_VERSION_PATCH 1) +set(META_APP_VERSION ${META_VERSION_MAJOR}.${META_VERSION_MINOR}.${META_VERSION_PATCH}) + +# add project files +set(HEADER_FILES + data/syncthingconnection.h + data/syncthingdirectorymodel.h + data/syncthingdevicemodel.h +) +set(SRC_FILES + data/syncthingconnection.cpp + data/syncthingdirectorymodel.cpp + data/syncthingdevicemodel.cpp +) + +set(WIDGETS_HEADER_FILES + application/settings.h + gui/tray.h + gui/settingsdialog.h + gui/webpage.h + gui/webviewdialog.h + gui/webviewprovider.h + gui/dirbuttonsitemdelegate.h + gui/dirview.h + gui/devview.h +) +set(WIDGETS_SRC_FILES + application/main.cpp + application/settings.cpp + gui/tray.cpp + gui/settingsdialog.cpp + gui/webpage.cpp + gui/webviewdialog.cpp + gui/dirbuttonsitemdelegate.cpp + gui/dirview.cpp + gui/devview.cpp + resources/icons.qrc +) +set(WIDGETS_UI_FILES + gui/traywidget.ui + gui/connectionoptionpage.ui + gui/notificationsoptionpage.ui + gui/launcheroptionpage.ui + gui/webviewoptionpage.ui +) + +#set(QUICK_HEADER_FILES +#) +#set(QUICK_SRC_FILES +#) + +set(TS_FILES + translations/${META_PROJECT_NAME}_de_DE.ts + translations/${META_PROJECT_NAME}_en_US.ts +) + +set(ICON_FILES + resources/icons/hicolor/scalable/apps/${META_PROJECT_NAME}.svg +) + +set(DOC_FILES + README.md +) + +# find c++utilities +find_package(c++utilities 4.0.0 REQUIRED) +use_cpp_utilities() +include(BasicConfig) + +# find qtutilities +if(WIDGETS_GUI OR QUICK_GUI) + find_package(qtutilities 5.0.0 REQUIRED) + use_qt_utilities() +endif() + +list(APPEND ADDITIONAL_QT_MODULES Network Svg) + +# include modules to apply configuration +include(BasicConfig) +include(QtGuiConfig) +include(QtConfig) +include(WindowsResources) +include(WebViewProviderConfig) +include(AppTarget) +include(ShellCompletion) +include(ConfigHeader) + +# create desktop file using previously defined meta data +add_desktop_file() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..23cb790 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f9974e --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Syncthing Tray + +Qt 5-based tray application for [Syncthing](https://github.com/syncthing/syncthing) + +* Still under development +* Designed to work under any desktop environment with tray icon support +* Doesn't require desktop environment specific libraries +* Provides quick access to most frequently used features but does not intend to replace the official web UI +* Shows Syncthing notifications +* Provides quick access to the official web UI + * Utilizes either Qt WebKit or Qt WebEngine + * Can be built without web view support as well (then the web UI is opened in the regular browser) + +## Screenshots +### Openbox/Tint2 +![Openbox/Tint2](/resources/screenshots/1.png?raw=true "Under Openbox with Tint2") + +## Download / binary repository +I will provide packages for Arch Linux and Windows when releasing. For more information checkout my +[website](http://martchus.no-ip.biz/website/page.php?name=programming). + +## Build instructions +The application depends on [c++utilities](https://github.com/Martchus/cpp-utilities) and [qtutilities](https://github.com/Martchus/qtutilities) and is built the same way as these libaries. For basic instructions checkout the README file of [c++utilities](https://github.com/Martchus/cpp-utilities). + +The following Qt 5 modules are requried: core network gui network widgets webenginewidgets/webkitwidgets + +#### Select Qt modules for WebView +* If Qt WebKitWidgets is installed on the system, the tray will link against it. Otherwise it will link against Qt WebEngineWidgets. +* To force usage of Qt WebKit/Qt WebEngine or to disable both add `-DWEBVIEW_PROVIDER=webkit/webengine/none` to the CMake arguments. diff --git a/application/main.cpp b/application/main.cpp new file mode 100644 index 0000000..0764257 --- /dev/null +++ b/application/main.cpp @@ -0,0 +1,68 @@ +#include "./settings.h" +#include "../gui/tray.h" + +#include "resources/config.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include + +using namespace std; +using namespace ApplicationUtilities; +using namespace QtGui; + +int main(int argc, char *argv[]) +{ + SET_APPLICATION_INFO; + // setup argument parser + ArgumentParser parser; + HelpArgument helpArg(parser); + // Qt configuration arguments + QT_CONFIG_ARGUMENTS qtConfigArgs; + parser.setMainArguments({&qtConfigArgs.qtWidgetsGuiArg(), &helpArg}); + try { + parser.parseArgs(argc, argv); + if(qtConfigArgs.qtWidgetsGuiArg().isPresent()) { + SET_QT_APPLICATION_INFO; + QApplication application(argc, argv); + Settings::restore(); + Settings::qtSettings().apply(); + qtConfigArgs.applySettings(true); + LOAD_QT_TRANSLATIONS; + QtUtilitiesResources::init(); + int res; +#ifndef QT_NO_SYSTEMTRAYICON + if(QSystemTrayIcon::isSystemTrayAvailable()) { + application.setQuitOnLastWindowClosed(false); + TrayIcon trayIcon; + trayIcon.show(); + res = application.exec(); + } else { + QMessageBox::critical(nullptr, QApplication::applicationName(), QApplication::translate("main", "The system tray is (currently) not available.")); + res = -1; + } +#else + QMessageBox::critical(nullptr, QApplication::applicationName(), QApplication::translate("main", "The Qt libraries have not been built with tray icon support.")); + res = -2; +#endif + Settings::save(); + QtUtilitiesResources::cleanup(); + return res; + } + } catch(const Failure &ex) { + CMD_UTILS_START_CONSOLE; + cout << "Unable to parse arguments. " << ex.what() << "\nSee --help for available commands." << endl; + } + + return 0; +} + diff --git a/application/settings.cpp b/application/settings.cpp new file mode 100644 index 0000000..7a61b4b --- /dev/null +++ b/application/settings.cpp @@ -0,0 +1,161 @@ +#include "./settings.h" +#include + +#include +#include +#include +#include + +using namespace Media; + +namespace Settings { + +// tray +QString &syncthingUrl() +{ + static QString v; + return v; +} +bool &authEnabled() +{ + static bool v = false; + return v; +} +QString &userName() +{ + static QString v; + return v; +} +QString &password() +{ + static QString v; + return v; +} +QByteArray &apiKey() +{ + static QByteArray v; + return v; +} +bool ¬ifyOnDisconnect() +{ + static bool v = true; + return v; +} +bool ¬ifyOnErrors() +{ + static bool v = true; + return v; +} +bool ¬ifyOnSyncComplete() +{ + static bool v = true; + return v; +} +bool &showSyncthingNotifications() +{ + static bool v = true; + return v; +} +bool &launchSynchting() +{ + static bool v = false; + return v; +} +QString &syncthingCommand() +{ + static QString v; + return v; +} + +// web view +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) +bool &webViewDisabled() +{ + static bool v = false; + return v; +} +double &webViewZoomFactor() +{ + static double v = 1.0; + return v; +} +QByteArray &webViewGeometry() +{ + static QByteArray v; + return v; +} +bool &webViewKeepRunning() +{ + static bool v = true; + return v; +} +#endif + +// Qt settings +Dialogs::QtSettings &qtSettings() +{ + static Dialogs::QtSettings v; + return v; +} + +void restore() +{ + QSettings settings(QSettings::IniFormat, QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); + + settings.beginGroup(QStringLiteral("tray")); + syncthingUrl() = settings.value(QStringLiteral("syncthingUrl"), QStringLiteral("http://localhost:8080/")).toString(); + authEnabled() = settings.value(QStringLiteral("authEnabled"), false).toBool(); + userName() = settings.value(QStringLiteral("userName")).toString(); + password() = settings.value(QStringLiteral("password")).toString(); + apiKey() = settings.value(QStringLiteral("apiKey")).toByteArray(); + notifyOnDisconnect() = settings.value(QStringLiteral("notifyOnDisconnect"), true).toBool(); + notifyOnErrors() = settings.value(QStringLiteral("notifyOnErrors"), true).toBool(); + notifyOnSyncComplete() = settings.value(QStringLiteral("notifyOnSyncComplete"), true).toBool(); + notifyOnSyncComplete() = settings.value(QStringLiteral("showSyncthingNotifications"), true).toBool(); + launchSynchting() = settings.value(QStringLiteral("launchSynchting"), false).toBool(); + syncthingCommand() = settings.value(QStringLiteral("syncthingCommand"), QStringLiteral("syncthing")).toString(); + settings.endGroup(); + +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) + settings.beginGroup(QStringLiteral("webview")); + webViewDisabled() = settings.value(QStringLiteral("isabled"), false).toBool(); + webViewZoomFactor() = settings.value(QStringLiteral("zoomFactor"), 1.0).toDouble(); + webViewGeometry() = settings.value(QStringLiteral("geometry")).toByteArray(); + webViewKeepRunning() = settings.value(QStringLiteral("keepRunning"), true).toBool(); + settings.endGroup(); +#endif + + qtSettings().restore(settings); +} + +void save() +{ + QSettings settings(QSettings::IniFormat, QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); + + settings.beginGroup(QStringLiteral("tray")); + settings.setValue(QStringLiteral("syncthingUrl"), syncthingUrl()); + settings.setValue(QStringLiteral("authEnabled"), authEnabled()); + settings.setValue(QStringLiteral("userName"), userName()); + settings.setValue(QStringLiteral("password"), password()); + settings.setValue(QStringLiteral("apiKey"), apiKey()); + settings.setValue(QStringLiteral("notifyOnDisconnect"), notifyOnDisconnect()); + settings.setValue(QStringLiteral("notifyOnErrors"), notifyOnErrors()); + settings.setValue(QStringLiteral("notifyOnSyncComplete"), notifyOnSyncComplete()); + settings.setValue(QStringLiteral("showSyncthingNotifications"), showSyncthingNotifications()); + settings.setValue(QStringLiteral("launchSynchting"), launchSynchting()); + settings.setValue(QStringLiteral("syncthingCommand"), syncthingCommand()); + settings.endGroup(); + +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) + settings.beginGroup(QStringLiteral("webview")); + settings.setValue(QStringLiteral("disabled"), webViewDisabled()); + settings.setValue(QStringLiteral("zoomFactor"), webViewZoomFactor()); + settings.setValue(QStringLiteral("geometry"), webViewGeometry()); + settings.setValue(QStringLiteral("keepRunning"), webViewKeepRunning()); + settings.endGroup(); +#endif + + qtSettings().save(settings); +} + +} diff --git a/application/settings.h b/application/settings.h new file mode 100644 index 0000000..68c44cb --- /dev/null +++ b/application/settings.h @@ -0,0 +1,51 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include + +#include + +QT_FORWARD_DECLARE_CLASS(QByteArray) +QT_FORWARD_DECLARE_CLASS(QString) + +namespace Media { +enum class TagUsage; +enum class ElementPosition; +} + +namespace Dialogs { +class QtSettings; +} + +namespace Settings { + +QString &syncthingUrl(); +bool &authEnabled(); +QString &userName(); +QString &password(); +QByteArray &apiKey(); + +bool ¬ifyOnDisconnect(); +bool ¬ifyOnErrors(); +bool ¬ifyOnSyncComplete(); +bool &showSyncthingNotifications(); + +bool &launchSynchting(); +QString &syncthingCommand(); + +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) +bool &webViewDisabled(); +double &webViewZoomFactor(); +QByteArray &webViewGeometry(); +bool &webViewKeepRunning(); +#endif + +// Qt settings +Dialogs::QtSettings &qtSettings(); + +void restore(); +void save(); + +} + +#endif // SETTINGS_H diff --git a/data/syncthingconnection.cpp b/data/syncthingconnection.cpp new file mode 100644 index 0000000..035d76d --- /dev/null +++ b/data/syncthingconnection.cpp @@ -0,0 +1,614 @@ +#include "./syncthingconnection.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; + +namespace Data { + +/*! + * \brief Returns the QNetworkAccessManager instance used by SyncthingConnection instances. + */ +QNetworkAccessManager &networkAccessManager() +{ + static QNetworkAccessManager networkAccessManager; + return networkAccessManager; +} + +/*! + * \class SyncthingConnection + * \brief The SyncthingConnection class allows Qt applications to access Syncthing. + */ + +/*! + * \brief Constructs a new instance ready to connect. To establish the connection, call connect(). + */ +SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByteArray &apiKey, QObject *parent) : + QObject(parent), + m_syncthingUrl(syncthingUrl), + m_apiKey(apiKey), + m_status(SyncthingStatus::Disconnected), + m_keepPolling(false), + m_reconnecting(false), + m_configReply(nullptr), + m_statusReply(nullptr), + m_eventsReply(nullptr), + m_unreadNotifications(false) +{} + +/*! + * \brief Destroys the instance. Ongoing requests are aborted. + */ +SyncthingConnection::~SyncthingConnection() +{ + disconnect(); +} + +/*! + * \brief Returns the string representation of the current status(). + */ +QString SyncthingConnection::statusText() const +{ + switch(m_status) { + case SyncthingStatus::Disconnected: + return tr("disconnected"); + case SyncthingStatus::Default: + return tr("connected"); + case SyncthingStatus::NotificationsAvailable: + return tr("connected, notifications available"); + case SyncthingStatus::Paused: + return tr("connected, paused"); + case SyncthingStatus::Synchronizing: + return tr("connected, synchronizing"); + default: + return tr("unknown"); + } +} + +/*! + * \brief Connects asynchronously to Syncthing. Does nothing if already connected. + */ +void SyncthingConnection::connect() +{ + if(!isConnected()) { + m_reconnecting = false; + requestConfig(); + requestStatus(); + m_lastEventId = 0; + requestEvents(); + m_keepPolling = true; + } +} + +/*! + * \brief Disconnects. Does nothing if not connected. + */ +void SyncthingConnection::disconnect() +{ + m_reconnecting = false; + if(m_configReply) { + m_configReply->abort(); + } + if(m_statusReply) { + m_statusReply->abort(); + } + if(m_eventsReply) { + m_eventsReply->abort(); + } +} + +/*! + * \brief Disconnects if connected, then (re-)connects asynchronously. + */ +void SyncthingConnection::reconnect() +{ + if(isConnected()) { + m_reconnecting = true; + if(m_configReply) { + m_configReply->abort(); + } + if(m_statusReply) { + m_statusReply->abort(); + } + if(m_eventsReply) { + m_eventsReply->abort(); + } + } else { + connect(); + } +} + +void SyncthingConnection::pause(const QString &dev) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("device"), dev); + QObject::connect(postData(QStringLiteral("system/pause"), query), &QNetworkReply::finished, this, &SyncthingConnection::readPauseResume); +} + +void SyncthingConnection::pauseAllDevs() +{ + for(const SyncthingDev &dev : m_devs) { + pause(dev.id); + } +} + +void SyncthingConnection::resume(const QString &dev) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("device"), dev); + QObject::connect(postData(QStringLiteral("system/resume"), query), &QNetworkReply::finished, this, &SyncthingConnection::readPauseResume); +} + +void SyncthingConnection::resumeAllDevs() +{ + for(const SyncthingDev &dev : m_devs) { + resume(dev.id); + } +} + +/*! + * \brief Requests rescanning the directory with the specified ID. + * + * The signal error() is emitted when the request was not successful. + */ +void SyncthingConnection::rescan(const QString &dir) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("folder"), dir); + QObject::connect(postData(QStringLiteral("db/scan"), query), &QNetworkReply::finished, this, &SyncthingConnection::readRescan); +} + +void SyncthingConnection::rescanAllDirs() +{ + for(const SyncthingDir &dir : m_dirs) { + rescan(dir.id); + } +} + +void SyncthingConnection::notificationsRead() +{ + m_unreadNotifications = false; + setStatus(status()); +} + +/*! + * \brief Prepares a request for the specified \a path and \a query. + */ +QNetworkRequest SyncthingConnection::prepareRequest(const QString &path, const QUrlQuery &query, bool rest) +{ + QUrl url(m_syncthingUrl); + url.setPath(rest ? (url.path() % QStringLiteral("/rest/") % path) : (url.path() + path)); + url.setUserName(user()); + url.setPassword(password()); + url.setQuery(query); + QNetworkRequest request(url); + request.setRawHeader("X-API-Key", m_apiKey); + return request; +} + +/*! + * \brief Requests asynchronously data using the rest API. + */ +inline QNetworkReply *SyncthingConnection::requestData(const QString &path, const QUrlQuery &query, bool rest) +{ + return networkAccessManager().get(prepareRequest(path, query, rest)); +} + +/*! + * \brief Posts asynchronously data using the rest API. + */ +inline QNetworkReply *SyncthingConnection::postData(const QString &path, const QUrlQuery &query, const QByteArray &data) +{ + return networkAccessManager().post(prepareRequest(path, query), data); +} + +SyncthingDir *SyncthingConnection::findDirInfo(const QString &dir, int &row) +{ + for(SyncthingDir &d : m_dirs) { + if(d.id == dir) { + return &d; + } + ++row; + } + return nullptr; // TODO: dir is unknown, trigger refreshing the config +} + +/*! + * \brief Requests the Syncthing configuration asynchronously. + * + * The signal newConfig() is emitted on success; otherwise error() is emitted. + */ +void SyncthingConnection::requestConfig() +{ + QObject::connect(m_configReply = requestData(QStringLiteral("system/config"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readConfig); +} + +void SyncthingConnection::requestStatus() +{ + QObject::connect(m_statusReply = requestData(QStringLiteral("system/status"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readStatus); +} + +/*! + * \brief Requests the Syncthing events (since the last successful call) asynchronously. + * + * The signal newEvents() is emitted on success; otherwise error() is emitted. + */ +void SyncthingConnection::requestEvents() +{ + QUrlQuery query; + if(m_lastEventId) { + query.addQueryItem(QStringLiteral("since"), QString::number(m_lastEventId)); + } + QObject::connect(m_eventsReply = requestData(QStringLiteral("events"), query), &QNetworkReply::finished, this, &SyncthingConnection::readEvents); +} + +/*! + * \brief Requests a QR code for the specified \a text. + * + * The specified \a callback is called on success; otherwise error() is emitted. + */ +void SyncthingConnection::requestQrCode(const QString &text, std::function callback) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("text"), text); + QNetworkReply *reply = requestData(QStringLiteral("/qr/"), query, false); + QObject::connect(reply, &QNetworkReply::finished, [this, reply, callback] { + reply->deleteLater(); + QPixmap pixmap; + switch(reply->error()) { + case QNetworkReply::NoError: + pixmap.loadFromData(reply->readAll()); + callback(pixmap); + break; + default: + emit error(tr("Unable to request QR-Code: ") + reply->errorString()); + } + }); +} + +void SyncthingConnection::requestLog(std::function &)> callback) +{ + QNetworkReply *reply = requestData(QStringLiteral("system/log"), QUrlQuery()); + QObject::connect(reply, &QNetworkReply::finished, [this, reply, callback] { + reply->deleteLater(); + switch(reply->error()) { + case QNetworkReply::NoError: { + QJsonParseError jsonError; + const QJsonDocument replyDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError); + if(jsonError.error == QJsonParseError::NoError) { + const QJsonArray log(replyDoc.object().value(QStringLiteral("messages")).toArray()); + vector logEntries; + logEntries.reserve(log.size()); + for(const QJsonValue &logVal : log) { + const QJsonObject logObj(logVal.toObject()); + SyncthingLogEntry entry; + entry.when = logObj.value(QStringLiteral("when")).toString(); + entry.message = logObj.value(QStringLiteral("message")).toString(); + logEntries.emplace_back(move(entry)); + } + callback(logEntries); + } else { + emit error(tr("Unable to parse Syncthing log: ") + jsonError.errorString()); + } + break; + } default: + emit error(tr("Unable to request system log: ") + reply->errorString()); + } + }); +} + +/*! + * \brief Reads results of requestConfig(). + */ +void SyncthingConnection::readConfig() +{ + auto *reply = static_cast(sender()); + reply->deleteLater(); + if(reply == m_configReply) { + m_configReply = nullptr; + } + + switch(reply->error()) { + case QNetworkReply::NoError: { + QJsonParseError jsonError; + const QJsonDocument replyDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError); + if(jsonError.error == QJsonParseError::NoError) { + const QJsonObject replyObj = replyDoc.object(); + emit newConfig(replyObj); + readDirs(replyObj.value(QStringLiteral("folders")).toArray()); + readDevs(replyObj.value(QStringLiteral("devices")).toArray()); + } else { + emit error(tr("Unable to parse Syncthing config: ") + jsonError.errorString()); + } + } case QNetworkReply::OperationCanceledError: + return; // intended, not an error + default: + emit error(tr("Unable to request Syncthing config: ") + reply->errorString()); + } +} + +void SyncthingConnection::readDirs(const QJsonArray &dirs) +{ + m_dirs.clear(); + m_dirs.reserve(dirs.size()); + for(const QJsonValue &dirVal : dirs) { + const QJsonObject dirObj(dirVal.toObject()); + SyncthingDir dirItem; + dirItem.id = dirObj.value(QStringLiteral("id")).toString(); + if(!dirItem.id.isEmpty()) { // ignore dirs with empty id + dirItem.label = dirObj.value(QStringLiteral("label")).toString(); + dirItem.path = dirObj.value(QStringLiteral("path")).toString(); + for(const QJsonValue &dev : dirObj.value(QStringLiteral("devices")).toArray()) { + const QString devId = dev.toObject().value(QStringLiteral("deviceID")).toString(); + if(!devId.isEmpty()) { + dirItem.devices << devId; + } + } + dirItem.readOnly = dirObj.value(QStringLiteral("readOnly")).toBool(false); + dirItem.rescanInterval = dirObj.value(QStringLiteral("rescanIntervalS")).toInt(-1); + dirItem.ignorePermissions = dirObj.value(QStringLiteral("ignorePerms")).toBool(false); + dirItem.autoNormalize = dirObj.value(QStringLiteral("autoNormalize")).toBool(false); + dirItem.minDiskFreePercentage = dirObj.value(QStringLiteral("minDiskFreePct")).toInt(-1); + dirItem.status = DirStatus::Unknown; + dirItem.progressPercentage = 0; + m_dirs.emplace_back(move(dirItem)); + } + } + emit newDirs(m_dirs); +} + +void SyncthingConnection::readDevs(const QJsonArray &devs) +{ + m_devs.clear(); + m_devs.reserve(devs.size()); + for(const QJsonValue &devVal: devs) { + const QJsonObject devObj(devVal.toObject()); + SyncthingDev devItem; + devItem.id = devObj.value(QStringLiteral("deviceID")).toString(); + if(!devItem.id.isEmpty()) { // ignore dirs with empty id + devItem.name = devObj.value(QStringLiteral("name")).toString(); + for(const QJsonValue &addrVal : devObj.value(QStringLiteral("addresses")).toArray()) { + devItem.addresses << addrVal.toString(); + } + devItem.compression = devObj.value(QStringLiteral("compression")).toString(); + devItem.certName = devObj.value(QStringLiteral("certName")).toString(); + devItem.introducer = devObj.value(QStringLiteral("introducer")).toBool(false); + devItem.status = DevStatus::Unknown; + devItem.progressPercentage = 0; + m_devs.push_back(move(devItem)); + } + } + emit newDevices(m_devs); +} + +void SyncthingConnection::readStatus() +{ + auto *reply = static_cast(sender()); + reply->deleteLater(); + if(reply == m_statusReply) { + m_statusReply = nullptr; + } + + switch(reply->error()) { + case QNetworkReply::NoError: { + QJsonParseError jsonError; + const QJsonDocument replyDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError); + if(jsonError.error == QJsonParseError::NoError) { + const QJsonObject replyObj = replyDoc.object(); + const QString myId(replyObj.value(QStringLiteral("myID")).toString()); + if(myId != m_myId) { + emit configDirChanged(m_myId = myId); + } + // other values are currently not interesting + } else { + emit error(tr("Unable to parse Syncthing config: ") + jsonError.errorString()); + } + } case QNetworkReply::OperationCanceledError: + return; // intended, not an error + default: + emit error(tr("Unable to request Syncthing config: ") + reply->errorString()); + } +} + +/*! + * \brief Reads results of requestEvents(). + */ +void SyncthingConnection::readEvents() +{ + auto *reply = static_cast(sender()); + reply->deleteLater(); + if(reply == m_eventsReply) { + m_eventsReply = nullptr; + } + + switch(reply->error()) { + case QNetworkReply::NoError: { + QJsonParseError jsonError; + const QJsonDocument replyDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError); + if(jsonError.error == QJsonParseError::NoError) { + const QJsonArray replyArray = replyDoc.array(); + emit newEvents(replyArray); + // search the array for interesting events + for(const QJsonValue &eventVal : replyArray) { + const QJsonObject event = eventVal.toObject(); + m_lastEventId = event.value(QStringLiteral("id")).toInt(m_lastEventId); + const QString eventType = event.value(QStringLiteral("type")).toString(); + const QJsonObject eventData = event.value(QStringLiteral("data")).toObject(); + if(eventType == QLatin1String("Starting")) { + readStartingEvent(eventData); + } else if(eventType == QLatin1String("StateChanged")) { + readStatusChangedEvent(eventData); + } + } + } else { + emit error(tr("Unable to parse Syncthing events: ") + jsonError.errorString()); + setStatus(SyncthingStatus::Disconnected); + return; + } + } case QNetworkReply::TimeoutError: + // no new events available, keep polling + break; + case QNetworkReply::OperationCanceledError: + // intended disconnect, not an error + if(m_reconnecting) { + // if reconnection flag is set, instantly etstablish a new connection ... + m_reconnecting = false; + requestConfig(); + requestStatus(); + m_lastEventId = 0; + requestEvents(); + m_keepPolling = true; + } else { + // ... otherwise keep disconnected + setStatus(SyncthingStatus::Disconnected); + } + return; + default: + emit error(tr("Unable to request Syncthing events: ") + reply->errorString()); + setStatus(SyncthingStatus::Disconnected); + return; + } + + if(m_keepPolling) { + requestEvents(); + // TODO: need to change the status somewhere else + setStatus(SyncthingStatus::Default); + } else { + setStatus(SyncthingStatus::Disconnected); + } +} + +/*! + * \brief Reads results of requestEvents(). + */ +void SyncthingConnection::readStartingEvent(const QJsonObject &event) +{ + QString strValue = event.value(QStringLiteral("home")).toString(); + if(strValue != m_configDir) { + emit configDirChanged(m_configDir = strValue); + } + strValue = event.value(QStringLiteral("myID")).toString(); + if(strValue != m_myId) { + emit configDirChanged(m_myId = strValue); + } +} + +/*! + * \brief Reads results of requestEvents(). + */ +void SyncthingConnection::readStatusChangedEvent(const QJsonObject &event) +{ + const QString dir(event.value(QStringLiteral("folder")).toString()); + if(!dir.isEmpty()) { + // dir status changed + int row; + if(SyncthingDir *dirInfo = findDirInfo(dir, row)) { + const QString statusStr(event.value(QStringLiteral("to")).toString()); + DirStatus status; + if(statusStr == QLatin1String("idle")) { + status = DirStatus::Idle; + } else if(statusStr == QLatin1String("scanning")) { + status = DirStatus::Scanning; + } else if(statusStr == QLatin1String("syncing")) { + status = DirStatus::Synchronizing; + } else if(statusStr == QLatin1String("error")) { + status = DirStatus::OutOfSync; + } else { + status = DirStatus::Unknown; + } + if(dirInfo->status != status) { + dirInfo->status = status; + emit dirStatusChanged(*dirInfo, row); + } + } + } +} + +/*! + * \brief Reads results of rescan(). + */ +void SyncthingConnection::readRescan() +{ + auto *reply = static_cast(sender()); + reply->deleteLater(); + + switch(reply->error()) { + case QNetworkReply::NoError: + break; + default: + emit error(tr("Unable to request rescan: ") + reply->errorString()); + } +} + +/*! + * \brief Reads results of pause(). + */ +void SyncthingConnection::readPauseResume() +{ + auto *reply = static_cast(sender()); + reply->deleteLater(); + + switch(reply->error()) { + case QNetworkReply::NoError: + break; + default: + emit error(tr("Unable to request pause/resume: ") + reply->errorString()); + } +} + +/*! + * \brief Sets the connection status. Ensures statusChanged() is emitted. + * \param status Specifies the status; should be either SyncthingStatus::Disconnected or SyncthingStatus::Default. + * \remarks If \a status is not SyncthingStatus::Disconnected the best status for the current connection is determined automatically. + */ +void SyncthingConnection::setStatus(SyncthingStatus status) +{ + if(m_status != status) { + switch(m_status = status) { + case SyncthingStatus::Disconnected: + break; + default: + if(m_unreadNotifications) { + m_status = SyncthingStatus::NotificationsAvailable; + } else { + // check whether at least one directory is synchronizing + bool synchronizing = false; + for(const SyncthingDir &dir : m_dirs) { + if(dir.status == DirStatus::Synchronizing) { + synchronizing = true; + } + } + if(synchronizing) { + m_status = SyncthingStatus::Synchronizing; + } else { + // check whether at least one device is paused + bool paused = false; + for(const SyncthingDev &dev : m_devs) { + if(dev.status == DevStatus::Paused) { + paused = true; + } + } + if(paused) { + m_status = SyncthingStatus::Paused; + } else { + m_status = SyncthingStatus::Default; + } + } + } + } + emit statusChanged(m_status); + } +} + +} diff --git a/data/syncthingconnection.h b/data/syncthingconnection.h new file mode 100644 index 0000000..d32f315 --- /dev/null +++ b/data/syncthingconnection.h @@ -0,0 +1,332 @@ +#ifndef SYNCTHINGCONNECTION_H +#define SYNCTHINGCONNECTION_H + +#include + +#include +#include + +QT_FORWARD_DECLARE_CLASS(QNetworkAccessManager) +QT_FORWARD_DECLARE_CLASS(QNetworkReply) +QT_FORWARD_DECLARE_CLASS(QNetworkRequest) +QT_FORWARD_DECLARE_CLASS(QUrlQuery) +QT_FORWARD_DECLARE_CLASS(QJsonObject) +QT_FORWARD_DECLARE_CLASS(QJsonArray) + +namespace Data { + +QNetworkAccessManager &networkAccessManager(); + +enum class SyncthingStatus +{ + Disconnected, + Default, + NotificationsAvailable, + Paused, + Synchronizing +}; + +enum class DirStatus +{ + Unknown, + Idle, + Scanning, + Synchronizing, + Paused, + OutOfSync +}; + +struct SyncthingDir +{ + QString id; + QString label; + QString path; + QStringList devices; + bool readOnly; + bool ignorePermissions; + bool autoNormalize; + int rescanInterval; + int minDiskFreePercentage; + DirStatus status; + int progressPercentage; +}; + +enum class DevStatus +{ + Unknown, + Disconnected, + Idle, + Synchronizing, + Paused, + OutOfSync +}; + +struct SyncthingDev +{ + QString id; + QString name; + QStringList addresses; + QString compression; + QString certName; + DevStatus status; + int progressPercentage; + bool introducer; +}; + +struct SyncthingLogEntry +{ + QString when; + QString message; +}; + +class SyncthingConnection : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString syncthingUrl READ syncthingUrl WRITE setSyncthingUrl) + Q_PROPERTY(QByteArray apiKey READ apiKey WRITE setApiKey) + Q_PROPERTY(SyncthingStatus status READ status NOTIFY statusChanged) + Q_PROPERTY(QString configDir READ configDir NOTIFY configDirChanged) + Q_PROPERTY(QString myId READ myId NOTIFY myIdChanged) + +public: + explicit SyncthingConnection(const QString &syncthingUrl = QStringLiteral("http://localhost:8080"), const QByteArray &apiKey = QByteArray(), QObject *parent = nullptr); + ~SyncthingConnection(); + + const QString &syncthingUrl() const; + void setSyncthingUrl(const QString &url); + const QByteArray &apiKey() const; + void setApiKey(const QByteArray &apiKey); + const QString &user() const; + const QString &password() const; + void setCredentials(const QString &user, const QString &password); + SyncthingStatus status() const; + QString statusText() const; + bool isConnected() const; + const QString &configDir() const; + const QString &myId() const; + const std::vector &dirInfo() const; + const std::vector &devInfo() const; + void requestQrCode(const QString &text, std::function callback); + void requestLog(std::function &)> callback); + +public Q_SLOTS: + void connect(); + void disconnect(); + void reconnect(); + void pause(const QString &dev); + void pauseAllDevs(); + void resume(const QString &dev); + void resumeAllDevs(); + void rescan(const QString &dir); + void rescanAllDirs(); + void notificationsRead(); + +Q_SIGNALS: + /*! + * \brief Indicates new configuration (dirs, devs, ...) is available. + * \remarks + * - Configuration is requested automatically when connecting. + * - Previous directories (and directory info objects!) are invalidated. + * - Previous devices (and device info objects!) are invalidated. + */ + void newConfig(const QJsonObject &config); + + /*! + * \brief Indicates new directories are available. + * \remarks Always emitted after newConfig() as soon as new directory info objects become available. + */ + void newDirs(const std::vector &dirs); + + /*! + * \brief Indicates new devices are available. + * \remarks Always emitted after newConfig() as soon as new device info objects become available. + */ + void newDevices(const std::vector &devs); + + /*! + * \brief Indicates new events (dir status changed, ...) are available. + * \remarks New events are automatically polled when connected. + */ + void newEvents(const QJsonArray &events); + + /*! + * \brief Indicates the status of the specified \a dir changed. + */ + void dirStatusChanged(const SyncthingDir &dir, int index); + + void newNotification(const QString &message); + + /*! + * \brief Indicates a request (for configuration, events, ...) failed. + */ + void error(const QString &errorMessage); + + /*! + * \brief Indicates the status of the connection changed. + */ + void statusChanged(SyncthingStatus newStatus); + + /*! + * \brief Indicates the Syncthing home/configuration directory changed. + */ + void configDirChanged(const QString &newConfigDir); + + /*! + * \brief Indicates ID of the own Syncthing device changed. + */ + void myIdChanged(const QString &myNewId); + +private Q_SLOTS: + void requestConfig(); + void requestStatus(); + void requestEvents(); + + void readConfig(); + void readDirs(const QJsonArray &dirs); + void readDevs(const QJsonArray &devs); + + void readStatus(); + + void readEvents(); + void readStartingEvent(const QJsonObject &event); + void readStatusChangedEvent(const QJsonObject &event); + void readRescan(); + void readPauseResume(); + + void setStatus(SyncthingStatus status); + +private: + QNetworkRequest prepareRequest(const QString &path, const QUrlQuery &query, bool rest = true); + QNetworkReply *requestData(const QString &path, const QUrlQuery &query, bool rest = true); + QNetworkReply *postData(const QString &path, const QUrlQuery &query, const QByteArray &data = QByteArray()); + SyncthingDir *findDirInfo(const QString &dir, int &row); + + QString m_syncthingUrl; + QByteArray m_apiKey; + QString m_user; + QString m_password; + SyncthingStatus m_status; + bool m_keepPolling; + bool m_reconnecting; + int m_lastEventId; + QString m_configDir; + QString m_myId; + QNetworkReply *m_configReply; + QNetworkReply *m_statusReply; + QNetworkReply *m_eventsReply; + bool m_unreadNotifications; + std::vector m_dirs; + std::vector m_devs; +}; + +/*! + * \brief Returns the URL used to connect to Syncthing. + */ +inline const QString &SyncthingConnection::syncthingUrl() const +{ + return m_syncthingUrl; +} + +/*! + * \brief Sets the URL used to connect to Syncthing. + */ +inline void SyncthingConnection::setSyncthingUrl(const QString &url) +{ + m_syncthingUrl = url; +} + +/*! + * \brief Returns the API key used to connect to Syncthing. + */ +inline const QByteArray &SyncthingConnection::apiKey() const +{ + return m_apiKey; +} + +/*! + * \brief Sets the API key used to connect to Syncthing. + */ +inline void SyncthingConnection::setApiKey(const QByteArray &apiKey) +{ + m_apiKey = apiKey; +} + +/*! + * \brief Returns the user name which has been set using setCredentials(). + */ +inline const QString &SyncthingConnection::user() const +{ + return m_user; +} + +/*! + * \brief Returns the password which has been set using setCredentials(). + */ +inline const QString &SyncthingConnection::password() const +{ + return m_password; +} + +/*! + * \brief Provides credentials used for HTTP authentication. + */ +inline void SyncthingConnection::setCredentials(const QString &user, const QString &password) +{ + m_user = user, m_password = password; +} + +/*! + * \brief Returns the connection status. + */ +inline SyncthingStatus SyncthingConnection::status() const +{ + return m_status; +} + +/*! + * \brief Returns whether the connection has been established. + */ +inline bool SyncthingConnection::isConnected() const +{ + return m_status != SyncthingStatus::Disconnected; +} + +/*! + * \brief Returns the Syncthing home/configuration directory. + */ +inline const QString &SyncthingConnection::configDir() const +{ + return m_configDir; +} + +/*! + * \brief Returns the ID of the own Syncthing device. + */ +inline const QString &SyncthingConnection::myId() const +{ + return m_myId; +} + +/*! + * \brief Returns all available directory info. + * \remarks The returned object container object is persistent. However, the contained + * info objects are invalidated when the newConfig() signal is emitted. + */ +inline const std::vector &SyncthingConnection::dirInfo() const +{ + return m_dirs; +} + +/*! + * \brief Returns all available device info. + * \remarks The returned object container object is persistent. However, the contained + * info objects are invalidated when the newConfig() signal is emitted. + */ +inline const std::vector &SyncthingConnection::devInfo() const +{ + return m_devs; +} + +} + +#endif // SYNCTHINGCONNECTION_H diff --git a/data/syncthingdevicemodel.cpp b/data/syncthingdevicemodel.cpp new file mode 100644 index 0000000..7f619ac --- /dev/null +++ b/data/syncthingdevicemodel.cpp @@ -0,0 +1,195 @@ +#include "./syncthingdevicemodel.h" +#include "./syncthingconnection.h" + +namespace Data { + +SyncthingDeviceModel::SyncthingDeviceModel(SyncthingConnection &connection, QObject *parent) : + QAbstractItemModel(parent), + m_connection(connection), + m_devs(connection.devInfo()), + m_unknownIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-disconnected.svg"))), + m_idleIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-ok.svg"))), + m_syncIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-sync.svg"))), + m_errorIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-error.svg"))), + m_pausedIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-pause.svg"))), + m_otherIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-default.svg"))) +{ + +} + +/*! + * \brief Returns the device info for the spcified \a index. The returned object is not persistent. + */ +const SyncthingDev *SyncthingDeviceModel::devInfo(const QModelIndex &index) const +{ + return (index.parent().isValid() ? devInfo(index.parent()) : (index.row() < m_devs.size() ? &m_devs[index.row()] : nullptr)); +} + +QModelIndex SyncthingDeviceModel::index(int row, int column, const QModelIndex &parent) const +{ + if(!parent.isValid()) { + // top-level: all dev IDs + if(row < rowCount(parent)) { + return createIndex(row, column, -1); + } + } else if(!parent.parent().isValid()) { + // dev-level: dev attributes + if(row < rowCount(parent)) { + return createIndex(row, column, parent.row()); + } + } + return QModelIndex(); +} + +QModelIndex SyncthingDeviceModel::parent(const QModelIndex &child) const +{ + return child.internalId() != static_cast(-1) ? index(child.internalId(), 0, QModelIndex()) : QModelIndex(); +} + +QVariant SyncthingDeviceModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch(orientation) { + case Qt::Horizontal: + switch(role) { + case Qt::DisplayRole: + switch(section) { + case 0: return tr("ID"); + case 1: return tr("Status"); + } + break; + default: + ; + } + break; + default: + ; + } + return QVariant(); +} + +QVariant SyncthingDeviceModel::data(const QModelIndex &index, int role) const +{ + if(index.isValid()) { + if(index.parent().isValid()) { + // dir attributes + if(index.parent().row() < m_devs.size()) { + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: // attribute names + switch(index.row()) { + case 0: return tr("ID"); + case 1: return tr("Addresses"); + case 2: return tr("Compression"); + case 3: return tr("Certificate"); + case 4: return tr("Introducer"); + } + break; + case 1: // attribute values + const SyncthingDev &dev = m_devs[index.parent().row()]; + switch(index.row()) { + case 0: return dev.id; + case 1: return dev.addresses.join(QStringLiteral(", ")); + case 2: return dev.compression; + case 3: return dev.certName.isEmpty() ? tr("none") : dev.certName; + case 4: return dev.introducer ? tr("yes") : tr("no"); + } + break; + } + break; + default: + ; + } + } + } else if(index.row() < m_devs.size()) { + // dir IDs and status + const SyncthingDev &dev = m_devs[index.row()]; + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: return dev.name.isEmpty() ? dev.id : dev.name; + case 1: + switch(dev.status) { + case DevStatus::Unknown: return tr("Unknown status"); + case DevStatus::Idle: return tr("Idle"); + case DevStatus::Disconnected: return tr("Idle"); + case DevStatus::Synchronizing: return dev.progressPercentage > 0 ? tr("Synchronizing (%1 %)").arg(dev.progressPercentage) : tr("Synchronizing"); + case DevStatus::Paused: return tr("Paused"); + case DevStatus::OutOfSync: return tr("Out of sync"); + } + break; + } + break; + case Qt::DecorationRole: + switch(index.column()) { + case 0: + switch(dev.status) { + case DevStatus::Unknown: + case DevStatus::Disconnected: return m_unknownIcon; + case DevStatus::Idle: return m_idleIcon; + case DevStatus::Synchronizing: return m_syncIcon; + case DevStatus::Paused: return m_pausedIcon; + case DevStatus::OutOfSync: return m_errorIcon; + } + break; + } + break; + case Qt::TextAlignmentRole: + switch(index.column()) { + case 0: break; + case 1: return static_cast(Qt::AlignRight | Qt::AlignVCenter); + } + break; + case Qt::ForegroundRole: + switch(index.column()) { + case 0: break; + case 1: + switch(dev.status) { + case DevStatus::Unknown: break; + case DevStatus::Disconnected: break; + case DevStatus::Idle: return QColor(Qt::darkGreen); + case DevStatus::Synchronizing: return QColor(Qt::blue); + case DevStatus::Paused: break; + case DevStatus::OutOfSync: return QColor(Qt::red); + } + break; + } + break; + default: + ; + } + } + } + return QVariant(); +} + +bool SyncthingDeviceModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + return false; +} + +int SyncthingDeviceModel::rowCount(const QModelIndex &parent) const +{ + if(!parent.isValid()) { + return m_devs.size(); + } else if(!parent.parent().isValid()) { + return 5; + } else { + return 0; + } +} + +int SyncthingDeviceModel::columnCount(const QModelIndex &parent) const +{ + if(!parent.isValid()) { + return 2; // name/id, status + } else if(!parent.parent().isValid()) { + return 2; // field name and value + } else { + return 0; + } +} + +} // namespace Data diff --git a/data/syncthingdevicemodel.h b/data/syncthingdevicemodel.h new file mode 100644 index 0000000..0628643 --- /dev/null +++ b/data/syncthingdevicemodel.h @@ -0,0 +1,43 @@ +#ifndef DATA_SYNCTHINGDEVICEMODEL_H +#define DATA_SYNCTHINGDEVICEMODEL_H + +#include +#include + +#include + +namespace Data { + +class SyncthingConnection; +struct SyncthingDev; + +class SyncthingDeviceModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit SyncthingDeviceModel(SyncthingConnection &connection, QObject *parent = nullptr); + +public Q_SLOTS: + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &child) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + const SyncthingDev *devInfo(const QModelIndex &index) const; + +private: + Data::SyncthingConnection &m_connection; + const std::vector &m_devs; + const QIcon m_unknownIcon; + const QIcon m_idleIcon; + const QIcon m_syncIcon; + const QIcon m_errorIcon; + const QIcon m_pausedIcon; + const QIcon m_otherIcon; +}; + +} // namespace Data + +#endif // DATA_SYNCTHINGDEVICEMODEL_H diff --git a/data/syncthingdirectorymodel.cpp b/data/syncthingdirectorymodel.cpp new file mode 100644 index 0000000..5ac0852 --- /dev/null +++ b/data/syncthingdirectorymodel.cpp @@ -0,0 +1,215 @@ +#include "./syncthingdirectorymodel.h" +#include "./syncthingconnection.h" + +namespace Data { + +SyncthingDirectoryModel::SyncthingDirectoryModel(SyncthingConnection &connection, QObject *parent) : + QAbstractItemModel(parent), + m_connection(connection), + m_dirs(connection.dirInfo()), + m_unknownIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-disconnected.svg"))), + m_idleIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-ok.svg"))), + m_syncIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-sync.svg"))), + m_errorIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-error.svg"))), + m_pausedIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-pause.svg"))), + m_otherIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-default.svg"))) +{ + connect(&m_connection, &SyncthingConnection::newConfig, this, &SyncthingDirectoryModel::newConfig); + connect(&m_connection, &SyncthingConnection::newDirs, this, &SyncthingDirectoryModel::newDirs); + connect(&m_connection, &SyncthingConnection::dirStatusChanged, this, &SyncthingDirectoryModel::dirStatusChanged); +} + +/*! + * \brief Returns the directory info for the spcified \a index. The returned object is not persistent. + */ +const SyncthingDir *SyncthingDirectoryModel::dirInfo(const QModelIndex &index) const +{ + return (index.parent().isValid() ? dirInfo(index.parent()) : (index.row() < m_dirs.size() ? &m_dirs[index.row()] : nullptr)); +} + +QModelIndex SyncthingDirectoryModel::index(int row, int column, const QModelIndex &parent) const +{ + if(!parent.isValid()) { + // top-level: all dir labels/IDs + if(row < rowCount(parent)) { + return createIndex(row, column, -1); + } + } else if(!parent.parent().isValid()) { + // dir-level: dir attributes + if(row < rowCount(parent)) { + return createIndex(row, column, parent.row()); + } + } + return QModelIndex(); +} + +QModelIndex SyncthingDirectoryModel::parent(const QModelIndex &child) const +{ + return child.internalId() != static_cast(-1) ? index(child.internalId(), 0, QModelIndex()) : QModelIndex(); +} + +QVariant SyncthingDirectoryModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch(orientation) { + case Qt::Horizontal: + switch(role) { + case Qt::DisplayRole: + switch(section) { + case 0: return tr("ID"); + case 1: return tr("Status"); + } + break; + default: + ; + } + break; + default: + ; + } + return QVariant(); +} + +QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const +{ + if(index.isValid()) { + if(index.parent().isValid()) { + // dir attributes + if(index.parent().row() < m_dirs.size()) { + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: // attribute names + switch(index.row()) { + case 0: return tr("ID"); + case 1: return tr("Path"); + case 2: return tr("Devices"); + case 3: return tr("Read-only"); + case 4: return tr("Rescan interval"); + } + break; + case 1: // attribute values + const SyncthingDir &dir = m_dirs[index.parent().row()]; + switch(index.row()) { + case 0: return dir.id; + case 1: return dir.path; + case 2: return dir.devices.join(QStringLiteral(", ")); + case 3: return dir.readOnly ? tr("yes") : tr("no"); + case 4: return QStringLiteral("%1 s").arg(dir.rescanInterval); + } + break; + } + break; + default: + ; + } + } + } else if(index.row() < m_dirs.size()) { + // dir IDs and status + const SyncthingDir &dir = m_dirs[index.row()]; + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: return dir.label.isEmpty() ? dir.id : dir.label; + case 1: + switch(dir.status) { + case DirStatus::Unknown: return tr("Unknown status"); + case DirStatus::Idle: return tr("Idle"); + case DirStatus::Scanning: return dir.progressPercentage > 0 ? tr("Scanning (%1 %)").arg(dir.progressPercentage) : tr("Scanning"); + case DirStatus::Synchronizing: return dir.progressPercentage > 0 ? tr("Synchronizing (%1 %)").arg(dir.progressPercentage) : tr("Synchronizing"); + case DirStatus::Paused: return tr("Paused"); + case DirStatus::OutOfSync: return tr("Out of sync"); + } + break; + } + break; + case Qt::DecorationRole: + switch(index.column()) { + case 0: + switch(dir.status) { + case DirStatus::Unknown: return m_unknownIcon; + case DirStatus::Idle: return m_idleIcon; + case DirStatus::Scanning: return m_otherIcon; + case DirStatus::Synchronizing: return m_syncIcon; + case DirStatus::Paused: return m_pausedIcon; + case DirStatus::OutOfSync: return m_errorIcon; + } + break; + } + break; + case Qt::TextAlignmentRole: + switch(index.column()) { + case 0: break; + case 1: return static_cast(Qt::AlignRight | Qt::AlignVCenter); + } + break; + case Qt::ForegroundRole: + switch(index.column()) { + case 0: break; + case 1: + switch(dir.status) { + case DirStatus::Unknown: break; + case DirStatus::Idle: return QColor(Qt::darkGreen); + case DirStatus::Scanning: return QColor(Qt::blue); + case DirStatus::Synchronizing: return QColor(Qt::blue); + case DirStatus::Paused: break; + case DirStatus::OutOfSync: return QColor(Qt::red); + } + break; + } + break; + default: + ; + } + } + } + return QVariant(); +} + +bool SyncthingDirectoryModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + return false; +} + +int SyncthingDirectoryModel::rowCount(const QModelIndex &parent) const +{ + if(!parent.isValid()) { + return m_dirs.size(); + } else if(!parent.parent().isValid()) { + return 5; + } else { + return 0; + } +} + +int SyncthingDirectoryModel::columnCount(const QModelIndex &parent) const +{ + if(!parent.isValid()) { + return 2; // label/ID, status/buttons + } else if(!parent.parent().isValid()) { + return 2; // field name and value + } else { + return 0; + } +} + +void SyncthingDirectoryModel::newConfig() +{ + beginResetModel(); +} + +void SyncthingDirectoryModel::newDirs() +{ + endResetModel(); +} + +void SyncthingDirectoryModel::dirStatusChanged(const SyncthingDir &, int index) +{ + const QModelIndex modelIndex1(this->index(index, 0, QModelIndex())); + emit dataChanged(modelIndex1, modelIndex1, QVector() << Qt::DecorationRole); + const QModelIndex modelIndex2(this->index(index, 1, QModelIndex())); + emit dataChanged(modelIndex2, modelIndex2, QVector() << Qt::DisplayRole << Qt::ForegroundRole); +} + +} // namespace Data diff --git a/data/syncthingdirectorymodel.h b/data/syncthingdirectorymodel.h new file mode 100644 index 0000000..415590a --- /dev/null +++ b/data/syncthingdirectorymodel.h @@ -0,0 +1,48 @@ +#ifndef DATA_SYNCTHINGDIRECTORYMODEL_H +#define DATA_SYNCTHINGDIRECTORYMODEL_H + +#include +#include + +#include + +namespace Data { + +class SyncthingConnection; +struct SyncthingDir; + +class SyncthingDirectoryModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit SyncthingDirectoryModel(SyncthingConnection &connection, QObject *parent = nullptr); + +public Q_SLOTS: + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &child) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + const SyncthingDir *dirInfo(const QModelIndex &index) const; + +private slots: + void newConfig(); + void newDirs(); + void dirStatusChanged(const SyncthingDir &, int index); + +private: + Data::SyncthingConnection &m_connection; + const std::vector &m_dirs; + const QIcon m_unknownIcon; + const QIcon m_idleIcon; + const QIcon m_syncIcon; + const QIcon m_errorIcon; + const QIcon m_pausedIcon; + const QIcon m_otherIcon; +}; + +} // namespace Data + +#endif // DATA_SYNCTHINGDIRECTORYMODEL_H diff --git a/gui/connectionoptionpage.ui b/gui/connectionoptionpage.ui new file mode 100644 index 0000000..5def1b0 --- /dev/null +++ b/gui/connectionoptionpage.ui @@ -0,0 +1,192 @@ + + + QtGui::ConnectionOptionPage + + + + 0 + 0 + 433 + 267 + + + + Connection + + + + + + Syncthing URL + + + + + + + + + + Authentication + + + + + + + + + + false + + + User + + + + + + + false + + + Password + + + + + + + false + + + + + + + false + + + QLineEdit::Password + + + + + + + Qt::Horizontal + + + + + + + disconnected + + + + + + + Status + + + + + + + Apply connection settings and try to reconnect + + + + .. + + + + + + + + + + API key + + + + + + + + Widgets::ClearLineEdit + QLineEdit +
qtutilities/widgets/clearlineedit.h
+
+
+ + + + authCheckBox + toggled(bool) + userNameLabel + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + authCheckBox + toggled(bool) + userNameLineEdit + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + authCheckBox + toggled(bool) + passwordLabel + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + authCheckBox + toggled(bool) + passwordLineEdit + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + +
diff --git a/gui/devview.cpp b/gui/devview.cpp new file mode 100644 index 0000000..2f2db6a --- /dev/null +++ b/gui/devview.cpp @@ -0,0 +1,48 @@ +#include "./devview.h" + +#include +#include +#include +#include +#include +#include + +namespace QtGui { + +DevView::DevView(QWidget *parent) : + QTreeView(parent) +{ + header()->setSectionResizeMode(QHeaderView::ResizeToContents); + header()->hide(); + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &DevView::customContextMenuRequested, this, &DevView::showContextMenu); +} + +void DevView::showContextMenu() +{ + if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { + QMenu menu; + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy")), &QAction::triggered, this, &DevView::copySelectedItem); + menu.exec(QCursor::pos()); + } +} + +void DevView::copySelectedItem() +{ + if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { + const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0); + QString text; + if(selectedIndex.parent().isValid()) { + // dev attribute + text = model()->data(model()->index(selectedIndex.row(), 1, selectedIndex.parent())).toString(); + } else { + // dev name/id + text = model()->data(selectedIndex).toString(); + } + if(!text.isEmpty()) { + QGuiApplication::clipboard()->setText(text); + } + } +} + +} diff --git a/gui/devview.h b/gui/devview.h new file mode 100644 index 0000000..abc6f74 --- /dev/null +++ b/gui/devview.h @@ -0,0 +1,22 @@ +#ifndef DEVVIEW_H +#define DEVVIEW_H + +#include + +namespace QtGui { + +class DevView : public QTreeView +{ + Q_OBJECT +public: + DevView(QWidget *parent = nullptr); + +private Q_SLOTS: + void showContextMenu(); + void copySelectedItem(); + +}; + +} + +#endif // DEVVIEW_H diff --git a/gui/dirbuttonsitemdelegate.cpp b/gui/dirbuttonsitemdelegate.cpp new file mode 100644 index 0000000..7f93549 --- /dev/null +++ b/gui/dirbuttonsitemdelegate.cpp @@ -0,0 +1,60 @@ +#include "./dirbuttonsitemdelegate.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QtGui { + +inline int centerObj(int avail, int size) +{ + return (avail - size) / 2; +} + +DirButtonsItemDelegate::DirButtonsItemDelegate(QObject* parent) : + QStyledItemDelegate(parent), + m_refreshIcon(QIcon::fromTheme(QStringLiteral("view-refresh")).pixmap(QSize(16, 16))), + m_folderIcon(QIcon::fromTheme(QStringLiteral("folder-open")).pixmap(QSize(16, 16))) +{} + +void DirButtonsItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // use the customization only on top-level rows + if(index.parent().isValid()) { + QStyledItemDelegate::paint(painter, option, index); + } else { + // init style options to use drawControl(), except for the text + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.text.clear(); + opt.features = QStyleOptionViewItem::None; + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter); + + // draw text + QRectF textRect = option.rect; + textRect.setWidth(textRect.width() - 38); + QTextOption textOption; + textOption.setAlignment(opt.displayAlignment); + painter->setFont(opt.font); + painter->setPen(opt.palette.color(QPalette::Text)); + painter->drawText(textRect, displayText(index.data(Qt::DisplayRole), option.locale), textOption); + + // draw buttons + const int buttonY = option.rect.y() + centerObj(option.rect.height(), 16); + painter->drawPixmap(option.rect.right() - 34, buttonY, 16, 16, m_refreshIcon); + painter->drawPixmap(option.rect.right() - 16, buttonY, 16, 16, m_folderIcon); + } + +} + +//QSize DirButtonsItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +//{ +// return QSize(16, 16); +//} + +} diff --git a/gui/dirbuttonsitemdelegate.h b/gui/dirbuttonsitemdelegate.h new file mode 100644 index 0000000..9043ea6 --- /dev/null +++ b/gui/dirbuttonsitemdelegate.h @@ -0,0 +1,25 @@ +#ifndef DIRBUTTONSITEMDELEGATE_H +#define DIRBUTTONSITEMDELEGATE_H + +#include +#include + +namespace QtGui { + +class DirButtonsItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + DirButtonsItemDelegate(QObject *parent); + + void paint(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const; + //QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; + +private: + const QPixmap m_refreshIcon; + const QPixmap m_folderIcon; +}; + +} + +#endif // DIRBUTTONSITEMDELEGATE_H diff --git a/gui/dirview.cpp b/gui/dirview.cpp new file mode 100644 index 0000000..661eef8 --- /dev/null +++ b/gui/dirview.cpp @@ -0,0 +1,67 @@ +#include "./dirview.h" +#include "./dirbuttonsitemdelegate.h" + +#include +#include +#include +#include +#include +#include + +namespace QtGui { + +DirView::DirView(QWidget *parent) : + QTreeView(parent) +{ + header()->setSectionResizeMode(QHeaderView::ResizeToContents); + header()->hide(); + setItemDelegateForColumn(1, new DirButtonsItemDelegate(this)); + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &DirView::customContextMenuRequested, this, &DirView::showContextMenu); +} + +void DirView::mouseReleaseEvent(QMouseEvent *event) +{ + QTreeView::mouseReleaseEvent(event); + const QPoint pos(event->pos()); + const QModelIndex clickedIndex(indexAt(event->pos())); + if(clickedIndex.isValid() && clickedIndex.column() == 1 && !clickedIndex.parent().isValid()) { + const QRect itemRect(visualRect(clickedIndex)); + if(pos.x() > itemRect.right() - 34) { + if(pos.x() > itemRect.right() - 17) { + emit openDir(clickedIndex); + } else { + emit scanDir(clickedIndex); + } + } + } +} + +void DirView::showContextMenu() +{ + if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { + QMenu menu; + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy")), &QAction::triggered, this, &DirView::copySelectedItem); + menu.exec(QCursor::pos()); + } +} + +void DirView::copySelectedItem() +{ + if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { + const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0); + QString text; + if(selectedIndex.parent().isValid()) { + // dev attribute + text = model()->data(model()->index(selectedIndex.row(), 1, selectedIndex.parent())).toString(); + } else { + // dev label/id + text = model()->data(selectedIndex).toString(); + } + if(!text.isEmpty()) { + QGuiApplication::clipboard()->setText(text); + } + } +} + +} diff --git a/gui/dirview.h b/gui/dirview.h new file mode 100644 index 0000000..af39465 --- /dev/null +++ b/gui/dirview.h @@ -0,0 +1,29 @@ +#ifndef DIRVIEW_H +#define DIRVIEW_H + +#include + +namespace QtGui { + +class DirView : public QTreeView +{ + Q_OBJECT +public: + DirView(QWidget *parent = nullptr); + +Q_SIGNALS: + void openDir(const QModelIndex &index); + void scanDir(const QModelIndex &index); + +protected: + void mouseReleaseEvent(QMouseEvent *event); + +private Q_SLOTS: + void showContextMenu(); + void copySelectedItem(); + +}; + +} + +#endif // DIRVIEW_H diff --git a/gui/launcheroptionpage.ui b/gui/launcheroptionpage.ui new file mode 100644 index 0000000..208d5d5 --- /dev/null +++ b/gui/launcheroptionpage.ui @@ -0,0 +1,20 @@ + + + QtGui::LauncherOptionPage + + + + 0 + 0 + 229 + 164 + + + + Launcher + + + + + + diff --git a/gui/notificationsoptionpage.ui b/gui/notificationsoptionpage.ui new file mode 100644 index 0000000..ff2f2a3 --- /dev/null +++ b/gui/notificationsoptionpage.ui @@ -0,0 +1,54 @@ + + + QtGui::NotificationsOptionPage + + + Notifications + + + + + + Notify on disconnect + + + + + + + Notify on errors + + + + + + + Notify on sync complete + + + + + + + Show Syncthing notifications + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/gui/settingsdialog.cpp b/gui/settingsdialog.cpp new file mode 100644 index 0000000..8064437 --- /dev/null +++ b/gui/settingsdialog.cpp @@ -0,0 +1,214 @@ +#include "./settingsdialog.h" + +#include "../application/settings.h" +#include "../data/syncthingconnection.h" + +#include "ui_connectionoptionpage.h" +#include "ui_notificationsoptionpage.h" +#include "ui_launcheroptionpage.h" +#include "ui_webviewoptionpage.h" + +#include +#include + +#include +#include +#include + +#include + +using namespace std; +using namespace Settings; +using namespace Dialogs; +using namespace Data; + +namespace QtGui { + +// ConnectionOptionPage +ConnectionOptionPage::ConnectionOptionPage(Data::SyncthingConnection *connection, QWidget *parentWidget) : + ConnectionOptionPageBase(parentWidget), + m_connection(connection) +{} + +ConnectionOptionPage::~ConnectionOptionPage() +{} + +QWidget *ConnectionOptionPage::setupWidget() +{ + auto *w = ConnectionOptionPageBase::setupWidget(); + updateConnectionStatus(); + QObject::connect(m_connection, &SyncthingConnection::statusChanged, bind(&ConnectionOptionPage::updateConnectionStatus, this)); + QObject::connect(ui()->connectPushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::applyAndReconnect, this)); + return w; +} + +void ConnectionOptionPage::updateConnectionStatus() +{ + ui()->statusLabel->setText(m_connection->statusText()); +} + +bool ConnectionOptionPage::apply() +{ + if(hasBeenShown()) { + syncthingUrl() = ui()->urlLineEdit->text(); + authEnabled() = ui()->authCheckBox->isChecked(); + userName() = ui()->userNameLineEdit->text(); + password() = ui()->passwordLineEdit->text(); + apiKey() = ui()->apiKeyLineEdit->text().toUtf8(); + + } + return true; +} + +void ConnectionOptionPage::reset() +{ + if(hasBeenShown()) { + ui()->urlLineEdit->setText(syncthingUrl()); + ui()->authCheckBox->setChecked(authEnabled()); + ui()->userNameLineEdit->setText(userName()); + ui()->passwordLineEdit->setText(password()); + ui()->apiKeyLineEdit->setText(apiKey()); + } +} + +void ConnectionOptionPage::applyAndReconnect() +{ + apply(); + m_connection->setSyncthingUrl(Settings::syncthingUrl()); + m_connection->setApiKey(Settings::apiKey()); + if(Settings::authEnabled()) { + m_connection->setCredentials(Settings::userName(), Settings::password()); + } else { + m_connection->setCredentials(QString(), QString()); + } + m_connection->reconnect(); +} + +// NotificationsOptionPage +NotificationsOptionPage::NotificationsOptionPage(QWidget *parentWidget) : + NotificationsOptionPageBase(parentWidget) +{} + +NotificationsOptionPage::~NotificationsOptionPage() +{} + +bool NotificationsOptionPage::apply() +{ + if(hasBeenShown()) { + notifyOnDisconnect() = ui()->notifyOnDisconnectCheckBox->isChecked(); + notifyOnErrors() = ui()->notifyOnErrorsCheckBox->isChecked(); + notifyOnSyncComplete() = ui()->notifyOnSyncCompleteCheckBox->isChecked(); + showSyncthingNotifications() = ui()->showSyncthingNotificationsCheckBox->isChecked(); + } + return true; +} + +void NotificationsOptionPage::reset() +{ + if(hasBeenShown()) { + ui()->notifyOnDisconnectCheckBox->setChecked(notifyOnDisconnect()); + ui()->notifyOnErrorsCheckBox->setChecked(notifyOnErrors()); + ui()->notifyOnSyncCompleteCheckBox->setChecked(notifyOnSyncComplete()); + ui()->showSyncthingNotificationsCheckBox->setChecked(showSyncthingNotifications()); + } +} + +// LauncherOptionPage +LauncherOptionPage::LauncherOptionPage(QWidget *parentWidget) : + LauncherOptionPageBase(parentWidget) +{} + +LauncherOptionPage::~LauncherOptionPage() +{} + +bool LauncherOptionPage::apply() +{ + if(hasBeenShown()) { + } + return true; +} + +void LauncherOptionPage::reset() +{ + if(hasBeenShown()) { + } +} + +// WebViewOptionPage +WebViewOptionPage::WebViewOptionPage(QWidget *parentWidget) : + WebViewOptionPageBase(parentWidget) +{} + +WebViewOptionPage::~WebViewOptionPage() +{} + +#if !defined(SYNCTHINGTRAY_USE_WEBENGINE) && !defined(SYNCTHINGTRAY_USE_WEBKIT) +QWidget *WebViewOptionPage::setupWidget() +{ + auto *label = new QLabel; + label->setWindowTitle(QCoreApplication::translate("QtGui::WebViewOptionPage", "General")); + label->setAlignment(Qt::AlignCenter); + label->setText(QCoreApplication::translate("QtGui::WebViewOptionPage", "Syncthing Tray has not been built with vieb view support utilizing either Qt WebKit or Qt WebEngine.\nThe Web UI will be opened in the default web browser instead.")); + return label; +} +#endif + +bool WebViewOptionPage::apply() +{ +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) + if(hasBeenShown()) { + webViewDisabled() = ui()->disableCheckBox->isChecked(); + webViewZoomFactor() = ui()->zoomDoubleSpinBox->value(); + webViewKeepRunning() = ui()->keepRunningCheckBox->isChecked(); + } +#endif + return true; +} + +void WebViewOptionPage::reset() +{ +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) + if(hasBeenShown()) { + ui()->disableCheckBox->setChecked(webViewDisabled()); + ui()->zoomDoubleSpinBox->setValue(webViewZoomFactor()); + ui()->keepRunningCheckBox->setChecked(webViewKeepRunning()); + } +#endif +} + +SettingsDialog::SettingsDialog(Data::SyncthingConnection *connection, QWidget *parent) : + Dialogs::SettingsDialog(parent) +{ + // setup categories + QList categories; + Dialogs::OptionCategory *category; + + category = new OptionCategory(this); + category->setDisplayName(tr("Tray")); + category->assignPages(QList() + << new ConnectionOptionPage(connection) << new NotificationsOptionPage + << new LauncherOptionPage); + category->setIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); + categories << category; + + category = new OptionCategory(this); + category->setDisplayName(tr("Web view")); + category->assignPages(QList() << new WebViewOptionPage); + category->setIcon(QIcon::fromTheme(QStringLiteral("internet-web-browser"), QIcon(QStringLiteral(":/icons/hicolor/scalable/app/")))); + categories << category; + + categories << Settings::qtSettings().category(); + + categoryModel()->setCategories(categories); + + setMinimumSize(800, 450); + setWindowIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); + + // some settings could be applied without restarting the application, good idea? + //connect(this, &Dialogs::SettingsDialog::applied, bind(&Dialogs::QtSettings::apply, &Settings::qtSettings())); +} + +SettingsDialog::~SettingsDialog() +{} + +} diff --git a/gui/settingsdialog.h b/gui/settingsdialog.h new file mode 100644 index 0000000..dab83c4 --- /dev/null +++ b/gui/settingsdialog.h @@ -0,0 +1,51 @@ +#ifndef SETTINGS_DIALOG_H +#define SETTINGS_DIALOG_H + +#include +#include +#include + +#include + +namespace Settings { +class KnownFieldModel; +class TargetLevelModel; +} + +namespace Data { +class SyncthingConnection; +} + +namespace QtGui { + +BEGIN_DECLARE_UI_FILE_BASED_OPTION_PAGE_CUSTOM_CTOR(ConnectionOptionPage) +public: + ConnectionOptionPage(Data::SyncthingConnection *connection, QWidget *parentWidget = nullptr); +private: + DECLARE_SETUP_WIDGETS + void updateConnectionStatus(); + void applyAndReconnect(); + Data::SyncthingConnection *m_connection; +END_DECLARE_OPTION_PAGE + +DECLARE_UI_FILE_BASED_OPTION_PAGE(NotificationsOptionPage) + +DECLARE_UI_FILE_BASED_OPTION_PAGE(LauncherOptionPage) + +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) +DECLARE_UI_FILE_BASED_OPTION_PAGE(WebViewOptionPage) +#else +DECLARE_OPTION_PAGE(WebViewOptionPage) +#endif + +class SettingsDialog : public Dialogs::SettingsDialog +{ + Q_OBJECT +public: + explicit SettingsDialog(Data::SyncthingConnection *connection, QWidget *parent = nullptr); + ~SettingsDialog(); +}; + +} + +#endif // SETTINGS_DIALOG_H diff --git a/gui/tray.cpp b/gui/tray.cpp new file mode 100644 index 0000000..382566f --- /dev/null +++ b/gui/tray.cpp @@ -0,0 +1,455 @@ +#include "./tray.h" +#include "./settingsdialog.h" +#include "./webviewdialog.h" +#include "./dirbuttonsitemdelegate.h" + +#include "../application/settings.h" + +#include "resources/config.h" + +#include "ui_traywidget.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace ApplicationUtilities; +using namespace Dialogs; +using namespace Data; +using namespace std; + +namespace QtGui { + +/*! + * \brief Instantiates a new tray widget. + */ +TrayWidget::TrayWidget(TrayMenu *parent) : + QWidget(parent), + m_menu(parent), + m_ui(new Ui::TrayWidget), + m_settingsDlg(nullptr), + m_aboutDlg(nullptr), +#ifndef SYNCTHINGTRAY_NO_WEBVIEW + m_webViewDlg(nullptr), +#endif + m_dirModel(m_connection), + m_devModel(m_connection) +{ + m_ui->setupUi(this); + + // setup model and view + m_ui->dirsTreeView->setModel(&m_dirModel); + m_ui->devsTreeView->setModel(&m_devModel); + + // apply settings, this also establishes the connection to Syncthing + applySettings(); + + // setup sync-all button + auto *cornerFrame = new QFrame(this); + cornerFrame->setFrameStyle(QFrame::StyledPanel), cornerFrame->setFrameShadow(QFrame::Sunken); + auto *cornerFrameLayout = new QHBoxLayout(cornerFrame); + cornerFrameLayout->setSpacing(0), cornerFrameLayout->setMargin(0); + cornerFrameLayout->addStretch(); + cornerFrame->setLayout(cornerFrameLayout); + auto *viewIdButton = new QPushButton(cornerFrame); + viewIdButton->setToolTip(tr("View own device ID")); + viewIdButton->setIcon(QIcon::fromTheme(QStringLiteral("view-barcode"))); + viewIdButton->setFlat(true); + connect(viewIdButton, &QPushButton::clicked, this, &TrayWidget::showOwnDeviceId); + cornerFrameLayout->addWidget(viewIdButton); + auto *showLogButton = new QPushButton(cornerFrame); + showLogButton->setToolTip(tr("Show Syncthing log")); + showLogButton->setIcon(QIcon::fromTheme(QStringLiteral("text-plain"))); + showLogButton->setFlat(true); + connect(showLogButton, &QPushButton::clicked, this, &TrayWidget::showLog); + cornerFrameLayout->addWidget(showLogButton); + auto *scanAllButton = new QPushButton(cornerFrame); + scanAllButton->setToolTip(tr("Rescan all directories")); + scanAllButton->setIcon(QIcon::fromTheme(QStringLiteral("folder-sync"))); + scanAllButton->setFlat(true); + connect(scanAllButton, &QPushButton::clicked, &m_connection, &SyncthingConnection::rescanAllDirs); + cornerFrameLayout->addWidget(scanAllButton); + m_ui->tabWidget->setCornerWidget(cornerFrame, Qt::BottomRightCorner); + + // connect signals and slots + connect(m_ui->statusPushButton, &QPushButton::clicked, this, &TrayWidget::handleStatusButtonClicked); + connect(m_ui->closePushButton, &QPushButton::clicked, &QApplication::quit); + connect(m_ui->aboutPushButton, &QPushButton::clicked, this, &TrayWidget::showAboutDialog); + connect(m_ui->webUiPushButton, &QPushButton::clicked, this, &TrayWidget::showWebUi); + connect(m_ui->settingsPushButton, &QPushButton::clicked, this, &TrayWidget::showSettingsDialog); + connect(&m_connection, &SyncthingConnection::statusChanged, this, &TrayWidget::updateStatusButton); + connect(m_ui->dirsTreeView, &DirView::openDir, this, &TrayWidget::openDir); + connect(m_ui->dirsTreeView, &DirView::scanDir, this, &TrayWidget::scanDir); +} + +TrayWidget::~TrayWidget() +{} + +void TrayWidget::showSettingsDialog() +{ + if(!m_settingsDlg) { + m_settingsDlg = new SettingsDialog(&m_connection, this); + m_settingsDlg->setWindowTitle(tr("Settings - Syncthing tray")); + connect(m_settingsDlg, &SettingsDialog::applied, this, &TrayWidget::applySettings); +#ifndef SYNCTHINGTRAY_NO_WEBVIEW + if(m_webViewDlg) { + connect(m_settingsDlg, &SettingsDialog::applied, m_webViewDlg, &WebViewDialog::applySettings); + } +#endif + } + m_settingsDlg->show(); + centerWidget(m_settingsDlg); + if(m_menu) { + m_menu->close(); + } + m_settingsDlg->activateWindow(); +} + +void TrayWidget::showAboutDialog() +{ + if(!m_aboutDlg) { + m_aboutDlg = new AboutDialog(this, tr("Tray application for Syncthing"), QImage(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); + m_aboutDlg->setWindowTitle(tr("About - Syncthing Tray")); + m_aboutDlg->setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); + } + m_aboutDlg->show(); + centerWidget(m_aboutDlg); + if(m_menu) { + m_menu->close(); + } + m_aboutDlg->activateWindow(); +} + +void TrayWidget::showWebUi() +{ +#ifndef SYNCTHINGTRAY_NO_WEBVIEW + if(Settings::webViewDisabled()) { +#endif + QDesktopServices::openUrl(Settings::syncthingUrl()); +#ifndef SYNCTHINGTRAY_NO_WEBVIEW + } else { + if(!m_webViewDlg) { + m_webViewDlg = new WebViewDialog(this); + connect(m_webViewDlg, &WebViewDialog::destroyed, this, &TrayWidget::handleWebViewDeleted); + if(m_settingsDlg) { + connect(m_settingsDlg, &SettingsDialog::applied, m_webViewDlg, &WebViewDialog::applySettings); + } + } + m_webViewDlg->show(); + if(m_menu) { + m_menu->close(); + } + m_webViewDlg->activateWindow(); + } +#endif +} + +void TrayWidget::showOwnDeviceId() +{ + auto *dlg = new QDialog(this); + dlg->setWindowTitle(tr("Own device ID - Syncthing Tray")); + dlg->setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->setBackgroundRole(QPalette::Background); + auto *layout = new QVBoxLayout(dlg); + layout->setAlignment(Qt::AlignCenter); + auto *pixmapLabel = new QLabel(dlg); + pixmapLabel->setAlignment(Qt::AlignCenter); + layout->addWidget(pixmapLabel); + auto *textLabel = new QLabel(dlg); + textLabel->setText(m_connection.myId().isEmpty() ? tr("device ID is unknown") : m_connection.myId()); + QFont defaultFont = textLabel->font(); + defaultFont.setBold(true); + defaultFont.setPointSize(defaultFont.pointSize() + 2); + textLabel->setFont(defaultFont); + textLabel->setAlignment(Qt::AlignCenter); + layout->addWidget(textLabel); + auto *copyPushButton = new QPushButton(dlg); + copyPushButton->setText(tr("Copy to clipboard")); + connect(copyPushButton, &QPushButton::clicked, bind(&QClipboard::setText, QGuiApplication::clipboard(), m_connection.myId(), QClipboard::Clipboard)); + layout->addWidget(copyPushButton); + m_connection.requestQrCode(m_connection.myId(), bind(&QLabel::setPixmap, pixmapLabel, placeholders::_1)); + dlg->setLayout(layout); + dlg->show(); + centerWidget(dlg); + if(m_menu) { + m_menu->close(); + } + dlg->activateWindow(); +} + +void TrayWidget::showLog() +{ + auto *dlg = new QDialog(this); + dlg->setWindowTitle(tr("Log - Syncthing")); + dlg->setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); + dlg->setAttribute(Qt::WA_DeleteOnClose); + auto *layout = new QVBoxLayout(dlg); + layout->setAlignment(Qt::AlignCenter); + auto *browser = new QTextBrowser(dlg); + m_connection.requestLog([browser] (const std::vector &entries) { + for(const SyncthingLogEntry &entry : entries) { + browser->append(entry.when % QChar(':') % QChar(' ') % QChar('\n') % entry.message % QChar('\n')); + } + }); + layout->addWidget(browser); + dlg->setLayout(layout); + dlg->show(); + dlg->resize(600, 500); + centerWidget(dlg); + if(m_menu) { + m_menu->close(); + } + dlg->activateWindow(); +} + +void TrayWidget::updateStatusButton(SyncthingStatus status) +{ + switch(status) { + case SyncthingStatus::Disconnected: + m_ui->statusPushButton->setText(tr("Connect")); + m_ui->statusPushButton->setToolTip(tr("Not connected to Syncthing, click to connect")); + m_ui->statusPushButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); + break; + case SyncthingStatus::Default: + case SyncthingStatus::NotificationsAvailable: + case SyncthingStatus::Synchronizing: + m_ui->statusPushButton->setText(tr("Pause")); + m_ui->statusPushButton->setToolTip(tr("Syncthing is doing its job, click to pause")); + m_ui->statusPushButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause"))); + break; + case SyncthingStatus::Paused: + m_ui->statusPushButton->setText(tr("Continue")); + m_ui->statusPushButton->setToolTip(tr("Syncthing is suspended, click to continue")); + m_ui->statusPushButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); + break; + } +} + +void TrayWidget::applySettings() +{ + m_connection.setSyncthingUrl(Settings::syncthingUrl()); + m_connection.setApiKey(Settings::apiKey()); + if(Settings::authEnabled()) { + m_connection.setCredentials(Settings::userName(), Settings::password()); + } else { + m_connection.setCredentials(QString(), QString()); + } + m_connection.reconnect(); +} + +void TrayWidget::openDir(const QModelIndex &index) +{ + if(const SyncthingDir *dir = m_dirModel.dirInfo(index)) { + if(QDir(dir->path).exists()) { + DesktopUtils::openLocalFileOrDir(dir->path); + } else { + QMessageBox::warning(this, QCoreApplication::applicationName(), tr("The directly %1 does not exist on the local machine.").arg(dir->path)); + } + } +} + +void TrayWidget::scanDir(const QModelIndex &index) +{ + if(const SyncthingDir *dir = m_dirModel.dirInfo(index)) { + m_connection.rescan(dir->id); + } +} + +void TrayWidget::handleStatusButtonClicked() +{ + switch(m_connection.status()) { + case SyncthingStatus::Disconnected: + m_connection.connect(); + break; + case SyncthingStatus::Default: + case SyncthingStatus::NotificationsAvailable: + case SyncthingStatus::Synchronizing: + m_connection.pauseAllDevs(); + break; + case SyncthingStatus::Paused: + m_connection.resumeAllDevs(); + break; + } +} + +void TrayWidget::handleWebViewDeleted() +{ + m_webViewDlg = nullptr; +} + +TrayMenu::TrayMenu(QWidget *parent) : + QMenu(parent) +{ + auto *menuLayout = new QHBoxLayout; + menuLayout->setMargin(0), menuLayout->setSpacing(0); + menuLayout->addWidget(m_trayWidget = new TrayWidget(this)); + setLayout(menuLayout); + setPlatformMenu(nullptr); +} + +QSize TrayMenu::sizeHint() const +{ + return QSize(350, 300); +} + +/*! + * \brief Instantiates a new tray icon. + */ +TrayIcon::TrayIcon(QObject *parent) : + QSystemTrayIcon(parent), + m_size(QSize(128, 128)), + m_statusIconDisconnected(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-disconnected.svg")))), + m_statusIconDefault(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-default.svg")))), + m_statusIconNotify(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-notify.svg")))), + m_statusIconPause(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-pause.svg")))), + m_statusIconSync(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-sync.svg")))), + m_status(SyncthingStatus::Disconnected) +{ + // set context menu + connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("internet-web-browser")), tr("Web UI")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showWebUi); + connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("preferences-other")), tr("Settings")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showSettingsDialog); + connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("help-about")), tr("About")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showAboutDialog); + m_contextMenu.addSeparator(); + connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("window-close")), tr("Close")), &QAction::triggered, &QCoreApplication::quit); + setContextMenu(&m_contextMenu); + + // set initial status + updateStatusIconAndText(SyncthingStatus::Disconnected); + + // connect signals and slots + SyncthingConnection *connection = &(m_trayMenu.widget()->connection()); + connect(this, &TrayIcon::activated, this, &TrayIcon::handleActivated); + connect(connection, &SyncthingConnection::error, this, &TrayIcon::showSyncthingError); + connect(connection, &SyncthingConnection::newNotification, this, &TrayIcon::showSyncthingNotification); + connect(connection, &SyncthingConnection::statusChanged, this, &TrayIcon::updateStatusIconAndText); +} + +void TrayIcon::handleActivated(QSystemTrayIcon::ActivationReason reason) +{ + switch(reason) { + case QSystemTrayIcon::Context: + // can't catch that event on Plasma 5 anyways + break; + case QSystemTrayIcon::Trigger: + // either show web UI or context menu + if(false) { + m_trayMenu.widget()->showWebUi(); + } else { + // when showing the menu manually + // move the menu to the closest of the currently available screen + // this implies that the tray icon is located near the edge of the screen; otherwise this behavior makes no sense + const QPoint cursorPos(QCursor::pos()); + const QRect availableGeometry(QApplication::desktop()->availableGeometry(cursorPos)); + Qt::Alignment alignment = 0; + alignment |= (cursorPos.x() - availableGeometry.left() < availableGeometry.right() - cursorPos.x() ? Qt::AlignLeft : Qt::AlignRight); + alignment |= (cursorPos.y() - availableGeometry.top() < availableGeometry.bottom() - cursorPos.y() ? Qt::AlignTop : Qt::AlignBottom); + m_trayMenu.setGeometry( + QStyle::alignedRect( + Qt::LeftToRight, + alignment, + m_trayMenu.sizeHint(), + availableGeometry + ) + ); + m_trayMenu.show(); + } + break; + default: + ; + } +} + +void TrayIcon::showSyncthingError(const QString &errorMsg) +{ + if(Settings::notifyOnErrors()) { + showMessage(tr("Syncthing error"), errorMsg, QSystemTrayIcon::Critical); + } +} + +void TrayIcon::showSyncthingNotification(const QString &message) +{ + if(Settings::showSyncthingNotifications()) { + showMessage(tr("Syncthing notification"), message, QSystemTrayIcon::Information); + } +} + +void TrayIcon::updateStatusIconAndText(SyncthingStatus status) +{ + switch(status) { + case SyncthingStatus::Disconnected: + setIcon(m_statusIconDisconnected); + setToolTip(tr("Not connected to Syncthing")); + if(Settings::notifyOnDisconnect()) { + showMessage(QCoreApplication::applicationName(), tr("Disconnected from Syncthing"), QSystemTrayIcon::Warning); + } + break; + case SyncthingStatus::Default: + setIcon(m_statusIconDefault); + setToolTip(tr("Syncthing is running")); + break; + case SyncthingStatus::NotificationsAvailable: + setIcon(m_statusIconNotify); + setToolTip(tr("Notifications available")); + break; + case SyncthingStatus::Paused: + setIcon(m_statusIconPause); + setToolTip(tr("Syncthing has been suspended")); + break; + case SyncthingStatus::Synchronizing: + setIcon(m_statusIconSync); + setToolTip(tr("Synchronization is ongoing")); + break; + } + switch(status) { + case SyncthingStatus::Disconnected: + case SyncthingStatus::Synchronizing: + break; + default: + if(m_status == SyncthingStatus::Synchronizing && Settings::notifyOnSyncComplete()) { + showMessage(QCoreApplication::applicationName(), tr("Synchronization complete"), QSystemTrayIcon::Information); + } + } + + m_status = status; +} + +/*! + * \brief Renders an SVG image to a QPixmap. + * \remarks If instantiating QIcon directly from SVG image the icon is not displayed under Plasma 5. It would work + * with Tint2, tough. + */ +QPixmap TrayIcon::renderSvgImage(const QString &path) +{ + QSvgRenderer renderer(path); + QPixmap pm(m_size); + pm.fill(QColor(Qt::transparent)); + QPainter painter(&pm); + renderer.render(&painter, pm.rect()); + return pm; +} + +} diff --git a/gui/tray.h b/gui/tray.h new file mode 100644 index 0000000..bd19311 --- /dev/null +++ b/gui/tray.h @@ -0,0 +1,125 @@ +#ifndef TRAY_H +#define TRAY_H + +#include "./webviewprovider.h" +#include "../data/syncthingconnection.h" +#include "../data/syncthingdirectorymodel.h" +#include "../data/syncthingdevicemodel.h" + +#include +#include +#include + +#include + +QT_FORWARD_DECLARE_CLASS(QString) +QT_FORWARD_DECLARE_CLASS(QPixmap) + +namespace ApplicationUtilities { +class QtConfigArguments; +} +namespace Dialogs { +class AboutDialog; +} + +namespace QtGui { + +class WebViewDialog; +class SettingsDialog; +class TrayMenu; +namespace Ui { +class TrayWidget; +} + +class TrayWidget : public QWidget +{ + Q_OBJECT + +public: + TrayWidget(TrayMenu *parent = nullptr); + ~TrayWidget(); + + Data::SyncthingConnection &connection(); + +public slots: + void showSettingsDialog(); + void showAboutDialog(); + void showWebUi(); + void showOwnDeviceId(); + void showLog(); + +private slots: + void updateStatusButton(Data::SyncthingStatus status); + void applySettings(); + void openDir(const QModelIndex &index); + void scanDir(const QModelIndex &index); + void handleStatusButtonClicked(); + void handleWebViewDeleted(); + +private: + TrayMenu *m_menu; + std::unique_ptr m_ui; + SettingsDialog *m_settingsDlg; + Dialogs::AboutDialog *m_aboutDlg; +#ifndef SYNCTHINGTRAY_NO_WEBVIEW + WebViewDialog *m_webViewDlg; +#endif + Data::SyncthingConnection m_connection; + Data::SyncthingDirectoryModel m_dirModel; + Data::SyncthingDeviceModel m_devModel; +}; + +inline Data::SyncthingConnection &TrayWidget::connection() +{ + return m_connection; +} + +class TrayMenu : public QMenu +{ + Q_OBJECT + +public: + TrayMenu(QWidget *parent = nullptr); + + QSize sizeHint() const; + TrayWidget *widget(); + +private: + TrayWidget *m_trayWidget; +}; + +inline TrayWidget *TrayMenu::widget() +{ + return m_trayWidget; +} + +class TrayIcon : public QSystemTrayIcon +{ + Q_OBJECT + +public: + TrayIcon(QObject *parent = nullptr); + +private slots: + void handleActivated(QSystemTrayIcon::ActivationReason reason); + void showSyncthingError(const QString &errorMsg); + void showSyncthingNotification(const QString &message); + void updateStatusIconAndText(Data::SyncthingStatus status); + +private: + QPixmap renderSvgImage(const QString &path); + + const QSize m_size; + const QIcon m_statusIconDisconnected; + const QIcon m_statusIconDefault; + const QIcon m_statusIconNotify; + const QIcon m_statusIconPause; + const QIcon m_statusIconSync; + TrayMenu m_trayMenu; + QMenu m_contextMenu; + Data::SyncthingStatus m_status; +}; + +} + +#endif // TRAY_H diff --git a/gui/traywidget.ui b/gui/traywidget.ui new file mode 100644 index 0000000..bbbf1f7 --- /dev/null +++ b/gui/traywidget.ui @@ -0,0 +1,248 @@ + + + QtGui::TrayWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 2 + + + 4 + + + 4 + + + 4 + + + 4 + + + + + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Close + + + + .. + + + true + + + + + + + About + + + + .. + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Connect + + + + .. + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Settings + + + + .. + + + true + + + + + + + Web UI + + + + .. + + + true + + + + + + + + + + QTabWidget::South + + + QTabWidget::Triangular + + + 0 + + + true + + + + + .. + + + Directories + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + .. + + + Devices + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + QtGui::DirView + QTreeView +
./gui/dirview.h
+
+ + QtGui::DevView + QTreeView +
./gui/devview.h
+
+
+ + +
diff --git a/gui/webpage.cpp b/gui/webpage.cpp new file mode 100644 index 0000000..403bf00 --- /dev/null +++ b/gui/webpage.cpp @@ -0,0 +1,88 @@ +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) +#include "./webpage.h" + +#include "../application/settings.h" +#include "../data/syncthingconnection.h" + +#include "resources/config.h" + +#include +#include +#include +#include +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) +# include +# include +#elif defined(SYNCTHINGTRAY_USE_WEBKIT) +# include +# include +# include +#endif + +namespace QtGui { + +WebPage::WebPage(WEB_VIEW_PROVIDER *view) : + WEB_PAGE_PROVIDER(view), + m_view(view) +{ +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) + settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); + connect(this, &WebPage::authenticationRequired, this, static_cast(&WebPage::supplyCredentials)); +#else + settings()->setAttribute(QWebSettings::JavascriptCanOpenWindows, true); + setNetworkAccessManager(&Data::networkAccessManager()); + connect(&Data::networkAccessManager(), &QNetworkAccessManager::authenticationRequired, this, static_cast(&WebPage::supplyCredentials)); +#endif + if(!m_view) { + // delegate to external browser if no view is assigned +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) + connect(this, &WebPage::urlChanged, this, &WebPage::delegateToExternalBrowser); +#else + connect(this->mainFrame(), &QWebFrame::urlChanged, this, &WebPage::delegateToExternalBrowser); +#endif + m_view = new WEB_VIEW_PROVIDER; + m_view->setPage(this); + } +} + +WEB_PAGE_PROVIDER *WebPage::createWindow(WEB_PAGE_PROVIDER::WebWindowType type) +{ + return new WebPage; +} + +void WebPage::delegateToExternalBrowser(const QUrl &url) +{ + openUrlExternal(url); + // this page and the associated view are useless + m_view->deleteLater(); + deleteLater(); +} + +void WebPage::supplyCredentials(const QUrl &requestUrl, QAuthenticator *authenticator) +{ + Q_UNUSED(requestUrl) + supplyCredentials(authenticator); +} + +void WebPage::supplyCredentials(QNetworkReply *reply, QAuthenticator *authenticator) +{ + Q_UNUSED(reply) + supplyCredentials(authenticator); +} + +void WebPage::supplyCredentials(QAuthenticator *authenticator) +{ + if(Settings::authEnabled()) { + authenticator->setUser(Settings::userName()); + authenticator->setPassword(Settings::password()); + } +} + +void WebPage::openUrlExternal(const QUrl &url) +{ + QDesktopServices::openUrl(url); +} + +} + +#endif // defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) diff --git a/gui/webpage.h b/gui/webpage.h new file mode 100644 index 0000000..a1cbaf5 --- /dev/null +++ b/gui/webpage.h @@ -0,0 +1,44 @@ +#ifndef WEBPAGE_H +#define WEBPAGE_H +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) + +#include "./webviewprovider.h" + +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) +# include +#elif defined(SYNCTHINGTRAY_USE_WEBKIT) +# include +#endif + +QT_FORWARD_DECLARE_CLASS(WEB_VIEW_PROVIDER) +QT_FORWARD_DECLARE_CLASS(QAuthenticator) +QT_FORWARD_DECLARE_CLASS(QNetworkReply) + +namespace QtGui { + +class WebPage : public WEB_PAGE_PROVIDER +{ + Q_OBJECT +public: + WebPage(WEB_VIEW_PROVIDER *view = nullptr); + +public slots: + void openUrlExternal(const QUrl &url); + +protected: + WEB_PAGE_PROVIDER *createWindow(WebWindowType type); + +private slots: + void delegateToExternalBrowser(const QUrl &url); + void supplyCredentials(const QUrl &requestUrl, QAuthenticator *authenticator); + void supplyCredentials(QNetworkReply *reply, QAuthenticator *authenticator); + void supplyCredentials(QAuthenticator *authenticator); + +private: + WEB_VIEW_PROVIDER *m_view; +}; + +} + +#endif // defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) +#endif // WEBPAGE_H diff --git a/gui/webviewdialog.cpp b/gui/webviewdialog.cpp new file mode 100644 index 0000000..21ac7df --- /dev/null +++ b/gui/webviewdialog.cpp @@ -0,0 +1,63 @@ +#ifndef SYNCTHINGTRAY_NO_WEBVIEW +#include "./webviewdialog.h" +#include "./webpage.h" + +#include "../application/settings.h" +#include "../data/syncthingconnection.h" + +#include + +#include +#include +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) +# include +#elif defined(SYNCTHINGTRAY_USE_WEBKIT) +# include +#endif + +using namespace Dialogs; + +namespace QtGui { + +WebViewDialog::WebViewDialog(QWidget *parent) : + QMainWindow(parent), + m_view(new WEB_VIEW_PROVIDER(this)) +{ + setWindowTitle(tr("Syncthing")); + setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); + setCentralWidget(m_view); + + m_view->setPage(new WebPage(m_view)); + + applySettings(); + + if(Settings::webViewGeometry().isEmpty()) { + resize(1200, 800); + centerWidget(this); + } else { + restoreGeometry(Settings::webViewGeometry()); + } +} + +QtGui::WebViewDialog::~WebViewDialog() +{ + Settings::webViewGeometry() = saveGeometry(); +} + +void QtGui::WebViewDialog::applySettings() +{ + m_view->setUrl(Settings::syncthingUrl()); + m_view->setZoomFactor(Settings::webViewZoomFactor()); +} + +void QtGui::WebViewDialog::closeEvent(QCloseEvent *event) +{ + if(!Settings::webViewKeepRunning()) { + deleteLater(); + } + event->accept(); +} + +} + +#endif diff --git a/gui/webviewdialog.h b/gui/webviewdialog.h new file mode 100644 index 0000000..54bc1f7 --- /dev/null +++ b/gui/webviewdialog.h @@ -0,0 +1,33 @@ +#ifndef WEBVIEW_DIALOG_H +#define WEBVIEW_DIALOG_H +#ifndef SYNCTHINGTRAY_NO_WEBVIEW + +#include "./webviewprovider.h" + +#include + +QT_FORWARD_DECLARE_CLASS(WEB_VIEW_PROVIDER) + +namespace QtGui { + +class WebViewDialog : public QMainWindow +{ + Q_OBJECT +public: + WebViewDialog(QWidget *parent = nullptr); + ~WebViewDialog(); + +public slots: + void applySettings(); + +protected: + void closeEvent(QCloseEvent *event); + +private: + WEB_VIEW_PROVIDER *m_view; +}; + +} + +#endif +#endif // WEBVIEW_DIALOG_H diff --git a/gui/webviewoptionpage.ui b/gui/webviewoptionpage.ui new file mode 100644 index 0000000..adfef0c --- /dev/null +++ b/gui/webviewoptionpage.ui @@ -0,0 +1,72 @@ + + + QtGui::WebViewOptionPage + + + + 0 + 0 + 356 + 98 + + + + General + + + + + + Usage + + + + + + + Disable web view (open regular web browser instead) + + + + + + + Zoom factor + + + + + + + QAbstractSpinBox::PlusMinus + + + 5.990000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + + + + + Hiding + + + + + + + Keep web view running when currently not shown + + + + + + + + diff --git a/gui/webviewprovider.h b/gui/webviewprovider.h new file mode 100644 index 0000000..1ce8b12 --- /dev/null +++ b/gui/webviewprovider.h @@ -0,0 +1,11 @@ +#ifndef WEB_VIEW_PROVIDER + +#if defined(SYNCTHINGTRAY_USE_WEBENGINE) +# define WEB_VIEW_PROVIDER QWebEngineView +# define WEB_PAGE_PROVIDER QWebEnginePage +#elif defined(SYNCTHINGTRAY_USE_WEBKIT) +# define WEB_VIEW_PROVIDER QWebView +# define WEB_PAGE_PROVIDER QWebPage +#endif + +#endif diff --git a/resources/icons.qrc b/resources/icons.qrc new file mode 100644 index 0000000..e0ed680 --- /dev/null +++ b/resources/icons.qrc @@ -0,0 +1,12 @@ + + + icons/hicolor/scalable/app/syncthingtray.svg + icons/hicolor/scalable/status/syncthing-default.svg + icons/hicolor/scalable/status/syncthing-notify.svg + icons/hicolor/scalable/status/syncthing-pause.svg + icons/hicolor/scalable/status/syncthing-sync.svg + icons/hicolor/scalable/status/syncthing-disconnected.svg + icons/hicolor/scalable/status/syncthing-ok.svg + icons/hicolor/scalable/status/syncthing-error.svg + + diff --git a/resources/icons/hicolor/scalable/app/syncthingtray.svg b/resources/icons/hicolor/scalable/app/syncthingtray.svg new file mode 100644 index 0000000..ff643fd --- /dev/null +++ b/resources/icons/hicolor/scalable/app/syncthingtray.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/hicolor/scalable/status/syncthing-default.svg b/resources/icons/hicolor/scalable/status/syncthing-default.svg new file mode 100644 index 0000000..b281649 --- /dev/null +++ b/resources/icons/hicolor/scalable/status/syncthing-default.svg @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/hicolor/scalable/status/syncthing-disconnected.svg b/resources/icons/hicolor/scalable/status/syncthing-disconnected.svg new file mode 100644 index 0000000..3eb9dc1 --- /dev/null +++ b/resources/icons/hicolor/scalable/status/syncthing-disconnected.svg @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/resources/icons/hicolor/scalable/status/syncthing-error.svg b/resources/icons/hicolor/scalable/status/syncthing-error.svg new file mode 100644 index 0000000..57baddb --- /dev/null +++ b/resources/icons/hicolor/scalable/status/syncthing-error.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/hicolor/scalable/status/syncthing-notify.svg b/resources/icons/hicolor/scalable/status/syncthing-notify.svg new file mode 100644 index 0000000..84998a0 --- /dev/null +++ b/resources/icons/hicolor/scalable/status/syncthing-notify.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/hicolor/scalable/status/syncthing-ok.svg b/resources/icons/hicolor/scalable/status/syncthing-ok.svg new file mode 100644 index 0000000..464a83e --- /dev/null +++ b/resources/icons/hicolor/scalable/status/syncthing-ok.svg @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/resources/icons/hicolor/scalable/status/syncthing-pause.svg b/resources/icons/hicolor/scalable/status/syncthing-pause.svg new file mode 100644 index 0000000..f2589e2 --- /dev/null +++ b/resources/icons/hicolor/scalable/status/syncthing-pause.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/hicolor/scalable/status/syncthing-sync.svg b/resources/icons/hicolor/scalable/status/syncthing-sync.svg new file mode 100644 index 0000000..9ec5af1 --- /dev/null +++ b/resources/icons/hicolor/scalable/status/syncthing-sync.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/screenshots/1.png b/resources/screenshots/1.png new file mode 100644 index 0000000000000000000000000000000000000000..5ad0025b7a3e202bdb608a82552ddff463c19697 GIT binary patch literal 118425 zcmV)9K*hg_P)S>00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4kiEq4gUe~Q|0pj000McNliru;0PENBNi>2m|*|_AOJ~3 zK~#9!e7tF_Y}s`l_I+#ZbEK^Cz1RAtwfTXUKaeSq2(vGVBkWo^LFnFa`Z}(^GJVNbNwS53fLmgXlcQ!w!^NX||3#d> zgHK-bt5=7F@@Rkbg4U~H_jst!lt4#Dhzcv+KjoI)<-Pf$bFIao3m$k@5iLO_01AtMH};EdE=t(93|C`hSE%4 z=z}>%22PIhXxCcVZrxwd<|x`!T0L1mYX&ta$M8`?YnP>+M~uz(ZX?Y&IO}#l?Wa8| zYyyU$K@$dG3JD~YR1Gr+0GA|70x&j`Bn1$Z(rB56k$ zYkMWGVm#XofAZ3g_wCm<+ei0Qv)JUcyL+BZ6Oa$D?zyI~o^2nm_GTg2d8}pr!s+?j zU%YVC?Y7)|xPABA@aosb&wTzEb=+;o^Ng3@b^h`1y}WqrKlkfr?GrhEN4x%MKl{I5 zK7M$**VkWu_^O{B)qN=J_}>2BgK0%vT>8bc<2NrhkKY)!(pIXNx&Pxs~RZSx>kzFIx1yX*FJ$9Of~+(&&7C9LgS!!)jKsHWHRd^cV{ z*lr%?(X(Oq`o3b71<0%f8N*;~md!0=UyKL~nK7g=nm{=%kcyHyF;48`_wbho9lhTE z$rXO}-N)a5ef_-^Ut5=>>-K9^ubuGyJOAbL_K(Lh$;vUy;Nw{NW<1&1L>6ldRuZ6v zCo4>ngJd=V06?0Q0E!U`z={R|sU-GEnpVS55H&LCgW0fEzCHb4&7%hU7kKi{^1_GO z@W=4N6|Vm;Jo*A@znH=ww@5r@Qyq8-dneyyNa)k z%?9bBkmT56EJu=srA0bM8VQ(LX)C9Zt;R(T!D{QYgwkWs%CX8!_dE-G8`eNxLVyh@ z0}7xCX}}q94Z9#Eot&O-Hb-r@-PTrWk+r~m&*OoQ?ysiL>d9yQ{8VeplzC(Hl~lsa zhhY%S-O>vpJ=-KQ+BUZ>odX7gVsNu>sAadGE{5r$%8C4q{rR)OcLmqQ)B?x|MP#9l zax73~oJ1*^myTx!F-D?FB{4w|979l0yCdJAD5#Z&DAy5Btg)mVZJS3Y<=B-@US<_VGp7n7iwAxUEaT59DMKD5?6TW0QzO1%(#fa9?8dZ{PPcX@qX zs^>T+M;mvx+fti)e1v-!kszuKBzhK_l9QlZ#Av;8GwXFzjV8_a4AoZ^$G{I`Al{nz*Z_yPXRgO|ST zwXc8ah!1zGU#RUY;{Uw%Ý%Za>YijHZ&udRV36M&I|2ns?k_>dKl%-WAFNCQAY zf+!{<<-RibFpTSBcCAp&VW3`4l(G$9hC zK}e>Pgo0p#bTdg706>?dAbU-ddXdLm;skMiX#K1G!&GDmWd9xz03ABNzIxk(hbfCd zNx2!VrGb4lrlKL04K5O8FcB`x@PL}Z3?3Hi#3ofihqa7r=mFQ#b;6V+2?#>A5n}qtOq#2`m4V+aKVnl;b>pIRA}qoJjR8mn`l z*h;o~t(}KTQikGiaYqniI>LgjWJ^+(U|EeD8w+ZyyM_Qm$*3>^B4q+>uo?JB0OWnC zDL_)P0ANOkOLWMgmM{dOz(5kn7N;ktoAGqp_S@}Ko$q`=xtGNXzObI&EVi$MwXm_& z%dos!pB!DsE92!0aq_|ivhmsR^R`+WPo~Q}OZ1`pL=tvQuT@*@KH0?_ssZ^*PTkj~>5%=gFJe)^%pE zG{MYSAc}xx9Z>{Xas?!uD9Rc&$AV$wP*Ix{mh6YXixmh=DM6BhmrPY=w8o5HOMoOP zCC#J|K)TF`0ti3}Ts(VLkK5#T$CGiti*@XX9LMn_uqrP;*F)5SGp-(8 z{gdO(gZ1`(Q7`VTKk#IKzQq@gR)1=jTi}0RsZE0`VkoQA@p#%+p*f1o!~_xv8u}JO zKawCxEkz;$qbqaI1^`4!rxM84S}|A|0Hnpx@^S9I2z{F$zbB5q*v`L<%@HpD7|#9# ze0TB6SI6zJ*z*L=bfWh$Cp;eFScoaT_4A^ zYx2>tLff^=r_Y{UP8Y4^2ukomgd`M#0I4Y@foOos3NRs>gEc0zqR5aSISEiCfC40K z>-Cj<_AVSf#NCHzU-rw=JZxngHs;sZ0oOTZWbnrDPrGK~%@ zjiZf&rG8LdXue8 zo(2U#vtpowNKSJvQOD6lr%X}+^tpTY5=IFGjn>47y#vVrMF4~kFy>AZ?wY<)ad#SC zE%64#LZl=l)6z%)k_buwB{Curd-q{1WvWdn7*^pzUTN!~21gc0CSb6EHpKviVGtx$ zl|3-;2Xg>q0F(qlQaD8+g_;RuOE!1xuh8~o7;uETpNL5*Tb-Z=curVACPJA6zztq< zmSz_up!ZI@YxA>+i-;Wk>Y}|#fN&ZG0&L32n8DE8p8M0DA<_Kr84}HlFn`bT%?^ns z6J#W$fPaiPexJF`5{u5GEo6$FON=P0e5^W5m7~#?@F>LqhAa2J!6j zb%cQA^0rMG(cFswrQl2u1_u&0e;0vX!vK;<^#|0S*x`eRZ`N~oXnAz4D-i@l6`(9B@p?ZmmT0Vv_weaQ-W#(THSH;GXSBJVU9lhD=xkPVD#+*H?K70}K=; zh>lXS3>KlR+MI#KFCHU9R#+Im0^qUptkCk5dTHs$g8*%QlL;6*&I1Ye8Eeu=A_P}b zrP#{KRHtSEqsctXjg*L}!@90cpjfr5{wLhcs9npwX!EP5U`)`!K_qQJBcnMDJPx7i zazvU*X2f8$6r`Ybf|RqqGxs3t&(o19DMh8Ej6=ujC5Do$pI0vBMg;rU`eIv(qztw^ z5Tij*Y8cAVW}`sOw%_ljy0^j1sysCr!iaFefGmkrW+{=L&cFsV*xn+BS`GUsEOFkCU)20^zXEsfd++XSjn99T&mL^_?c4EJ20lG?EA*$Qr`27%hGD?! zC`U(|czIbau2$2&P?iGG$OMfbp#&kzOcX=*y`7+d0d>u!4f|3ek(pAmLyP{1L=XlL zDF_jf2&lsk07T||R51T32r!?|RAy%^Py#-dkx4>@R?Np>v4T{qQ)9r)T2|zawNeCv zTBRMwP_12c2$X4H0t5_9LQ3^c&tCZEQ!4^WlZz5es!$p6!O=cQX3fu6T^-v8grmr2 zrS03-2&~Guk5@xqF{?6n*vQgUr&i~~Z@OJ9Zhyw*zjde}lGu+qkaLqW%rYfGiXkOq zlt099gy9h;5-(}Ii}4hGf&CYGz8_XA3@L$}iYivC;lc56EX9=j zG$ssGM)V>Q$b_UB29LC(OruZ86>O8fi@F`GLKCuYioPO*hI9ytbeV&Js8o_fMxu}+ zLOP(MHLS=aPpb|zV8=8Pgp@{qaec;tU)Oo{7}9ty3ro&0Xl`}$Rwcn+>8uq2^q0yHxiOO^zUiHKaEVq9at zT_GLJtTjUz}U$@=< z!QB^{;_>6JvV{4t^6_XWjOnWKazCXi3F9E9GL2?FwAMQEBI$Iak5=3Z-2>576&Vqd zhWn+CwLWbQH(2bl(8^FG1L14i?ymORE7iBWO9JSiHI>UT=dGfxj*cXl4&Vs-L#XKg z@8dbJ#9oC(@THetGB4}Z(X`)w{q@%Y6~fqSA*`!VQbPUE&0q}b7*!Bp2xXE*Botu6 z1R31GwCGE9z;hsyosI_pm;oXhOft!qorS#pgyz!Si&;mAuuiv;6exA-2z7r+2?LN~ zz?30jH4dIBK_KNyaR3OjOj9T&7!aapbR1A%FQvwqMLDGCWWrnlMT6JSC=jBMnMtT) zxcRv!Ws-=w9!dShN|=EvX{6{v@0Wmb5&rx4&&M5IUgi9cB)U5U7@c0s%-jq@o$6Rd zw7j~$q}xDi&XiaO?IahKaHFy)t58v3R*|5Ja~ruz9)%~lE3FbOt%l6BDx#rIQ1Yl~ zN~N$`;qC@km$+DAcn79O&{c6W`tIap8dsPmm4WM38nNHo#l>)WIkvh=0i~6h0=NKy zNt^MbW~EpHWk3pPN^&h_WP-hnX^}>=cY1e9s7OR+-#1I2N4L;sBp{FkMFH7`l-^DO zf`-f~rcwsd8&X**4V5l8CIE%hJ48P-4uT;yz(5lt6V8-0vtO8y3m^e<5qA#WWHFTX_TOP{o{B63CDQ8qEfkt9dK1O>9<&5Qtrl*`n+L$`Q`fX~j#(p9PSTS3d z(>k2Wk6Sjz-i-U=`(mDLohmKa*F9_G9@@*YAJs+r79=G3Jc>JjynguTWb~)oxnD|7 zvLm|%=*_D>d7ei-!+47FH0{EZ$SKWKpxUsPiqd-(jA`HY0jG%uC+DS1dS^N|Q$|8= zuwSacx<(Mt$CU2+TEFM(ax|!qgOva4*PK69)=0CXQ0d+!q=#)~M^j zg)QW;YayHtnvhV=1x`1r&mVz@@D?g_qkk;esDI(6NA@a0H+3U&R%4h*q?Bl02=+H>G@23aaAR?L3aOB#n4nCSEpy#SutAc{M zK)d-C0}N22Lxr5-(jwzY1hT*r0uV-I4se4BAzK!x_np4$I}z{emua)D2goo7gQWQ} z0CIIPOB8ID9Q*o#2`|uFA>I_I2A+=iFlL2EFM=cFzV%Yral4=&K?w47@s91it zB!kKN=}E#Cu+i(-+`B|}3nUHLP57*2D}C$A-m}loU)L|34DZV}24Q%qC_K0(|JMv?cOVuOR z5efK9IL?gMJSvCr=!fFfL^HzGK~b99EJPZhWS(PxfpQP4W0Y;~CPC=MCzUOj#YzTKF|Uav zG1LJEg-p(e7D^xk1dymgsy9#2&^8_$&|50q3h(4oTv zncaQ9eS+rC#HGqCn`u!(`=eaTWy8)5up%@Z)V-Tk&Vz8k!|$5^j+mTTx{Jh&$vE?A zg@Pi?4VDVZ44#y_FnrkaT&!drv?<**T;9TjS|D|ol*))=9jX_|Sm;nRC;^zQO(Rq@ zCogN7m_ZmY0jAs&NgIu9DZ?y|i+@PUz1{cwp4a?3dYuPh3%%A2K3Mjd=GYqr(o&Hf zLq~F05k)U{`8^Bw%^?(hG|n#hXA9jF<%@Vn#{J>tKZd+(!XggYOK?EErcDgwkveakc_e zGS6WJ3>G^Gxw(Uk;kXD8i)=_AEgNI%Mo8ydoq?!VGcJ5@&W#AYW{O0LU3=*gly%0B} z$`A&c9A=S-Y?hS^knFh*bP;YAfFvU`m&(mWLzPIr{^;?n33q_19=4v*rGU&-hBD_S z4s*A;ia=9_nb92vOInKRj7k79(d1$2juLHFEy#v2^HJED6gq3=F3^}7Y=(IanRH_c zQvlY+lFzPi-n=~UI@R*Vk%Oyodv+ESXgRJx$Mto&d^TKN9rctLOolX+WC|(vCNit= zH1AMV%D$L@b=eld1zp~vl|9Vhgfg-%^obymJk(uiQAlofF%KkR7V7BGp>z^Z#3U;P zL-8UqvsuX4RRG%J?siZvW6d^Z8Ng~#@6jfqXFTO|A&_pxdit^JC%*ST&2PyP*HivB z=o`L&N$s93YWOh6FQ*C@5)A zWM|njV62cqJ%BUkW|OYYKU>lOLmINs6JD8JC9*~3$*w{hM48RLI~V!6OB{2b?EBu> zq3jG&e9_ZLG5aGHfmF84?1Z3K9W$uMhq^!AVYN(fv-zxc*mP(soN=qonwQWBM%>>lkh6BhrDrGNmi~c$*!thP zrPtoh>q6EAsaZDb(+Wvgmvjkc0-%=yYq1ApT>Csevnza&R++uAi}Yv{kX`da3dY%O z#f_YK8xQGLj1^ zP%Kl%m@A?Iot01qkxpY~r~;(9C}j^~_73mL9@a|hsW9QjKsc0+S*l&O@PRJp-GB)>*H)_w&)hdsodcZI)^_GsFj z1GLf1tYvn6*ws9)$7yqJ_x9yJ*B88i8c3LsDOo2D3$j6GKiRhxlltU=83|& z&o0~=){()CS^-C?7CUIlJjQs2&Ccr1%d}<7S2krbNQbWQGD(*qrV)v;LNo zU8W{84f7Z)(d~>pSdYE_NMa_PW=tiZvSbw!U7TNJTcZ0OmK}KAN(jur9icfVe8}%f zkTQEuJY+(Q7=WgX%rRw)zd8R{9t0;oXD8{lhS_-y*vD;*vgA!(inIA`NF-7=r{Z9# zOwGB}88t{u<3WjG#~qwwn=@1~?=?YMEJd=%L8%b?0|U&>O4dfLoz`rJR*N5J+XqUE zph>V64Fp*dQ;%1ta()#{gjc}rF^HQ?Tz_F?Ka)z$rue)C59CZ;`RQeg-K@!k4@wo< z=)P|aEW^0o?XHX4RC_p%DOpE*vx`BHDHJb58Tb3WGOd`ILq;R%g)VY98h`XZ`b+PB z|NDOTcYpV|6%)V+YRMi+fA_p@Zk@C z_+uaY_}gE4+bdsrjZ?sJmTbH3$VfFaQ^FEPq85D1Ee2nbwAh6Gb49SyKq{UHPm64_D+v_8x;01Y`0 zlrv|t&pFovlR{ilMCzc-6V^ysPCzqD9pY1m*txhw9F}wwyO*D{f8K)!nuQY~NE)ou zf~uAogAarf4L)Pk4oazznLt}~ot|T1_S66qX`EFOWDig%+LCxus+UeMo5`0#$jLwWW>iMcIuG6yTh_8W8tUNJw2g|vh(hc^gwc3;ZKid6 z=$gC0tE<86!QG3^CR<`P!W@sDj8EU(OndKi*O&#Zu>?x*WO-vT5Ge<9RwpTPHk{^6 zd4K`Aa)1XIop70(8d^$7OM?pxn$2U&VVF17V*gaJN<#nuAOJ~3K~&BZlsA+E9l>`< zvY0Tu6>iI{bt;yn6s|_rKq7fimdq3>ktt&Z3cUm>6UDmL2P7QX`YIR`!MW60kA%#& zDHi^NP@k;#)YVSExn>#g9^*H*{KACaU6pTRI}_ygAo2|pSHpX^4WTbp1Y}Mx=2K)c zXOFeJFFm}hgGF#-D`K8lJ8&;a=t{Qcnsp zGr15M0g&||LCP2;QkmHsRF5}_*3q^MP(?ayoEc719%x-XKy-tvNir0}0IPG!BO#-k z%P0%81Ujw>*PO|e(hy9O+3b&Xxl{&3rprra8nUO&64m=vAba=AE=Q{e zmzV(}axtz6BIlf*(8*h`n{&2ElqE*Ul5O!j`*%q_)l$M6^Cm|xm_au{wucUTyCx{7 zO7wsQ9Td`#0uwpP)3BE$GX$_o0OnG3v&v?F5I5M49L?oBewvlV`y@esWmSdDP)wPw_aa;@H;mD%D*aTzD2R``&f9@lH|L6aqixJa)x9zjkAO6sXfBL6>>W$YQefUEk z`tcwCvA_5q{0PWTeBu)TKmF-{fZ65+&2;e5H3vhDw=DHTsXl>&EFLQl^^`}5B{k?^Pz9}rgs8(^>bhS)qnbHzw=w4?flUp zs%lP=&hl(ULeo6yEEob5>GB2%YGS|Kr-DK2PDha`Jw0M^rf~!N7K(oh+${ic)?IEU zY^S^%%#v~NINZe7e&-N+kcPv@AMG9tpg46Ai! zdP9vCwG}G^KyZSBoLCG=#R#NIi0)P_)d%N@UKDx@*bC{|NF9- zhRj!d7x?bI{$SsJ|7iHYRPRt$n4wp*zO?rD?J^Uugc^u~k{0t|(Sm?-W*gKzz48_n z`M^7|(ByR3EQH7x`ks&j+9QcN%1%~ietbXZPg|Vqc$97pBljy~B$N5n@n+j9wHhKE zJLHAx-B{iA(`#NBLqJ6IF^?{c_5xrq1I-QHB!-)rJIy-(T<)Es8BD2E^od4E2$4Jp z4Pi?5oLxTkJZp9YKilivVh@fo&)g%ID|3oN=~$`IXl8<3%$_yv0xqB+(lRqq(Wg|5|=TKn;K%ht}jz^1}c%&aN>3+0mHT6na9!vF+e-_t`{0P zmJfXA*+)MKv<0&b-|!=!*t^NTX&CmN)Z6soANscM|G|IbZ~fPQ=h5S*4ghKd;FN669raqKj_wVID^dWH zQE7x_pG`0*oN%9q&)6MqA%UA`@mvz;F4J>S_A!>9a*efscb# z(=;Jvf$R(&$zC^-W*suy5~X}hTG}fWj4(mK#Nw)SF1IE$V@L#wAv79#=eQrw^DC{uZ%|}-}cfq zat@k*{f+hcvvsBomd5PC=mh(%OU_vkg*g>6Ql88%AlbrwdTL{SCdNMN*U^*#ZbeYG zh&gmkLM>Hj%*^?Eg+z9_sKXY`TwoqyvN^DSAQ=m=y-9d3TnvO1Q{5THV#VOm!Wni5 zaCTQ9)u;k1#MG4z&iAfm!sfBHC9SJIhBs>?yd|O2L?Ib8fUZ2>=077f`pX$=`h)BG zdne=jYCTCA5KUk6c>AWje^)8Il5ELw@F8n7)L!h7v>Eb=&t2njxGjYex+H^}QHdbj z=9>+=nbqe=b5azJC&v%Y)^`A1w*7h}gF`K)vSeMaQ1%gu>g}1g{mQf0ut!Zjvp8$} zx7p^I#%o@)F~dP4-OY-*n;XsDY3`UaGno+ROt?}lYfA}?Xh~QGTGqaipiE_N4n3OM zKr;==pq>GV^*3NMwYtB!*dZI%Lft~Ji#6AlVXTvO$pS;Meo2SXiophR>(N8Hv3PN# zm0?^wzU;PJ1b|R$skEvdGg@39CQxp!J|R*2L}PFg8{AQ)Y{&tr^Z+6ub3kYy=>xkm z#hh=lk;yn?BR6Ef*&vp1qYhJ+T_BY;$O#jzAGU7nCbT$~TIMXX&veNQUAdO27hgii zo&*66mQ>iRE_EJBDtF=pI&Gknj9ETks=i?CnOq}!6rgA9p3fflX@&0a+LAi6TYLfs z$l@>|TQ*oDJNXawOHoKOAw3t88ce7x9t+_H8gfE5@m6fAU&xRU6bO?$)iMcM9?`Ag z+Hj<{ZhA6~UZ;s1%=c`$`M!U1{oUX9$M3E_`ts>(+xp7&9^wZc9=~HV{4cLw{N_jg zz3aEV`|AJrhjyUHGtB8-ic*#`{98ZxL*Mf~-*dYgOw;tY|K{KPe}3~fQ(s#OjhjL4L%0w(Zopf9l^yUy|LPpY={$saYj+J~^ck)` z3@{HzE*cc?+`seh{N*3|;_h=sbF(grN}1SR>i7S`fAWKW@4xy7cyvBr@M1vnkVz20 zm|+G$1a7Vz?86zer*P}#2i`KHs3mXx++CFiGVSwKGZa7v`=H|?)69OyhS7|ki>+Rx z7V5wJtAF*s_|N}J3(YT135rVD(1o}HO_I##K@2Vdl=;3cGo{T5%EFLFAS=QoWlKRS zeSBiM=|H#B^<7k0*v4VTLAvS1TITUVMHf-#_AK7qF=MojL^Hz7N-4;Uh}x!pQK6eD zTFOLED~YatCt!gYX(<_m13f=wI1HUIp{i__)myF^F0Zg_UXyGIkJU$$ES5qrqI_DYMMiE!RGe*xl1Zz?OD+Mj1(p#rhy| z3X!ShUZdqE14|eYFq_}gOw@IF+Bk%e`~B(CZ6d)w33w>7c8kG~nQGBsW?pP8stg6e z0tVDC3z!t%eawCZ<-FEL?=yFYhzD}kZ0zvo5BQ{;3?}Pun<&8LR=*)>;Jd(#{PK4C z{CfS~R*w-V%%(Sne6aHOR%OIjVGUVEH*2cVWQV(8Zib)dtioFcyPmr!WfpFXMs!L_ zo6~O^ZL>MMUsiWoMrJiDf!waaXm`?v;C8pI*#_{Qu!TKZMMbW+)~a0=o!9Ap9ba0V zesyhta~MGH@=s??-6U_yh?c3$i0tXH5EN=ThY`cN$i&X&|!K^rofYR@J>#jCA*k~L`vtbQ` zjhw9zC8L$PNQJZhk<0BQkxHKiTf2r1?P3m7W@grDwiH?1`y@h_muGWj4uTStSr0CD ziW#$JsQG3KlYweL0ZXVMg(Zn>2U37UTK6-o$Fb>T645USm_sf-N+rzA*yoY^IdDNW zDAXmZMXKa{DV3IS;9L?D^U>(s^wyBjGN4S8CqoEknY|NY-j-Qtlb|JOuFjB=g3S?f z=6wG^pC4GnAmJba8A+Kb4mniJ=1NaxEz$5<&j4rxRUie@?MxX4npL1mqw-?-w(nSf z=lA~Ew;#W}8Gh=wA6?dV7$Dme`{aA?y?tH&*YEni_S%=~XMX)H$>7dl$}s$=|Jh%8 z=gaTBkqRPz<2U}fm+}{X&?S0E2}&I?^^5K`Qe71t9R z5wIXj3Vra<13y}Q;MIS4!If|S+gP=S>nF&{mwe#jjAmtZ^^0Hp;*H<=jsYL|z~ume z#hKptCvOHbF~9Y~HzGHeU3O`7>L0x964Lh2p@Vnbb=S4mettY21GxPC@BiW#|LH6L z{Lg^@;xGKdx#ymH(M1=5aQS7I6(0?7(M4|t;Kib3afueF7|1%?Be^*<%q_Pe&KFAE zcDra5&`~*r3C8AuC-HDM9DVdruYJ{d@BhGGMe7yyD_X?_SX8P&3QH&?5NkokNM?TQ zQAfY_Rp-6$FaNTr^}11EL9L*3-^cW+Z~|fnSV#m=BMt#z$aV`{gqUEiIg2n8n`(BL zn`-XoNKB?M=R{RvQ#XNgG1MEKRl?p}ECaY4`r>;PMiIlW0vutT~7Go_KnKMP0-N9Tbo(#ONz(QHwJ+ zgoUN~79w*36D5$Et07Y-Yz>@T%z}sk?Eq)pN8NjU5m6Db0@GFL)C5d3XCX~VApqRj zoMEPxy_KpaN#x|3sjssLF6Qb24zU6iwK5ZhBF4>%KZlFX5&u01yUziqx+eUOnfSwvOT7p*;C@mLheG-UYe(k5_&YnwB7a@ z&j$qsC`PIlYc-}cNQ)a5j;!mUY0gj~&V-4|&?n=%(FvBE#dsa&;L3fA@`U}M!&K%{ zjfz+6dP76o1{+6pT2qbkOD(HrvuviZ4a)B1YTnYfCPYtoqDX0G9TaAtC4a{?8Y zZmEs9Go^vSU?-!Bh=7&>GwzfDMy|a~Xxbdc7N~E4>G0RnqqO4h<<3v%AVL}3bs7p{ z;!wdkTc@tI;#y}e<&Xb3FU>%{Yexpv$# zYG55`U_+P5;>&*h>6?bz=hKJp-iLlyNpQz%V;{b2-`jrjsON5(`}^VVe1F$%u-wi5 zI-3sY{8zoYltc~?CX>lK-|_ajxj8le%x6A*@x|}F^Uk{-dH7*Wk$Gh($y_BCYbOdF z3>KSa;%J&Y2C*at%q;*n<*iW7v+GbI;wdPh9MJ|fQit!`dtl3!BcIsw_~y+U5A5Gh z%m&}TfB&{4wmz1h+_L4!{rmQho3VGM_wJ(sS09f?*IxU%SHALSu z!&7zrzkxBp?2fJbe=9rH9)^L#U2maJMNINT%oU|w8R)>A&p7jp`|=}A)O|?z9K8S7 zg`+3Ad1!LaiAyJp%(D6Y_VCGPoqq2fcg?6UqV}c>|MgcDA>|B**RnNzJ3w>o0I-A~G{FWv~#Nngfz5%UMkmB~VQ3;cgk~%;IiL zZe#$nlUX!ULI$C*b8tt3aAJZIjN>V&00suojCPF%&0q;_K7kJ{Wxs1Mr}@}zEfWqk z*t4%%J~VJes;ok;*&4WlJ*MlfWrIipmi`w~rq&@Cuu?u1os+u3%Jh)q;3IQVcvxmY zp*AW**P=&c0>`*ET)my2@{O~P&A!~K>S&uspZlJ{V zV!fdn9#L0w){K>u8A4=cBX6^g=SW9vW2GZ7sFB7n6`5n%qU1i`NJ>IAc-4K`#z(kp zSf#yrwRJT!bB9^@mYJdrC7w~}6+uTiPMkYUjEAul!o+G3reWRb+E2raR*b<}uwO^p zFx-T}9-pkC_0E~7HUU>wlzZ7caF|pmVFgV{1(*aQFq43oJX3tg0fk1Pp$5sZU2>*M z2*OYsR9Yo4lL&{XE>Q(#v25f9u8EV&m>dWIa$;7pz_WQIKn3m~W_YolO(10!0%bRc zi$h`(%P?nR5@U-^yK{Klp4ecyE2=3%!-MTwcSHY{mSRqLxOfM%OWsz4l=z1ZNu%`{BW-DNsm z_mfcK7!{eRheg0{x#iZEzv5+AUUlWmUi#8oZ@bM2P`~x&+g|!hFWvdoPyNzMUi`i9 zeJAG@9GOQY9_IDG{rXRR=2N4U(JNm5ihJ+9_s7~o%o>C#1Z|zC;BX<)^k$vGbRMQF zngPJ}*5tAvW-2b9as}0!Eboq=nbDb{;Uq54IQv-#n$f!Y=K9FRwBc!+&YF`AHP0VC zcl|G-g)O;Qyo>2u;1e)?Hwz2mKa`tM%x3J0=#)@*<fO8U3Go>BR!Zv|a#J!30YrzVQb(CudRgyJ7xdu+rQj7A zGY4I<+$Ge{2jjOBGhfo82MN%ibK!zZAznCfXbZ~W5kv+Xl2+`AY<{3Da6%d&GggdR z_2JR7v~4c!X4-?NY;G3ju`| zFrlDMdPLf>mEI#cpt;f9I-EsBip`c3nH}(&n1ioEGhn1}t3n32PtwNo+t`-;PwhijdgEWL&HaGW-JjNvF^>Mp)5R+9MgK3;(%(Mv$OBgi6 zdbB7DCP$1QO+GXkjmIM$ukH7hwY<7{#M0!potR~}T<|R1(M!E|4x)Pp5yf(Rtc&=d zfP;E``s+!HT3;?>ha-p8WiAW&9o=sAF5)fDD413Yj_vLbzTIAh-Ep~cw zZ+ge%caE*0O;P9tI0wMkx(+^42!pkP&V}jdFR+k2m6ED_MmzpONC?VgsKWs>$BYVn zh7NVi`=D}XH=h}K!aNvh#!SrIQ$A<1BMOwGAQ6kxox3QmeiSo(jtEQ`K*+JqaH2YP z&YCBLWae_lvyK^7yVvu|L|tJDQ@8A{?urWdPiG!^`@ZobtL+c>kE=6(YKC!8zS-$# zoPom)88_YZJ!V!jRjsRP<ptgDcK-Zz*Kggr z?Z3VIgIkW+{Hf1e6K~Ab*Id&P$8hJ}cV7Hw7k}tOA3EifQ$F;e55=e8nw{5NbIneG zuGzWsnrn7Wd1M}!03ZHKIlPOcA8TC?!z|9y8lAf6iWxczj8M&QW3FQeTxLbqp^cV~ z-Lk&2_oSs`_KsJd7~S=Z&8Oab@ZrR%mimdcy(cUkyBqtL$A{*R-E2kc;$er17kT*y zFWMGsJ1xs8!5nYSGSy&_+zbImw3BDmI-1=G`jRZE9*s^;6l(;S>O+*K#bM{c%B#uIx7 z2iJxmsmPLqEY2a$7I|C&!Rg@N#V|nK@sxwM8?7>%fn%>W_1;b+4-3VlpNMuWUvdVS`rfhKHel507|Ndg(Q!+I zbRzSH;2CGzE7&?mr#GO+9&pMPRTX=qO$WX2m2A}W!KZSPZiz8t0s}OcH>a`y03ZNKL_t&)HlBMC(U^CBWh&7D#6|nW zGn}I`aqB_MVx;iB7#)GZ5g4psJSi3t2fVem=Am$`|1<+)% z-J{O2J2TZ`m=~c@p{7V5aT?MK4sjHPY@8U{vFE1(kvJ5>x0wI~J2H8q?%@y2WtWFf=*6!;wtB;(tG|5GhAp+oQ76Z1>x?&W;DP(@ zyY-e^p8f1+$Io%qRafobx9{Nc!P8DVjaeRk=;7b~?cV{gu&^*5kE7KkH)2mb5J|Zm zyV~4fQ??OFZrdCq7=r@l1S!HD{=cm3Qc(Xw5VFL=*>VR{H2`aCtM7UDdt1v@&6J41 zDamMk{r#7I(6=<}la;*I5HScl&fnpo{vyBOn_s`-h8v(LgGP`O&f9T*Cng4_xlh51 z;zZQ?tOoSo$Vr)wxcU`DbbM1qb_q8LYTpboGp?WouI7=3$dfdkki#9`n60ZnvbKE8 z{Fc_*L+yB7Rg5M&5gtA|!-1WSZLvWje+(>NzXo5YjA8srws<7%CF{)8+~ zq49lo#2(wWZIW6%zHhj?GBB5ld_ZR8tz#&tg(huxx4QQfj(FgO86AnPq!bW?Sxn2Q zz@s`WJ_I+`*kMGVM?i0l5Spd1dnF<&c=;HYhpaQ8Z9TftyZ^Bs9@+&wm5>(4Y~AV* zrtpb(oE9yXYKNKS?8*d+eF&gXHLt(|${pq|h=kK7fLYzhJvkAvAso+T%SY}?ha+#9 zQ4og$HnqKDzs(49=~z=ze{`HNCnqsB1wb|19-?gnIn8pF^**M_fCfZpyy-TCT~WlC zhF^6S{!VJ-*^QltX|b+14yuJp1SK7#0Wn#lG9g%^dRV8#Q07MSL04oY*GDnhjA~;` zE9>hAQaZAs4UjdmL*8c50esDPne7;xe*%Y=y^*-T!}Ub&%Zz%+=aSM$O1h*&J+ec4kVcO4T3^T-SEe=By3e z$qGQE6JCQH3T??mi5y@na`KXX3Uf%l;jESV6!f^IxO<${YIq(coBzAtsGXy z;nod_GshsjOZEXsy6B58EIB|b?8d+XcoM70m{fC*Q=FY!mQqc;J z6-n!+Af&{Tc4FqIZ5W(&cfZ7VOXq-X)gX{ z200x#u>WuW=C9}H7oPjv=Zzj5FT&$8X$na9jM^&D220!iQTOWM{~F_v%|7l4DgMV zrDjU3oVA^-jC7z>!ZGP>&NBKSUZN)B8#d>KC^mr(1C|`uPxe|t~@bbdFJL5zklH2&DFvKYfl37)Q!hI zx^`fHvo`?;AKv5Q4n}}hBY6VIs@Gk6-5>nHAKr8CuC>u<`qG&IJo9N!``A^V{{F2u zb2EC@GtPR()6aVNi5ucIZQQW&WiNaA?YG}CYtH(_CqGGyDc>I*z_w%Bf_n~GaS`)W zlv&yXzz!T(KJ}DSfGNc|e%QDC>7NBHx#G&5gj+$oI@)BD4Iz9n&1al`!d-Vi;5oTF zjIx_JV6;A3KmM4fEG%t=AqIR7&k~9VIPCOOPr2*fdpsgmq8`>V(xAdniKlFKhzoKf zGdGISd&xf`Qd8H=KoVk*nuV`>x_+i$O)wFYdtyLgBwa<~609zVxf^+Exk?akS;*am zysm6WuE2W3qnuhN5t<*GFf^l?&}x%tQ`V<9D=W1;)Nb55t`~j%iOSbDrKa^kn`$*< z^D3og(|prtGGp&T*HKPGc9=xF1TK_1ipddoKLut8{86-;7xPQ$-VmnW$aaZ_F&WJp z$gytTQkNCblfPzyLOR!1K>f5;q2!c{WBGh$Mk$ z^VSFgW;5e}8+UPC=AAH=ORi+>-*!ZN6pJAYnIe^H5ezUJXbBX7L9(n+k2ND(Ue+LmMFq$P;VSm8|pM9V)JaS zW}aJfXkBSlOQK|%a`tQ!n`qPeSer&|D{3d(?W08uk6WL+M<#o?-2&R`oV98PO|9ab z_E58AbK7O}itZ+4M+zRDc>&va@p*TFR%Q6%g})r0sHm%n!+u@DT#bWcS;T9hZitvg za7?E5dUJ&95g1NjtH>Sk#Z0rY#86m3=B=PA#Gjs1A8H%GT_gq{Na0iCDZcZI z7F6ch(JHhcQu_Tk$dDMqMCM9vR7tC?oa z22v(wK&b>H5DcacZpn?2%?6YPjs#bSIk_umDuPH!TvEq0Vu-v1F!lKT4j~jafKInl zXki^bL`&{-m!hrQGr(fO%}i`5iYRq1UudchaRT@LY42ECJWV|hL9t659L^=V&p}`& zQdnoUo#EV}6P-tS8}f8hhUX}j`}?FLt8}U&VJiSn?&x0*5a!_vZW*)=-_m4C$q0?Z zBqeVS6N0u~tee2wF&yAn@^2^`W73l6;t37?)jk*=3i#|ME*Oi}~#P-}k;*Cc$A##|}F4PhY(Lf?xa9 zr=EU#`DKTklS{;to95hVq+xSKT#?mq5HfK-^UM?PzW082z^mBjxS6_r{afF7{p((L z;_=&IL|`3u2~M%OR-AF_X?NdyZ-}}6{E~}fANJ>$yer=KKfmPS2-umqd^`QMgPD^g z5plSh=XOFlS3rzHOw}!S^L+1Mhny&hNF;d0#HDLo z(v8Emu55M8qgGTEgeut}xwinY@6aHd%W1h?!sL;>iN}jiu|o~^AKW;Y)Y~9Oz>^~b zEVg-3>)M)$xYrC}a%L75aH8VA4g_9o6D$Nx23ATrRW3!p+O%-*nI>?ecvqn0*+~rsWKIp!n7v^{ znIrC$+nf!|HR^feBwi&*2afEQK!h&Zn5>+wOpq>W#1H3ga0GqDK9FOJRx~S${Uf_M z>!Wk^Q|9=%>`#u``-z3Ig>f`!sqx3mSB7%3&{G?)o2(U8!d#L_y5?|3cp3sYi~FYH z`T6;y7Uqr|R&yX|&biHSO_kJh_1t_db#m2lGd{F-aCx+UWwg9LI+&Yvpc9zbG7>M?u}fe;rw zp^!*Xpi!Z#6l)R-gnL#eh@BG=2iAjynb*Y+U^E0Dfw~5Z*8~!9hP7x}5+fx<7I5Ek zb=(c)2;0;03Gq&@68bprxV&Zs0cZx6G%q|aG59FsYbjM7$dKI|HFY;n2|hp}*a@5F zJZ_q`(PU+9efiM(@?^ByHZ357dU0WKE*f8oA^-+pxU{oi@~kH7ZFD^A={6E-B?JV=imY}&iN<5PZLw^tx~-=&wG z^D{ro%;xs!qmTZ?v!4C7x4kV&qBz~Y{V(6NwxvEKOHs*?D5+*spC4{$npI++KR1l_4oX-G)@L3sSY7 z$Lq9QjH}%go`_9`E`pqqDpYgG6SQk$X4TNEnzWI20#hP#Vj=}n^eE*tquz5@Zj|^* zB^ruO65@TR{B#-^(?|i4bMa#*R|B_$1tJWZmO$(oaFdGNhD4+Bwu9phPAy?fV!;Nr z2?IlTTMeH?JrZ`QiHtS7!F;n@1v*qg1!}O2WDth3HE@t8f=w6pLBXCK*QOd2w;vaaj<7K6B< z81%|+CT1fu_F<3P*h)@8$!rQB8c~2~HVyeF=jB9e_pIX)p)Cvb2~7j=!B!tv>`Qda zKsHVASd;Ircm%FAPg=Bvl3-m=8YvManm8>~n})*;DGdpvZ41`K6-#o?232dlU2|>C zCXH$~Co&_Wg?UCL z5RsaV;bSQyD<@=zwD1hem^iY*5)IRE5l)`1Ld~#c+q+vv-HkzP;ge=!NUB4SOMnZ!}>`Mw?J6VJ+!;qBh5DHbx0dE{RCM=H7ba<|#}W zMfw)wXEHK&9wODA47p_^hA*IX=twEEi&N`cWy9iduus36j z*c}2+DA-X3+9EOWB_u!;YUJjW$pw_01OlQF)FHK}MAaG}TC)RT$Dcvr0f2Dja5arp zgb*3Capgpm)kSP!6Sr$VX$U6Tnp8%KtzpmREXYCfHAl^-i~fXTpUbYf>a59l$aU*@ zvT54+;oP`+0M%;sns=YIZT{SoHvPp-PlWxYA9%g=qzylP+y-;I>dphV?^zpPaS>F* zPt+q{-TgVweeUo6?(28%+sgZkWH zf$PO}ZM?y8TepAUFW(LFefxIbamNp@zVhnjgZm*wN~8@7)$EnfN=vKbSno?>7D(ch zL}E0n?%A`tIZTLJZ9|c5ZY(mK8&q|ro>x~7Hlqm~I0hi_UKoR&ZDK^^999>Y;+lWh z3E;r;@~1xasq4S|!+Z2IfJ{?H%%=RY{GysU2CrGIp9s2?7B z5xs7gIZVA>iDzg?Mamy8o^3}TB~+z_6Z3fY?!Eh+Q<_Dc#Tx#*S6p}IYk%?P)yKcJ z_rdsvkB!zU;!q_&Z}I5cuK2t_3><4_6gc5bTr+v_Qk{B%ns<~=LaLxO*_aNCU7neN z1#h~ugmgWv7q(?5FmS?*^n!^BdaZNf-_eCX-O9p~I7w}) zojbz&cyux&R!6dAlC$S*G=R8ks61On;y|4dldHkFGeJAe4vEbu1}DrMip*$W`nWUm zn67lPu(EmXTc3XALBcFD$*Pts`^>G>q zO;inukvA^1$rpZjh4OS0Kt6DWz>*E0d zBIc$bGNhQfy19~Mq;T99eFvAEhJt`rkIii8iMpV>aN2R1O~#)Hww!hEO+!%jXrV(_IPE1y*J-j}7a8Pg0 z?ZyOVQ1;!eXTs5mlG$CL)mmy8TF*vP5J&`hGjS#J!@=e%%_mNgT38Kg5q8VDU1{=q zqfJ&9%EV4EGAA=0m>XvjHx~jivg>~0BL#I%yOXj`wxMn?_b^8LG1;VpMNaEt0@O&$ zm>eT>C*=A2dCdw<+dF(i_%UfF9M|@b6=GweXo?0|;80!pPbw$}tB9&U#5| zAq@OcSu3N<215ok1Uc$s&I3sUCqf29tIlo5-RYiOvRLb8I%21)5$05LRo693H9xm5QCUedu0)ALocIMsS6>M|dN9f6>v@a%ElSZbInxLUqS|Q!FIKi|$Tr)Td`s zfzcE+jrX|iGN6slQVTAbi3KIJ(z@HoX@uF$5ya3uA0+?n7cBz%zpN5<)1EnwdZHcYkxi zYhK+pZCUnAfO#&q{BCMPR(wT7IPbh2L^$(lPkZy5-hBO+zXbS>9p`s-%XGML_St7M z^H06-1z-55FOpGNsu21`f2W3;U5iow0irHfc<0qu@7Qs{6zIfEhF*$8h_IPlNX+g% zssj!zFR%C&U-`LLz2qC?$5*Y5FES!sU!ntR4)YNAIjA_ecqWs(pWDQP zVF=b?zHyXS4j%S!hPe@w#_`4Q1WhCjNzpf%F}p^pAT@*`+tAPgs1SGsM7#+x$O2^_F^g0Z z5QFPS!c$>K*3h@Q5xG_HG6+QcgK3mIG)%5#;28@B2y3hEIgvA{?e=TbA?HY>M9F#r zQ=LFV6IH2@8;qK&E?-X+G^lK;Hc}i~LdGxe}pB1-NGRdr?}HlmpQk-^o$ z9td?$43kg zgipzGo;2C2s;ZeA2YHaJ0xYhPi9n2!JSL?w{ZGWT5j z%suCVlJ1X`L|DdnKL%ScSVBFr30NJhW=a!E6=@>046y<{F4W+XPGyA)ECu@@MA0#J zlDW9Qp@zmOajXVCELUZO!==Jd4iGaeLxGl11&x9o!V;<89U{n;R2kXaLXYQJ+)Oo3 zvOA1eQdQS!Zf+g}w3>6s)1z}KQFK-s5b42TGkG55fp6tPAd6j1o;k#4COz(;3{UY@!V3l@aKSZL znEs%hPJfof)#J@N NFNmDl?PApO!FFx8BC7KM$-hFuh|fg0v{ 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 literal 0 HcmV?d00001 diff --git a/translations/syncthingtray_de_DE.ts b/translations/syncthingtray_de_DE.ts new file mode 100644 index 0000000..249c8ff --- /dev/null +++ b/translations/syncthingtray_de_DE.ts @@ -0,0 +1,614 @@ + + + + + Data::SyncthingConnection + + + disconnected + + + + + connected + + + + + connected, notifications available + + + + + connected, paused + + + + + connected, synchronizing + + + + + unknown + + + + + Unable to parse Syncthing log: + + + + + Unable to request system log: + + + + + + Unable to parse Syncthing config: + + + + + + Unable to request Syncthing config: + + + + + Unable to parse Syncthing events: + + + + + Unable to request Syncthing events: + + + + + Unable to request rescan: + + + + + Unable to request pause/resume: + + + + + Unable to request QR-Code: + + + + + Data::SyncthingDeviceModel + + + + ID + + + + + Status + + + + + Addresses + + + + + Compression + + + + + Certificate + + + + + Introducer + + + + + none + + + + + yes + + + + + no + + + + + Unknown status + + + + + + Idle + + + + + Synchronizing (%1 %) + + + + + Synchronizing + + + + + Paused + + + + + Out of sync + + + + + Data::SyncthingDirectoryModel + + + + ID + + + + + Status + + + + + Path + + + + + Devices + + + + + Rescan interval + + + + + yes + + + + + no + + + + + Idle + + + + + Scanning (%1 %) + + + + + Synchronizing (%1 %) + + + + + Out of sync + + + + + Scanning + + + + + Read-only + + + + + Unknown status + + + + + Synchronizing + + + + + Paused + + + + + QtGui::ConnectionOptionPage + + + Connection + + + + + Syncthing URL + + + + + Authentication + + + + + User + + + + + disconnected + + + + + Status + + + + + Apply connection settings and try to reconnect + + + + + API key + + + + + Password + + + + + QtGui::DevView + + + Copy + + + + + QtGui::DirView + + + Copy + + + + + QtGui::LauncherOptionPage + + + Launcher + + + + + QtGui::NotificationsOptionPage + + + Notifications + + + + + Notify on disconnect + + + + + Notify on errors + + + + + Notify on sync complete + + + + + Show Syncthing notifications + + + + + QtGui::SettingsDialog + + + Tray + + + + + Web view + + + + + QtGui::TrayIcon + + + Web UI + + + + + Settings + + + + + About + + + + + Close + + + + + Syncthing error + + + + + Syncthing notification + + + + + Not connected to Syncthing + + + + + Disconnected from Syncthing + + + + + Syncthing is running + + + + + Notifications available + + + + + Syncthing has been suspended + + + + + Synchronization is ongoing + + + + + Synchronization complete + + + + + QtGui::TrayWidget + + + Form + + + + + Close + + + + + + Connect + + + + + Directories + + + + + Devices + + + + + About + + + + + Settings + + + + + Web UI + + + + + View own device ID + + + + + Settings - Syncthing tray + + + + + Tray application for Syncthing + + + + + Rescan all directories + + + + + Show Syncthing log + + + + + About - Syncthing Tray + + + + + Own device ID - Syncthing Tray + + + + + device ID is unknown + + + + + Copy to clipboard + + + + + Log - Syncthing + + + + + Not connected to Syncthing, click to connect + + + + + Pause + + + + + Syncthing is doing its job, click to pause + + + + + Continue + + + + + Syncthing is suspended, click to continue + + + + + The directly <i>%1</i> does not exist on the local machine. + + + + + QtGui::WebViewDialog + + + Syncthing + + + + + QtGui::WebViewOptionPage + + + + General + + + + + Usage + + + + + Disable web view (open regular web browser instead) + + + + + Keep web view running when currently not shown + + + + + Zoom factor + + + + + Hiding + + + + + Syncthing Tray has not been built with vieb view support utilizing either Qt WebKit or Qt WebEngine. +The Web UI will be opened in the default web browser instead. + + + + + main + + + The system tray is (currently) not available. + + + + + The Qt libraries have not been built with tray icon support. + + + + diff --git a/translations/syncthingtray_en_US.ts b/translations/syncthingtray_en_US.ts new file mode 100644 index 0000000..fc133c1 --- /dev/null +++ b/translations/syncthingtray_en_US.ts @@ -0,0 +1,614 @@ + + + + + Data::SyncthingConnection + + + disconnected + + + + + connected + + + + + connected, notifications available + + + + + connected, paused + + + + + connected, synchronizing + + + + + unknown + + + + + Unable to parse Syncthing log: + + + + + Unable to request system log: + + + + + + Unable to parse Syncthing config: + + + + + + Unable to request Syncthing config: + + + + + Unable to parse Syncthing events: + + + + + Unable to request Syncthing events: + + + + + Unable to request rescan: + + + + + Unable to request pause/resume: + + + + + Unable to request QR-Code: + + + + + Data::SyncthingDeviceModel + + + + ID + + + + + Status + + + + + Addresses + + + + + Compression + + + + + Certificate + + + + + Introducer + + + + + none + + + + + yes + + + + + no + + + + + Unknown status + + + + + + Idle + + + + + Synchronizing (%1 %) + + + + + Synchronizing + + + + + Paused + + + + + Out of sync + + + + + Data::SyncthingDirectoryModel + + + + ID + + + + + Status + + + + + Path + + + + + Devices + + + + + Rescan interval + + + + + yes + + + + + no + + + + + Idle + + + + + Scanning (%1 %) + + + + + Synchronizing (%1 %) + + + + + Out of sync + + + + + Scanning + + + + + Read-only + + + + + Unknown status + + + + + Synchronizing + + + + + Paused + + + + + QtGui::ConnectionOptionPage + + + Connection + + + + + Syncthing URL + + + + + Authentication + + + + + User + + + + + disconnected + + + + + Status + + + + + Apply connection settings and try to reconnect + + + + + API key + + + + + Password + + + + + QtGui::DevView + + + Copy + + + + + QtGui::DirView + + + Copy + + + + + QtGui::LauncherOptionPage + + + Launcher + + + + + QtGui::NotificationsOptionPage + + + Notifications + + + + + Notify on disconnect + + + + + Notify on errors + + + + + Notify on sync complete + + + + + Show Syncthing notifications + + + + + QtGui::SettingsDialog + + + Tray + + + + + Web view + + + + + QtGui::TrayIcon + + + Web UI + + + + + Settings + + + + + About + + + + + Close + + + + + Syncthing error + + + + + Syncthing notification + + + + + Not connected to Syncthing + + + + + Disconnected from Syncthing + + + + + Syncthing is running + + + + + Notifications available + + + + + Syncthing has been suspended + + + + + Synchronization is ongoing + + + + + Synchronization complete + + + + + QtGui::TrayWidget + + + Form + + + + + Close + + + + + + Connect + + + + + Directories + + + + + Devices + + + + + About + + + + + Settings + + + + + Web UI + + + + + View own device ID + + + + + Settings - Syncthing tray + + + + + Tray application for Syncthing + + + + + Rescan all directories + + + + + Show Syncthing log + + + + + About - Syncthing Tray + + + + + Own device ID - Syncthing Tray + + + + + device ID is unknown + + + + + Copy to clipboard + + + + + Log - Syncthing + + + + + Not connected to Syncthing, click to connect + + + + + Pause + + + + + Syncthing is doing its job, click to pause + + + + + Continue + + + + + Syncthing is suspended, click to continue + + + + + The directly <i>%1</i> does not exist on the local machine. + + + + + QtGui::WebViewDialog + + + Syncthing + + + + + QtGui::WebViewOptionPage + + + + General + + + + + Usage + + + + + Disable web view (open regular web browser instead) + + + + + Keep web view running when currently not shown + + + + + Zoom factor + + + + + Hiding + + + + + Syncthing Tray has not been built with vieb view support utilizing either Qt WebKit or Qt WebEngine. +The Web UI will be opened in the default web browser instead. + + + + + main + + + The system tray is (currently) not available. + + + + + The Qt libraries have not been built with tray icon support. + + + +