Initial import

This commit is contained in:
Martchus 2016-08-25 00:45:32 +02:00
commit b7609d7d3e
45 changed files with 5618 additions and 0 deletions

44
.gitignore vendored Normal file
View File

@ -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.*

102
CMakeLists.txt Normal file
View File

@ -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()

339
LICENSE Normal file
View File

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
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.

29
README.md Normal file
View File

@ -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.

68
application/main.cpp Normal file
View File

@ -0,0 +1,68 @@
#include "./settings.h"
#include "../gui/tray.h"
#include "resources/config.h"
#include <c++utilities/application/argumentparser.h>
#include <c++utilities/application/commandlineutils.h>
#include <c++utilities/application/failure.h>
#include <qtutilities/resources/qtconfigarguments.h>
#include <qtutilities/resources/resources.h>
#include <qtutilities/settingsdialog/qtsettings.h>
#include <QApplication>
#include <QMessageBox>
#include <iostream>
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;
}

161
application/settings.cpp Normal file
View File

@ -0,0 +1,161 @@
#include "./settings.h"
#include <qtutilities/settingsdialog/qtsettings.h>
#include <QString>
#include <QByteArray>
#include <QApplication>
#include <QSettings>
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 &notifyOnDisconnect()
{
static bool v = true;
return v;
}
bool &notifyOnErrors()
{
static bool v = true;
return v;
}
bool &notifyOnSyncComplete()
{
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);
}
}

51
application/settings.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef SETTINGS_H
#define SETTINGS_H
#include <c++utilities/conversion/types.h>
#include <QtGlobal>
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 &notifyOnDisconnect();
bool &notifyOnErrors();
bool &notifyOnSyncComplete();
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

View File

@ -0,0 +1,614 @@
#include "./syncthingconnection.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QPixmap>
#include <QAuthenticator>
#include <QStringBuilder>
#include <utility>
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<void(const QPixmap &)> 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<void (const std::vector<SyncthingLogEntry> &)> 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<SyncthingLogEntry> 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<QNetworkReply *>(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<QNetworkReply *>(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<QNetworkReply *>(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<QNetworkReply *>(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<QNetworkReply *>(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);
}
}
}

332
data/syncthingconnection.h Normal file
View File

@ -0,0 +1,332 @@
#ifndef SYNCTHINGCONNECTION_H
#define SYNCTHINGCONNECTION_H
#include <QObject>
#include <functional>
#include <vector>
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<SyncthingDir> &dirInfo() const;
const std::vector<SyncthingDev> &devInfo() const;
void requestQrCode(const QString &text, std::function<void (const QPixmap &)> callback);
void requestLog(std::function<void (const std::vector<SyncthingLogEntry> &)> 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<SyncthingDir> &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<SyncthingDev> &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<SyncthingDir> m_dirs;
std::vector<SyncthingDev> 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<SyncthingDir> &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<SyncthingDev> &SyncthingConnection::devInfo() const
{
return m_devs;
}
}
#endif // SYNCTHINGCONNECTION_H

View File

@ -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<quintptr>(-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<int>(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

View File

@ -0,0 +1,43 @@
#ifndef DATA_SYNCTHINGDEVICEMODEL_H
#define DATA_SYNCTHINGDEVICEMODEL_H
#include <QAbstractItemModel>
#include <QIcon>
#include <vector>
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<SyncthingDev> &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

View File

@ -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<quintptr>(-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<int>(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<int>() << Qt::DecorationRole);
const QModelIndex modelIndex2(this->index(index, 1, QModelIndex()));
emit dataChanged(modelIndex2, modelIndex2, QVector<int>() << Qt::DisplayRole << Qt::ForegroundRole);
}
} // namespace Data

View File

@ -0,0 +1,48 @@
#ifndef DATA_SYNCTHINGDIRECTORYMODEL_H
#define DATA_SYNCTHINGDIRECTORYMODEL_H
#include <QAbstractItemModel>
#include <QIcon>
#include <vector>
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<SyncthingDir> &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

192
gui/connectionoptionpage.ui Normal file
View File

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QtGui::ConnectionOptionPage</class>
<widget class="QWidget" name="QtGui::ConnectionOptionPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>433</width>
<height>267</height>
</rect>
</property>
<property name="windowTitle">
<string>Connection</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="urlLabel">
<property name="text">
<string>Syncthing URL</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="Widgets::ClearLineEdit" name="urlLineEdit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="authLabel">
<property name="text">
<string>Authentication</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="authCheckBox"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="userNameLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>User</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="Widgets::ClearLineEdit" name="userNameLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="passwordLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>disconnected</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="statusTextLabel">
<property name="text">
<string>Status</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QPushButton" name="connectPushButton">
<property name="text">
<string>Apply connection settings and try to reconnect</string>
</property>
<property name="icon">
<iconset theme="view-refresh">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="Widgets::ClearLineEdit" name="apiKeyLineEdit"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="apiKeyLabel">
<property name="text">
<string>API key</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Widgets::ClearLineEdit</class>
<extends>QLineEdit</extends>
<header location="global">qtutilities/widgets/clearlineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>authCheckBox</sender>
<signal>toggled(bool)</signal>
<receiver>userNameLabel</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>authCheckBox</sender>
<signal>toggled(bool)</signal>
<receiver>userNameLineEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>authCheckBox</sender>
<signal>toggled(bool)</signal>
<receiver>passwordLabel</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>authCheckBox</sender>
<signal>toggled(bool)</signal>
<receiver>passwordLineEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
</ui>

48
gui/devview.cpp Normal file
View File

@ -0,0 +1,48 @@
#include "./devview.h"
#include <QHeaderView>
#include <QMouseEvent>
#include <QMenu>
#include <QCursor>
#include <QGuiApplication>
#include <QClipboard>
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);
}
}
}
}

