commit b7609d7d3e0b099094dde76b454661e1f9e40d4c Author: Martchus Date: Thu Aug 25 00:45:32 2016 +0200 Initial import 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 0000000..5ad0025 Binary files /dev/null and b/resources/screenshots/1.png differ 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. + + + +