22
gui/devview.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef DEVVIEW_H
#define DEVVIEW_H
#include <QTreeView>
namespace QtGui {
class DevView : public QTreeView
{
Q_OBJECT
public:
DevView(QWidget *parent = nullptr);
private Q_SLOTS:
void showContextMenu();
void copySelectedItem();
};
}
#endif // DEVVIEW_H

View File

@ -0,0 +1,60 @@
#include "./dirbuttonsitemdelegate.h"
#include <QPixmap>
#include <QPainter>
#include <QApplication>
#include <QStyle>
#include <QTextOption>
#include <QStyleOptionViewItem>
#include <QBrush>
#include <QPalette>
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);
//}
}

View File

@ -0,0 +1,25 @@
#ifndef DIRBUTTONSITEMDELEGATE_H
#define DIRBUTTONSITEMDELEGATE_H
#include <QStyledItemDelegate>
#include <QPixmap>
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

67
gui/dirview.cpp Normal file
View File

@ -0,0 +1,67 @@
#include "./dirview.h"
#include "./dirbuttonsitemdelegate.h"
#include <QHeaderView>
#include <QMouseEvent>
#include <QMenu>
#include <QCursor>
#include <QGuiApplication>
#include <QClipboard>
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);
}
}
}
}

29
gui/dirview.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef DIRVIEW_H
#define DIRVIEW_H
#include <QTreeView>
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

20
gui/launcheroptionpage.ui Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QtGui::LauncherOptionPage</class>
<widget class="QWidget" name="QtGui::LauncherOptionPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>229</width>
<height>164</height>
</rect>
</property>
<property name="windowTitle">
<string>Launcher</string>
</property>
<layout class="QFormLayout" name="formLayout"/>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QtGui::NotificationsOptionPage</class>
<widget class="QWidget" name="QtGui::NotificationsOptionPage">
<property name="windowTitle">
<string>Notifications</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="notifyOnDisconnectCheckBox">
<property name="text">
<string>Notify on disconnect</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="notifyOnErrorsCheckBox">
<property name="text">
<string>Notify on errors</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="notifyOnSyncCompleteCheckBox">
<property name="text">
<string>Notify on sync complete</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showSyncthingNotificationsCheckBox">
<property name="text">
<string>Show Syncthing notifications</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

214
gui/settingsdialog.cpp Normal file
View File

@ -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 <tagparser/mediafileinfo.h>
#include <tagparser/backuphelper.h>
#include <qtutilities/settingsdialog/optioncategory.h>
#include <qtutilities/settingsdialog/optioncategorymodel.h>
#include <qtutilities/settingsdialog/qtsettings.h>
#include <functional>
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<Dialogs::OptionCategory *> categories;
Dialogs::OptionCategory *category;
category = new OptionCategory(this);
category->setDisplayName(tr("Tray"));
category->assignPages(QList<Dialogs::OptionPage *>()
<< 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<Dialogs::OptionPage *>() << 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()
{}
}

51
gui/settingsdialog.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef SETTINGS_DIALOG_H
#define SETTINGS_DIALOG_H
#include <qtutilities/settingsdialog/settingsdialog.h>
#include <qtutilities/settingsdialog/optionpage.h>
#include <qtutilities/settingsdialog/qtsettings.h>
#include <QWidget>
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

455
gui/tray.cpp Normal file
View File

@ -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 <qtutilities/resources/qtconfigarguments.h>
#include <qtutilities/resources/resources.h>
#include <qtutilities/resources/importplugin.h>
#include <qtutilities/settingsdialog/qtsettings.h>
#include <qtutilities/aboutdialog/aboutdialog.h>
#include <qtutilities/misc/dialogutils.h>
#include <qtutilities/misc/desktoputils.h>
#include <QApplication>
#include <QSvgRenderer>
#include <QPainter>
#include <QPixmap>
#include <QMenu>
#include <QDesktopServices>
#include <QMainWindow>
#include <QMessageBox>
#include <QCursor>
#include <QDesktopWidget>
#include <QLabel>
#include <QClipboard>
#include <QUrl>
#include <QDir>
#include <QTextBrowser>
#include <QStringBuilder>
#include <functional>
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<SyncthingLogEntry> &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 <i>%1</i> 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;
}
}

125
gui/tray.h Normal file
View File

@ -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 <QWidget>
#include <QSystemTrayIcon>
#include <QMenu>
#include <memory>
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<Ui::TrayWidget> 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

248
gui/traywidget.ui Normal file
View File

@ -0,0 +1,248 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QtGui::TrayWidget</class>
<widget class="QWidget" name="QtGui::TrayWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QFrame" name="buttonsFrame">
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="closePushButton">
<property name="toolTip">
<string>Close</string>
</property>
<property name="icon">
<iconset theme="window-close">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="aboutPushButton">
<property name="toolTip">
<string>About</string>
</property>
<property name="icon">
<iconset theme="help-about">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="statusPushButton">
<property name="text">
<string>Connect</string>
</property>
<property name="icon">
<iconset theme="view-refresh">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="settingsPushButton">
<property name="toolTip">
<string>Settings</string>
</property>
<property name="icon">
<iconset theme="preferences-other">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="webUiPushButton">
<property name="toolTip">
<string>Web UI</string>
</property>
<property name="icon">
<iconset theme="internet-web-browser">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="tabPosition">
<enum>QTabWidget::South</enum>
</property>
<property name="tabShape">
<enum>QTabWidget::Triangular</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<widget class="QWidget" name="dirsTab">
<attribute name="icon">
<iconset theme="folder">
<normaloff>.</normaloff>.</iconset>
</attribute>
<attribute name="title">
<string>Directories</string>
</attribute>
<layout class="QVBoxLayout" name="dirsTabVerticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QtGui::DirView" name="dirsTreeView"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="devsTab">
<attribute name="icon">
<iconset theme="network-server">
<normaloff>.</normaloff>.</iconset>
</attribute>
<attribute name="title">
<string>Devices</string>
</attribute>
<layout class="QVBoxLayout" name="devsTabVerticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QtGui::DevView" name="devsTreeView"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QtGui::DirView</class>
<extends>QTreeView</extends>
<header>./gui/dirview.h</header>
</customwidget>
<customwidget>
<class>QtGui::DevView</class>
<extends>QTreeView</extends>
<header>./gui/devview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

88
gui/webpage.cpp Normal file
View File

@ -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 <QDesktopServices>
#include <QAuthenticator>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#if defined(SYNCTHINGTRAY_USE_WEBENGINE)
# include <QWebEngineSettings>
# include <QWebEngineView>
#elif defined(SYNCTHINGTRAY_USE_WEBKIT)
# include <QWebSettings>
# include <QWebView>
# include <QWebFrame>
#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<void(WebPage::*)(const QUrl &, QAuthenticator *)>(&WebPage::supplyCredentials));
#else
settings()->setAttribute(QWebSettings::JavascriptCanOpenWindows, true);
setNetworkAccessManager(&Data::networkAccessManager());
connect(&Data::networkAccessManager(), &QNetworkAccessManager::authenticationRequired, this, static_cast<void(WebPage::*)(QNetworkReply *, QAuthenticator *)>(&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)

44
gui/webpage.h Normal file
View File

@ -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 <QWebEnginePage>
#elif defined(SYNCTHINGTRAY_USE_WEBKIT)
# include <QWebPage>
#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

63
gui/webviewdialog.cpp Normal file
View File

@ -0,0 +1,63 @@
#ifndef SYNCTHINGTRAY_NO_WEBVIEW
#include "./webviewdialog.h"
#include "./webpage.h"
#include "../application/settings.h"
#include "../data/syncthingconnection.h"
#include <qtutilities/misc/dialogutils.h>
#include <QIcon>
#include <QCloseEvent>
#if defined(SYNCTHINGTRAY_USE_WEBENGINE)
# include <QWebEngineView>
#elif defined(SYNCTHINGTRAY_USE_WEBKIT)
# include <QWebView>
#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

33
gui/webviewdialog.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef WEBVIEW_DIALOG_H
#define WEBVIEW_DIALOG_H
#ifndef SYNCTHINGTRAY_NO_WEBVIEW
#include "./webviewprovider.h"
#include <QMainWindow>
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

72
gui/webviewoptionpage.ui Normal file
View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QtGui::WebViewOptionPage</class>
<widget class="QWidget" name="QtGui::WebViewOptionPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>356</width>
<height>98</height>
</rect>
</property>
<property name="windowTitle">
<string>General</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="0">
<widget class="QLabel" name="usageLabel">
<property name="text">
<string>Usage</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="disableCheckBox">
<property name="text">
<string>Disable web view (open regular web browser instead)</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="zoomLabel">
<property name="text">
<string>Zoom factor</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="zoomDoubleSpinBox">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::PlusMinus</enum>
</property>
<property name="maximum">
<double>5.990000000000000</double>
</property>
<property name="singleStep">
<double>0.200000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="keepRunningLabel">
<property name="text">
<string>Hiding</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="keepRunningCheckBox">
<property name="text">
<string>Keep web view running when currently not shown</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

11
gui/webviewprovider.h Normal file
View File

@ -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

12
resources/icons.qrc Normal file
View File

@ -0,0 +1,12 @@
<RCC>
<qresource prefix="/">
<file>icons/hicolor/scalable/app/syncthingtray.svg</file>
<file>icons/hicolor/scalable/status/syncthing-default.svg</file>
<file>icons/hicolor/scalable/status/syncthing-notify.svg</file>
<file>icons/hicolor/scalable/status/syncthing-pause.svg</file>
<file>icons/hicolor/scalable/status/syncthing-sync.svg</file>
<file>icons/hicolor/scalable/status/syncthing-disconnected.svg</file>
<file>icons/hicolor/scalable/status/syncthing-ok.svg</file>
<file>icons/hicolor/scalable/status/syncthing-error.svg</file>
</qresource>
</RCC>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 117.3 117.3" enable-background="new 0 0 117.3 117.3" xml:space="preserve">
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="58.666" y1="117.332" x2="58.666" y2="0">
<stop offset="0" style="stop-color:#0882C8"/>
<stop offset="1" style="stop-color:#26B6DB"/>
</linearGradient>
<circle fill="url(#SVGID_1_)" cx="58.7" cy="58.7" r="58.7"/>
<g>
<circle fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" cx="58.7" cy="58.5" r="43.7"/>
<g>
<path fill="#FFFFFF" d="M94.7,47.8c4.7,1.6,9.8-0.9,11.4-5.6c1.6-4.7-0.9-9.8-5.6-11.4c-4.7-1.6-9.8,0.9-11.4,5.6
C87.5,41.1,90,46.2,94.7,47.8z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="97.6" y1="39.4" x2="67.5" y2="64.4"/>
</g>
<g>
<path fill="#FFFFFF" d="M77.6,91c-0.4,4.9,3.2,9.3,8.2,9.8c5,0.4,9.3-3.2,9.8-8.2c0.4-4.9-3.2-9.3-8.2-9.8
C82.4,82.4,78,86,77.6,91z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="86.5" y1="91.8" x2="67.5" y2="64.4"/>
</g>
<path fill="#FFFFFF" d="M60,69.3c2.7,4.2,8.3,5.4,12.4,2.7c4.2-2.7,5.4-8.3,2.7-12.4c-2.7-4.2-8.3-5.4-12.4-2.7
C58.5,59.5,57.3,65.1,60,69.3z"/>
<g>
<path fill="#FFFFFF" d="M21.2,61.4c-4.3-2.5-9.8-1.1-12.3,3.1c-2.5,4.3-1.1,9.8,3.1,12.3c4.3,2.5,9.8,1.1,12.3-3.1
C26.8,69.5,25.4,64,21.2,61.4z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="16.6" y1="69.1" x2="67.5" y2="64.4"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs>
<linearGradient id="grad" gradientUnits="userSpaceOnUse" x1="8" y1="0" x2="8" y2="16">
<stop offset="0" style="stop-color:#26B6DB"/>
<stop offset="1" style="stop-color:#0882C8"/>
</linearGradient>
</defs>
<g id="syncthing-logo">
<circle id="outer" cx="8" cy="8" r="8" style="fill:url(#grad)"/>
<circle id="inner" cx="8" cy="7.9727402" r="5.9557071" style="fill:none;stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-l" x1="9.1993189" y1="8.776825" x2="2.262351" y2="9.4173737" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-tr" x1="9.1993189" y1="8.776825" x2="13.301533" y2="5.3696747" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-br" x1="9.1993189" y1="8.776825" x2="11.788756" y2="12.51107" style="stroke:#ffffff;stroke-width:0.81771719"/>
<circle id="node-c" cx="9.1993189" cy="8.776825" r="1.22" style="fill:#ffffff"/>
<circle id="node-l" cx="2.262351" cy="9.4173737" r="1.22" style="fill:#ffffff"/>
<circle id="node-tr" cx="13.301533" cy="5.3696747" r="1.22" style="fill:#ffffff"/>
<circle id="node-br" cx="11.788756" cy="12.51107" r="1.22" style="fill:#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs>
<linearGradient id="grad" gradientUnits="userSpaceOnUse" x1="8" y1="0" x2="8" y2="16">
<stop offset="0" style="stop-color:#888888"/>
<stop offset="1" style="stop-color:#5E6061"/>
</linearGradient>
</defs>
<g id="syncthing-logo">
<circle id="outer" cx="8" cy="8" r="8" style="fill:url(#grad)"/>
<circle id="inner" cx="8" cy="7.9727402" r="5.9557071" style="fill:none;stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-l" x1="9.1993189" y1="8.776825" x2="2.262351" y2="9.4173737" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-tr" x1="9.1993189" y1="8.776825" x2="13.301533" y2="5.3696747" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-br" x1="9.1993189" y1="8.776825" x2="11.788756" y2="12.51107" style="stroke:#ffffff;stroke-width:0.81771719"/>
<circle id="node-c" cx="9.1993189" cy="8.776825" r="1.22" style="fill:#ffffff"/>
<circle id="node-l" cx="2.262351" cy="9.4173737" r="1.22" style="fill:#ffffff"/>
<circle id="node-tr" cx="13.301533" cy="5.3696747" r="1.22" style="fill:#ffffff"/>
<circle id="node-br" cx="11.788756" cy="12.51107" r="1.22" style="fill:#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs>
<linearGradient id="grad" gradientUnits="userSpaceOnUse" x1="8" y1="0" x2="8" y2="16">
<stop offset="0" style="stop-color:#DB3C26"/>
<stop offset="1" style="stop-color:#C80828"/>
</linearGradient>
<mask id="bitemask" maskUnits="userSpaceOnUse">
<g>
<rect id="mask-bg" x="0" y="0" width="16" height="16" style="fill:#ffffff"/>
<circle id="mask-subtract" cx="11.5" cy="11.5" r="5.5" style="fill:#000000"/>
</g>
</mask>
</defs>
<g id="syncthing-logo" mask="url(#bitemask)">
<circle id="outer" cx="8" cy="8" r="8" style="fill:url(#grad)"/>
<circle id="inner" cx="8" cy="7.9727402" r="5.9557071" style="fill:none;stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-l" x1="9.1993189" y1="8.776825" x2="2.262351" y2="9.4173737" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-tr" x1="9.1993189" y1="8.776825" x2="13.301533" y2="5.3696747" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-br" x1="9.1993189" y1="8.776825" x2="11.788756" y2="12.51107" style="stroke:#ffffff;stroke-width:0.81771719"/>
<circle id="node-c" cx="9.1993189" cy="8.776825" r="1.22" style="fill:#ffffff"/>
<circle id="node-l" cx="2.262351" cy="9.4173737" r="1.22" style="fill:#ffffff"/>
<circle id="node-tr" cx="13.301533" cy="5.3696747" r="1.22" style="fill:#ffffff"/>
<circle id="node-br" cx="11.788756" cy="12.51107" r="1.22" style="fill:#ffffff"/>
</g>
<circle id="bubble" cx="11.5" cy="11.5" r="4.5" style="fill:#000000"/>
<g id="exclaim">
<rect id="exclaim-top" x="11" y="9" width="1" height="3" style="fill:#ffffff"/>
<rect id="exclaim-bottom" x="11" y="13" width="1" height="1" style="fill:#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs>
<linearGradient id="grad" gradientUnits="userSpaceOnUse" x1="8" y1="0" x2="8" y2="16">
<stop offset="0" style="stop-color:#26B6DB"/>
<stop offset="1" style="stop-color:#0882C8"/>
</linearGradient>
<mask id="bitemask" maskUnits="userSpaceOnUse">
<g>
<rect id="mask-bg" x="0" y="0" width="16" height="16" style="fill:#ffffff"/>
<circle id="mask-subtract" cx="11.5" cy="11.5" r="5.5" style="fill:#000000"/>
</g>
</mask>
</defs>
<g id="syncthing-logo" mask="url(#bitemask)">
<circle id="outer" cx="8" cy="8" r="8" style="fill:url(#grad)"/>
<circle id="inner" cx="8" cy="7.9727402" r="5.9557071" style="fill:none;stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-l" x1="9.1993189" y1="8.776825" x2="2.262351" y2="9.4173737" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-tr" x1="9.1993189" y1="8.776825" x2="13.301533" y2="5.3696747" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-br" x1="9.1993189" y1="8.776825" x2="11.788756" y2="12.51107" style="stroke:#ffffff;stroke-width:0.81771719"/>
<circle id="node-c" cx="9.1993189" cy="8.776825" r="1.22" style="fill:#ffffff"/>
<circle id="node-l" cx="2.262351" cy="9.4173737" r="1.22" style="fill:#ffffff"/>
<circle id="node-tr" cx="13.301533" cy="5.3696747" r="1.22" style="fill:#ffffff"/>
<circle id="node-br" cx="11.788756" cy="12.51107" r="1.22" style="fill:#ffffff"/>
</g>
<circle id="bubble" cx="11.5" cy="11.5" r="4.5" style="fill:#000000"/>
<g id="exclaim">
<rect id="exclaim-top" x="11" y="9" width="1" height="3" style="fill:#ffffff"/>
<rect id="exclaim-bottom" x="11" y="13" width="1" height="1" style="fill:#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs>
<linearGradient id="grad" gradientUnits="userSpaceOnUse" x1="8" y1="0" x2="8" y2="16">
<stop offset="0" style="stop-color:#26DBA4"/>
<stop offset="1" style="stop-color:#08C86F"/>
</linearGradient>
</defs>
<g id="syncthing-logo">
<circle id="outer" cx="8" cy="8" r="8" style="fill:url(#grad)"/>
<circle id="inner" cx="8" cy="7.9727402" r="5.9557071" style="fill:none;stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-l" x1="9.1993189" y1="8.776825" x2="2.262351" y2="9.4173737" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-tr" x1="9.1993189" y1="8.776825" x2="13.301533" y2="5.3696747" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-br" x1="9.1993189" y1="8.776825" x2="11.788756" y2="12.51107" style="stroke:#ffffff;stroke-width:0.81771719"/>
<circle id="node-c" cx="9.1993189" cy="8.776825" r="1.22" style="fill:#ffffff"/>
<circle id="node-l" cx="2.262351" cy="9.4173737" r="1.22" style="fill:#ffffff"/>
<circle id="node-tr" cx="13.301533" cy="5.3696747" r="1.22" style="fill:#ffffff"/>
<circle id="node-br" cx="11.788756" cy="12.51107" r="1.22" style="fill:#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs>
<linearGradient id="grad" gradientUnits="userSpaceOnUse" x1="8" y1="0" x2="8" y2="16">
<stop offset="0" style="stop-color:#26B6DB"/>
<stop offset="1" style="stop-color:#0882C8"/>
</linearGradient>
<mask id="bitemask" maskUnits="userSpaceOnUse">
<g>
<rect id="mask-bg" x="0" y="0" width="16" height="16" style="fill:#ffffff"/>
<circle id="mask-subtract" cx="11.5" cy="11.5" r="5.5" style="fill:#000000"/>
</g>
</mask>
</defs>
<g id="syncthing-logo" mask="url(#bitemask)">
<circle id="outer" cx="8" cy="8" r="8" style="fill:url(#grad)"/>
<circle id="inner" cx="8" cy="7.9727402" r="5.9557071" style="fill:none;stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-l" x1="9.1993189" y1="8.776825" x2="2.262351" y2="9.4173737" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-tr" x1="9.1993189" y1="8.776825" x2="13.301533" y2="5.3696747" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-br" x1="9.1993189" y1="8.776825" x2="11.788756" y2="12.51107" style="stroke:#ffffff;stroke-width:0.81771719"/>
<circle id="node-c" cx="9.1993189" cy="8.776825" r="1.22" style="fill:#ffffff"/>
<circle id="node-l" cx="2.262351" cy="9.4173737" r="1.22" style="fill:#ffffff"/>
<circle id="node-tr" cx="13.301533" cy="5.3696747" r="1.22" style="fill:#ffffff"/>
<circle id="node-br" cx="11.788756" cy="12.51107" r="1.22" style="fill:#ffffff"/>
</g>
<circle id="bubble" cx="11.5" cy="11.5" r="4.5" style="fill:#000000"/>
<g id="pause">
<rect id="pause-leftbar" x="10" y="9" width="1" height="5" style="fill:#ffffff"/>
<rect id="pause-rightbar" x="12" y="9" width="1" height="5" style="fill:#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs>
<linearGradient id="grad" gradientUnits="userSpaceOnUse" x1="8" y1="0" x2="8" y2="16">
<stop offset="0" style="stop-color:#26B6DB"/>
<stop offset="1" style="stop-color:#0882C8"/>
</linearGradient>
<mask id="bitemask" maskUnits="userSpaceOnUse">
<g>
<rect id="mask-bg" x="0" y="0" width="16" height="16" style="fill:#ffffff"/>
<circle id="mask-subtract" cx="11.5" cy="11.5" r="5.5" style="fill:#000000"/>
</g>
</mask>
</defs>
<g id="syncthing-logo" mask="url(#bitemask)">
<circle id="outer" cx="8" cy="8" r="8" style="fill:url(#grad)"/>
<circle id="inner" cx="8" cy="7.9727402" r="5.9557071" style="fill:none;stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-l" x1="9.1993189" y1="8.776825" x2="2.262351" y2="9.4173737" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-tr" x1="9.1993189" y1="8.776825" x2="13.301533" y2="5.3696747" style="stroke:#ffffff;stroke-width:0.81771719"/>
<line id="arm-br" x1="9.1993189" y1="8.776825" x2="11.788756" y2="12.51107" style="stroke:#ffffff;stroke-width:0.81771719"/>
<circle id="node-c" cx="9.1993189" cy="8.776825" r="1.22" style="fill:#ffffff"/>
<circle id="node-l" cx="2.262351" cy="9.4173737" r="1.22" style="fill:#ffffff"/>
<circle id="node-tr" cx="13.301533" cy="5.3696747" r="1.22" style="fill:#ffffff"/>
<circle id="node-br" cx="11.788756" cy="12.51107" r="1.22" style="fill:#ffffff"/>
</g>
<circle id="bubble" cx="11.5" cy="11.5" r="4.5" style="fill:#000000"/>
<g id="arrows" transform="rotate(0 11.5 11.5)">
<path id="arrow-left" d="m 11.5,14 0,-1 c -1.5,0 -1.5,0 -1.5,-2 l 1,0 -1.5,-2 -1.5,2 1,0 c 0,3 0,3 2.5,3 z" style="fill:#ffffff" />
<path id="arrow-right" d="m 11.5,9 0,1 c 1.5,0 1.5,0 1.5,2 l -1,0 1.5,2 1.5,-2 -1,0 C 14,9 14,9 11.5,9 Z" style="fill:#ffffff" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
resources/screenshots/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -0,0 +1,614 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE">
<context>
<name>Data::SyncthingConnection</name>
<message>
<location filename="../data/syncthingconnection.cpp" line="65"/>
<source>disconnected</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="67"/>
<source>connected</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="69"/>
<source>connected, notifications available</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="71"/>
<source>connected, paused</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="73"/>
<source>connected, synchronizing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="75"/>
<source>unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="302"/>
<source>Unable to parse Syncthing log: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="306"/>
<source>Unable to request system log: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="332"/>
<location filename="../data/syncthingconnection.cpp" line="415"/>
<source>Unable to parse Syncthing config: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="337"/>
<location filename="../data/syncthingconnection.cpp" line="420"/>
<source>Unable to request Syncthing config: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="455"/>
<source>Unable to parse Syncthing events: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="478"/>
<source>Unable to request Syncthing events: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="550"/>
<source>Unable to request rescan: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="566"/>
<source>Unable to request pause/resume: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="275"/>
<source>Unable to request QR-Code: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Data::SyncthingDeviceModel</name>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="56"/>
<location filename="../data/syncthingdevicemodel.cpp" line="82"/>
<source>ID</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="57"/>
<source>Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="83"/>
<source>Addresses</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="84"/>
<source>Compression</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="85"/>
<source>Certificate</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="86"/>
<source>Introducer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="95"/>
<source>none</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="96"/>
<source>yes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="96"/>
<source>no</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="115"/>
<source>Unknown status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="116"/>
<location filename="../data/syncthingdevicemodel.cpp" line="117"/>
<source>Idle</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="118"/>
<source>Synchronizing (%1 %)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="118"/>
<source>Synchronizing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="119"/>
<source>Paused</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="120"/>
<source>Out of sync</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Data::SyncthingDirectoryModel</name>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="58"/>
<location filename="../data/syncthingdirectorymodel.cpp" line="84"/>
<source>ID</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="59"/>
<source>Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="85"/>
<source>Path</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="86"/>
<source>Devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="88"/>
<source>Rescan interval</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="97"/>
<source>yes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="97"/>
<source>no</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="118"/>
<source>Idle</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="119"/>
<source>Scanning (%1 %)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="120"/>
<source>Synchronizing (%1 %)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="122"/>
<source>Out of sync</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="119"/>
<source>Scanning</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="87"/>
<source>Read-only</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="117"/>
<source>Unknown status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="120"/>
<source>Synchronizing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="121"/>
<source>Paused</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::ConnectionOptionPage</name>
<message>
<location filename="../gui/connectionoptionpage.ui" line="14"/>
<source>Connection</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="20"/>
<source>Syncthing URL</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="30"/>
<source>Authentication</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="43"/>
<source>User</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="84"/>
<source>disconnected</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="91"/>
<source>Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="98"/>
<source>Apply connection settings and try to reconnect</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="112"/>
<source>API key</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="53"/>
<source>Password</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::DevView</name>
<message>
<location filename="../gui/devview.cpp" line="25"/>
<source>Copy</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::DirView</name>
<message>
<location filename="../gui/dirview.cpp" line="44"/>
<source>Copy</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::LauncherOptionPage</name>
<message>
<location filename="../gui/launcheroptionpage.ui" line="14"/>
<source>Launcher</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::NotificationsOptionPage</name>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="6"/>
<source>Notifications</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="12"/>
<source>Notify on disconnect</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="19"/>
<source>Notify on errors</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="26"/>
<source>Notify on sync complete</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="33"/>
<source>Show Syncthing notifications</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::SettingsDialog</name>
<message>
<location filename="../gui/settingsdialog.cpp" line="187"/>
<source>Tray</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/settingsdialog.cpp" line="195"/>
<source>Web view</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::TrayIcon</name>
<message>
<location filename="../gui/tray.cpp" line="333"/>
<source>Web UI</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="334"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="335"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="337"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="389"/>
<source>Syncthing error</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="396"/>
<source>Syncthing notification</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="405"/>
<source>Not connected to Syncthing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="407"/>
<source>Disconnected from Syncthing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="412"/>
<source>Syncthing is running</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="416"/>
<source>Notifications available</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="420"/>
<source>Syncthing has been suspended</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="424"/>
<source>Synchronization is ongoing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="433"/>
<source>Synchronization complete</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::TrayWidget</name>
<message>
<location filename="../gui/traywidget.ui" line="14"/>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="62"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="103"/>
<location filename="../gui/tray.cpp" line="233"/>
<source>Connect</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="178"/>
<source>Directories</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="207"/>
<source>Devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="76"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="130"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="144"/>
<source>Web UI</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="78"/>
<source>View own device ID</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="115"/>
<source>Settings - Syncthing tray</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="134"/>
<source>Tray application for Syncthing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="90"/>
<source>Rescan all directories</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="84"/>
<source>Show Syncthing log</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="135"/>
<source>About - Syncthing Tray</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="173"/>
<source>Own device ID - Syncthing Tray</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="183"/>
<source>device ID is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="191"/>
<source>Copy to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="207"/>
<source>Log - Syncthing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="234"/>
<source>Not connected to Syncthing, click to connect</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="240"/>
<source>Pause</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="241"/>
<source>Syncthing is doing its job, click to pause</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="245"/>
<source>Continue</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="246"/>
<source>Syncthing is suspended, click to continue</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="270"/>
<source>The directly &lt;i&gt;%1&lt;/i&gt; does not exist on the local machine.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::WebViewDialog</name>
<message>
<location filename="../gui/webviewdialog.cpp" line="26"/>
<source>Syncthing</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::WebViewOptionPage</name>
<message>
<location filename="../gui/webviewoptionpage.ui" line="14"/>
<location filename="../gui/settingsdialog.cpp" line="149"/>
<source>General</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="20"/>
<source>Usage</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="27"/>
<source>Disable web view (open regular web browser instead)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="64"/>
<source>Keep web view running when currently not shown</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="34"/>
<source>Zoom factor</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="57"/>
<source>Hiding</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/settingsdialog.cpp" line="151"/>
<source>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.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../application/main.cpp" line="50"/>
<source>The system tray is (currently) not available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../application/main.cpp" line="54"/>
<source>The Qt libraries have not been built with tray icon support.</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,614 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US">
<context>
<name>Data::SyncthingConnection</name>
<message>
<location filename="../data/syncthingconnection.cpp" line="65"/>
<source>disconnected</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="67"/>
<source>connected</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="69"/>
<source>connected, notifications available</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="71"/>
<source>connected, paused</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="73"/>
<source>connected, synchronizing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="75"/>
<source>unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="302"/>
<source>Unable to parse Syncthing log: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="306"/>
<source>Unable to request system log: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="332"/>
<location filename="../data/syncthingconnection.cpp" line="415"/>
<source>Unable to parse Syncthing config: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="337"/>
<location filename="../data/syncthingconnection.cpp" line="420"/>
<source>Unable to request Syncthing config: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="455"/>
<source>Unable to parse Syncthing events: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="478"/>
<source>Unable to request Syncthing events: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="550"/>
<source>Unable to request rescan: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="566"/>
<source>Unable to request pause/resume: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingconnection.cpp" line="275"/>
<source>Unable to request QR-Code: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Data::SyncthingDeviceModel</name>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="56"/>
<location filename="../data/syncthingdevicemodel.cpp" line="82"/>
<source>ID</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="57"/>
<source>Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="83"/>
<source>Addresses</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="84"/>
<source>Compression</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="85"/>
<source>Certificate</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="86"/>
<source>Introducer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="95"/>
<source>none</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="96"/>
<source>yes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="96"/>
<source>no</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="115"/>
<source>Unknown status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="116"/>
<location filename="../data/syncthingdevicemodel.cpp" line="117"/>
<source>Idle</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="118"/>
<source>Synchronizing (%1 %)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="118"/>
<source>Synchronizing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="119"/>
<source>Paused</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdevicemodel.cpp" line="120"/>
<source>Out of sync</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Data::SyncthingDirectoryModel</name>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="58"/>
<location filename="../data/syncthingdirectorymodel.cpp" line="84"/>
<source>ID</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="59"/>
<source>Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="85"/>
<source>Path</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="86"/>
<source>Devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="88"/>
<source>Rescan interval</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="97"/>
<source>yes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="97"/>
<source>no</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="118"/>
<source>Idle</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="119"/>
<source>Scanning (%1 %)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="120"/>
<source>Synchronizing (%1 %)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="122"/>
<source>Out of sync</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="119"/>
<source>Scanning</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="87"/>
<source>Read-only</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="117"/>
<source>Unknown status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="120"/>
<source>Synchronizing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../data/syncthingdirectorymodel.cpp" line="121"/>
<source>Paused</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::ConnectionOptionPage</name>
<message>
<location filename="../gui/connectionoptionpage.ui" line="14"/>
<source>Connection</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="20"/>
<source>Syncthing URL</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="30"/>
<source>Authentication</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="43"/>
<source>User</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="84"/>
<source>disconnected</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="91"/>
<source>Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="98"/>
<source>Apply connection settings and try to reconnect</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="112"/>
<source>API key</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/connectionoptionpage.ui" line="53"/>
<source>Password</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::DevView</name>
<message>
<location filename="../gui/devview.cpp" line="25"/>
<source>Copy</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::DirView</name>
<message>
<location filename="../gui/dirview.cpp" line="44"/>
<source>Copy</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::LauncherOptionPage</name>
<message>
<location filename="../gui/launcheroptionpage.ui" line="14"/>
<source>Launcher</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::NotificationsOptionPage</name>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="6"/>
<source>Notifications</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="12"/>
<source>Notify on disconnect</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="19"/>
<source>Notify on errors</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="26"/>
<source>Notify on sync complete</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/notificationsoptionpage.ui" line="33"/>
<source>Show Syncthing notifications</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::SettingsDialog</name>
<message>
<location filename="../gui/settingsdialog.cpp" line="187"/>
<source>Tray</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/settingsdialog.cpp" line="195"/>
<source>Web view</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::TrayIcon</name>
<message>
<location filename="../gui/tray.cpp" line="333"/>
<source>Web UI</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="334"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="335"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="337"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="389"/>
<source>Syncthing error</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="396"/>
<source>Syncthing notification</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="405"/>
<source>Not connected to Syncthing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="407"/>
<source>Disconnected from Syncthing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="412"/>
<source>Syncthing is running</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="416"/>
<source>Notifications available</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="420"/>
<source>Syncthing has been suspended</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="424"/>
<source>Synchronization is ongoing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="433"/>
<source>Synchronization complete</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::TrayWidget</name>
<message>
<location filename="../gui/traywidget.ui" line="14"/>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="62"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="103"/>
<location filename="../gui/tray.cpp" line="233"/>
<source>Connect</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="178"/>
<source>Directories</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="207"/>
<source>Devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="76"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="130"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/traywidget.ui" line="144"/>
<source>Web UI</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="78"/>
<source>View own device ID</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="115"/>
<source>Settings - Syncthing tray</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="134"/>
<source>Tray application for Syncthing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="90"/>
<source>Rescan all directories</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="84"/>
<source>Show Syncthing log</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="135"/>
<source>About - Syncthing Tray</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="173"/>
<source>Own device ID - Syncthing Tray</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="183"/>
<source>device ID is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="191"/>
<source>Copy to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="207"/>
<source>Log - Syncthing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="234"/>
<source>Not connected to Syncthing, click to connect</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="240"/>
<source>Pause</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="241"/>
<source>Syncthing is doing its job, click to pause</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="245"/>
<source>Continue</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="246"/>
<source>Syncthing is suspended, click to continue</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/tray.cpp" line="270"/>
<source>The directly &lt;i&gt;%1&lt;/i&gt; does not exist on the local machine.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::WebViewDialog</name>
<message>
<location filename="../gui/webviewdialog.cpp" line="26"/>
<source>Syncthing</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::WebViewOptionPage</name>
<message>
<location filename="../gui/webviewoptionpage.ui" line="14"/>
<location filename="../gui/settingsdialog.cpp" line="149"/>
<source>General</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="20"/>
<source>Usage</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="27"/>
<source>Disable web view (open regular web browser instead)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="64"/>
<source>Keep web view running when currently not shown</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="34"/>
<source>Zoom factor</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/webviewoptionpage.ui" line="57"/>
<source>Hiding</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/settingsdialog.cpp" line="151"/>
<source>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.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../application/main.cpp" line="50"/>
<source>The system tray is (currently) not available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../application/main.cpp" line="54"/>
<source>The Qt libraries have not been built with tray icon support.</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>