Just deal with it like with the paused flag. This will hopefully solve the issue that dirs are wrongly shown as unshared till the next status update.
458 lines
18 KiB
C++
458 lines
18 KiB
C++
#include "./syncthingfileitemaction.h"
|
|
#include "../model/syncthingicons.h"
|
|
|
|
#include "../connector/syncthingconfig.h"
|
|
#include "../connector/syncthingconnectionsettings.h"
|
|
#include "../connector/syncthingdir.h"
|
|
#include "../connector/utils.h"
|
|
|
|
#include <qtutilities/aboutdialog/aboutdialog.h>
|
|
#include <qtutilities/resources/resources.h>
|
|
|
|
#include <c++utilities/application/argumentparser.h>
|
|
|
|
#include <KFileItem>
|
|
#include <KPluginFactory>
|
|
#include <KPluginLoader>
|
|
|
|
#include <QAction>
|
|
#include <QDir>
|
|
#include <QEvent>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QWidget>
|
|
|
|
#include <functional>
|
|
#include <iostream>
|
|
|
|
#include "resources/config.h"
|
|
|
|
using namespace std;
|
|
using namespace Dialogs;
|
|
using namespace Data;
|
|
|
|
K_PLUGIN_FACTORY(SyncthingFileItemActionFactory, registerPlugin<SyncthingFileItemAction>();)
|
|
|
|
struct SyncthingItem {
|
|
SyncthingItem(const SyncthingDir *dir, const QString &path);
|
|
const SyncthingDir *dir;
|
|
QString path;
|
|
QString name;
|
|
};
|
|
|
|
SyncthingItem::SyncthingItem(const SyncthingDir *dir, const QString &path)
|
|
: dir(dir)
|
|
, path(path)
|
|
{
|
|
int lastSep = path.lastIndexOf(QChar('/'));
|
|
if (lastSep > 0) {
|
|
name = path.mid(lastSep + 1);
|
|
} else {
|
|
name = path;
|
|
}
|
|
}
|
|
|
|
SyncthingMenuAction::SyncthingMenuAction(const KFileItemListProperties &properties, const QList<QAction *> &actions, QWidget *parentWidget)
|
|
: QAction(parentWidget)
|
|
, m_properties(properties)
|
|
{
|
|
if (!actions.isEmpty()) {
|
|
auto *menu = new QMenu(parentWidget);
|
|
menu->addActions(actions);
|
|
setMenu(menu);
|
|
}
|
|
updateStatus(SyncthingFileItemAction::connection().status());
|
|
}
|
|
|
|
void SyncthingMenuAction::updateStatus(SyncthingStatus status)
|
|
{
|
|
if (status != SyncthingStatus::Disconnected && status != SyncthingStatus::Reconnecting && status != SyncthingStatus::BeingDestroyed) {
|
|
setText(tr("Syncthing"));
|
|
setIcon(statusIcons().scanninig);
|
|
if (!menu()) {
|
|
const QList<QAction *> actions = SyncthingFileItemAction::createActions(m_properties, parentWidget());
|
|
if (!actions.isEmpty()) {
|
|
auto *menu = new QMenu(parentWidget());
|
|
menu->addActions(actions);
|
|
setMenu(menu);
|
|
}
|
|
}
|
|
} else {
|
|
if (status != SyncthingStatus::Reconnecting) {
|
|
SyncthingFileItemAction::connection().connect();
|
|
}
|
|
setText(tr("Syncthing - connecting"));
|
|
setIcon(statusIcons().disconnected);
|
|
if (QMenu *menu = this->menu()) {
|
|
setMenu(nullptr);
|
|
delete menu;
|
|
}
|
|
}
|
|
}
|
|
|
|
SyncthingInfoAction::SyncthingInfoAction(QObject *parent)
|
|
: QWidgetAction(parent)
|
|
{
|
|
}
|
|
|
|
QWidget *SyncthingInfoAction::createWidget(QWidget *parent)
|
|
{
|
|
auto *container = new QWidget(parent);
|
|
auto *layout = new QHBoxLayout(parent);
|
|
layout->setMargin(4);
|
|
layout->setSpacing(5);
|
|
auto *iconLabel = new QLabel(parent);
|
|
iconLabel->setPixmap(icon().pixmap(16));
|
|
iconLabel->setFixedWidth(16);
|
|
iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum);
|
|
layout->addWidget(iconLabel);
|
|
auto *textLabel = new QLabel(text(), parent);
|
|
layout->addWidget(textLabel);
|
|
container->setLayout(layout);
|
|
return container;
|
|
}
|
|
|
|
SyncthingDirActions::SyncthingDirActions(const SyncthingDir &dir, QObject *parent)
|
|
: QObject(parent)
|
|
, m_dirId(dir.id)
|
|
{
|
|
m_infoAction.setSeparator(true);
|
|
updateStatus(dir);
|
|
}
|
|
|
|
void SyncthingDirActions::updateStatus(const std::vector<SyncthingDir> &dirs)
|
|
{
|
|
for (const SyncthingDir &dir : dirs) {
|
|
if (updateStatus(dir)) {
|
|
return;
|
|
}
|
|
}
|
|
m_statusAction.setText(tr("Status: not available anymore"));
|
|
m_statusAction.setIcon(statusIcons().disconnected);
|
|
}
|
|
|
|
bool SyncthingDirActions::updateStatus(const SyncthingDir &dir)
|
|
{
|
|
if (dir.id != m_dirId) {
|
|
return false;
|
|
}
|
|
m_infoAction.setText(tr("Directory info for %1").arg(dir.displayName()));
|
|
m_infoAction.setIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
|
|
m_statusAction.setText(tr("Status: ") + dir.statusString());
|
|
if (dir.paused && dir.status != SyncthingDirStatus::OutOfSync) {
|
|
m_statusAction.setIcon(statusIcons().pause);
|
|
} else if (dir.isUnshared()) {
|
|
m_statusAction.setIcon(statusIcons().disconnected);
|
|
} else {
|
|
switch (dir.status) {
|
|
case SyncthingDirStatus::Unknown:
|
|
m_statusAction.setIcon(statusIcons().disconnected);
|
|
break;
|
|
case SyncthingDirStatus::Idle:
|
|
m_statusAction.setIcon(statusIcons().idling);
|
|
break;
|
|
case SyncthingDirStatus::Scanning:
|
|
m_statusAction.setIcon(statusIcons().scanninig);
|
|
break;
|
|
case SyncthingDirStatus::Synchronizing:
|
|
m_statusAction.setIcon(statusIcons().sync);
|
|
break;
|
|
case SyncthingDirStatus::OutOfSync:
|
|
m_statusAction.setIcon(statusIcons().error);
|
|
break;
|
|
}
|
|
}
|
|
m_globalStatusAction.setText(tr("Global: ") + directoryStatusString(dir.globalStats));
|
|
m_localStatusAction.setText(tr("Local: ") + directoryStatusString(dir.localStats));
|
|
m_lastScanAction.setText(tr("Last scan time: ") + agoString(dir.lastScanTime));
|
|
m_lastScanAction.setIcon(QIcon::fromTheme(QStringLiteral("accept_time_event")));
|
|
m_rescanIntervalAction.setText(tr("Rescan interval: %1 seconds").arg(dir.rescanInterval));
|
|
if (dir.itemErrors.empty()) {
|
|
m_errorsAction.setVisible(false);
|
|
} else {
|
|
m_errorsAction.setVisible(true);
|
|
m_errorsAction.setIcon(QIcon::fromTheme(QStringLiteral("dialog-error")));
|
|
m_errorsAction.setText(tr("%1 item(s) out-of-sync", nullptr, trQuandity(dir.itemErrors.size())).arg(dir.itemErrors.size()));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QList<QAction *> &operator<<(QList<QAction *> &actions, SyncthingDirActions &dirActions)
|
|
{
|
|
return actions << &dirActions.m_infoAction << &dirActions.m_statusAction << &dirActions.m_globalStatusAction << &dirActions.m_localStatusAction
|
|
<< &dirActions.m_lastScanAction << &dirActions.m_rescanIntervalAction << &dirActions.m_errorsAction;
|
|
}
|
|
|
|
SyncthingConnection SyncthingFileItemAction::s_connection;
|
|
|
|
SyncthingFileItemAction::SyncthingFileItemAction(QObject *parent, const QVariantList &)
|
|
: KAbstractFileItemActionPlugin(parent)
|
|
{
|
|
if (s_connection.apiKey().isEmpty()) {
|
|
// first initialization: load translations, determine config, establish connection
|
|
|
|
LOAD_QT_TRANSLATIONS;
|
|
|
|
// determine path of Syncthing config file
|
|
const QByteArray configPathFromEnv(qgetenv("KIO_SYNCTHING_CONFIG_PATH"));
|
|
const QString configPath = !configPathFromEnv.isEmpty() ? QString::fromLocal8Bit(configPathFromEnv) : SyncthingConfig::locateConfigFile();
|
|
if (configPath.isEmpty()) {
|
|
cerr << "Unable to determine location of Syncthing config. Set KIO_SYNCTHING_CONFIG_PATH to specify location." << endl;
|
|
return;
|
|
}
|
|
|
|
// load Syncthing config
|
|
SyncthingConfig config;
|
|
if (!config.restore(configPath)) {
|
|
cerr << "Unable to load Syncthing config from \"" << configPath.toLocal8Bit().data() << "\"" << endl;
|
|
if (configPathFromEnv.isEmpty()) {
|
|
cerr << "Note: Set KIO_SYNCTHING_CONFIG_PATH to specify config file explicitely." << endl;
|
|
}
|
|
return;
|
|
}
|
|
cerr << "Syncthing config loaded from \"" << configPath.toLocal8Bit().data() << "\"" << endl;
|
|
SyncthingConnectionSettings settings;
|
|
settings.syncthingUrl = config.syncthingUrl();
|
|
settings.apiKey.append(config.guiApiKey);
|
|
|
|
// establish connection
|
|
bool ok;
|
|
int reconnectInterval = qEnvironmentVariableIntValue("KIO_SYNCTHING_RECONNECT_INTERVAL", &ok);
|
|
if (!ok || reconnectInterval < 0) {
|
|
reconnectInterval = 10000;
|
|
}
|
|
s_connection.setAutoReconnectInterval(reconnectInterval);
|
|
s_connection.reconnect(settings);
|
|
connect(&s_connection, &SyncthingConnection::error, &SyncthingFileItemAction::logConnectionError);
|
|
connect(&s_connection, &SyncthingConnection::statusChanged, &SyncthingFileItemAction::logConnectionStatus);
|
|
}
|
|
}
|
|
|
|
QList<QAction *> SyncthingFileItemAction::actions(const KFileItemListProperties &fileItemInfo, QWidget *parentWidget)
|
|
{
|
|
// handle case when not connected yet
|
|
if (!s_connection.isConnected()) {
|
|
s_connection.connect();
|
|
auto *menuAction = new SyncthingMenuAction(fileItemInfo, QList<QAction *>(), parentWidget);
|
|
connect(&s_connection, &SyncthingConnection::statusChanged, menuAction, &SyncthingMenuAction::updateStatus);
|
|
return QList<QAction *>() << menuAction;
|
|
}
|
|
|
|
const QList<QAction *> actions = createActions(fileItemInfo, parentWidget);
|
|
// don't show anything if no relevant actions could be determined
|
|
if (actions.isEmpty()) {
|
|
return actions;
|
|
}
|
|
|
|
return QList<QAction *>() << new SyncthingMenuAction(fileItemInfo, actions, parentWidget);
|
|
}
|
|
|
|
SyncthingConnection &SyncthingFileItemAction::connection()
|
|
{
|
|
return s_connection;
|
|
}
|
|
|
|
void SyncthingFileItemAction::logConnectionStatus()
|
|
{
|
|
cerr << "Syncthing connection status changed to: " << s_connection.statusText().toLocal8Bit().data() << endl;
|
|
}
|
|
|
|
void SyncthingFileItemAction::logConnectionError(const QString &errorMessage, SyncthingErrorCategory errorCategory)
|
|
{
|
|
switch (errorCategory) {
|
|
case SyncthingErrorCategory::Parsing:
|
|
case SyncthingErrorCategory::SpecificRequest:
|
|
QMessageBox::critical(nullptr, tr("Syncthing connection error"), errorMessage);
|
|
break;
|
|
default:
|
|
cerr << "Syncthing connection error: " << errorMessage.toLocal8Bit().data() << endl;
|
|
}
|
|
}
|
|
|
|
void SyncthingFileItemAction::rescanDir(const QString &dirId, const QString &relpath)
|
|
{
|
|
s_connection.rescan(dirId, relpath);
|
|
}
|
|
|
|
void SyncthingFileItemAction::showAboutDialog()
|
|
{
|
|
auto *aboutDialog = new AboutDialog(nullptr, QStringLiteral(APP_NAME), QStringLiteral(APP_AUTHOR "\nSyncthing icons from Syncthing project"),
|
|
QStringLiteral(APP_VERSION), ApplicationUtilities::dependencyVersions2, QStringLiteral(APP_URL), QStringLiteral(APP_DESCRIPTION),
|
|
QImage(statusIcons().scanninig.pixmap(128).toImage()));
|
|
aboutDialog->setWindowTitle(tr("About") + QStringLiteral(" - " APP_NAME));
|
|
aboutDialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("syncthingtray")));
|
|
aboutDialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
aboutDialog->show();
|
|
}
|
|
|
|
QList<QAction *> SyncthingFileItemAction::createActions(const KFileItemListProperties &fileItemInfo, QWidget *parentWidget)
|
|
{
|
|
QList<QAction *> actions;
|
|
|
|
// check whether any directories are known
|
|
const auto &dirs = s_connection.dirInfo();
|
|
if (dirs.empty()) {
|
|
return actions;
|
|
}
|
|
|
|
// get all paths
|
|
QStringList paths;
|
|
paths.reserve(fileItemInfo.items().size());
|
|
for (const KFileItem &item : fileItemInfo.items()) {
|
|
if (!item.isLocalFile()) {
|
|
// don't show any actions when remote files are selected
|
|
return QList<QAction *>();
|
|
}
|
|
paths << item.localPath();
|
|
}
|
|
|
|
// determine relevant Syncthing dirs
|
|
QList<const SyncthingDir *> detectedDirs;
|
|
QList<const SyncthingDir *> containingDirs;
|
|
QList<SyncthingItem> detectedItems;
|
|
const SyncthingDir *lastDir;
|
|
for (const SyncthingDir &dir : dirs) {
|
|
QStringRef dirPath(dir.pathWithoutTrailingSlash());
|
|
for (const QString &path : paths) {
|
|
if (path == dirPath) {
|
|
lastDir = &dir;
|
|
if (!detectedDirs.contains(lastDir)) {
|
|
detectedDirs << lastDir;
|
|
}
|
|
} else if (path.startsWith(dir.path)) {
|
|
detectedItems << SyncthingItem(&dir, path.mid(dir.path.size()));
|
|
lastDir = &dir;
|
|
if (!containingDirs.contains(lastDir)) {
|
|
containingDirs << lastDir;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// add actions for the selected items itself
|
|
if (!detectedItems.isEmpty()) {
|
|
actions << new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")),
|
|
detectedItems.size() == 1 ? tr("Rescan %1 (in %2)").arg(detectedItems.front().name, detectedItems.front().dir->displayName())
|
|
: tr("Rescan selected items"),
|
|
parentWidget);
|
|
if (s_connection.isConnected()) {
|
|
for (const SyncthingItem &item : detectedItems) {
|
|
connect(actions.back(), &QAction::triggered, bind(&SyncthingFileItemAction::rescanDir, item.dir->id, item.path));
|
|
}
|
|
} else {
|
|
actions.back()->setEnabled(false);
|
|
}
|
|
}
|
|
|
|
// add actions for explicitely selected Syncthing dirs
|
|
if (!detectedDirs.isEmpty()) {
|
|
// rescan item
|
|
actions << new QAction(QIcon::fromTheme(QStringLiteral("folder-sync")),
|
|
detectedDirs.size() == 1 ? tr("Rescan %1").arg(detectedDirs.front()->displayName()) : tr("Rescan selected directories"), parentWidget);
|
|
if (s_connection.isConnected()) {
|
|
for (const SyncthingDir *dir : detectedDirs) {
|
|
connect(actions.back(), &QAction::triggered, bind(&SyncthingFileItemAction::rescanDir, dir->id, QString()));
|
|
containingDirs.removeAll(dir);
|
|
}
|
|
} else {
|
|
actions.back()->setEnabled(false);
|
|
}
|
|
|
|
// pause/resume item
|
|
QStringList ids;
|
|
ids.reserve(detectedDirs.size());
|
|
bool isPaused = false;
|
|
for (const SyncthingDir *dir : detectedDirs) {
|
|
ids << dir->id;
|
|
if (dir->paused) {
|
|
isPaused = true;
|
|
break;
|
|
}
|
|
}
|
|
if (isPaused) {
|
|
actions << new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")),
|
|
detectedDirs.size() == 1 ? tr("Resume %1").arg(detectedDirs.front()->displayName()) : tr("Resume selected directories"),
|
|
parentWidget);
|
|
} else {
|
|
actions << new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")),
|
|
detectedDirs.size() == 1 ? tr("Pause %1").arg(detectedDirs.front()->displayName()) : tr("Pause selected directories"), parentWidget);
|
|
}
|
|
if (s_connection.isConnected()) {
|
|
connect(actions.back(), &QAction::triggered,
|
|
bind(isPaused ? &SyncthingConnection::resumeDirectories : &SyncthingConnection::pauseDirectories, &s_connection, ids));
|
|
} else {
|
|
actions.back()->setEnabled(false);
|
|
}
|
|
}
|
|
|
|
// add actions for the Syncthing dirs containing selected items
|
|
if (!containingDirs.isEmpty()) {
|
|
// rescan item
|
|
actions << new QAction(QIcon::fromTheme(QStringLiteral("folder-sync")),
|
|
containingDirs.size() == 1 ? tr("Rescan %1").arg(containingDirs.front()->displayName()) : tr("Rescan containing directories"),
|
|
parentWidget);
|
|
if (s_connection.isConnected()) {
|
|
for (const SyncthingDir *dir : containingDirs) {
|
|
connect(actions.back(), &QAction::triggered, bind(&SyncthingFileItemAction::rescanDir, dir->id, QString()));
|
|
}
|
|
} else {
|
|
actions.back()->setEnabled(false);
|
|
}
|
|
|
|
// pause/resume item
|
|
QStringList ids;
|
|
ids.reserve(containingDirs.size());
|
|
bool isPaused = false;
|
|
for (const SyncthingDir *dir : containingDirs) {
|
|
ids << dir->id;
|
|
if (dir->paused) {
|
|
isPaused = true;
|
|
break;
|
|
}
|
|
}
|
|
if (isPaused) {
|
|
actions << new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")),
|
|
containingDirs.size() == 1 ? tr("Resume %1").arg(containingDirs.front()->displayName()) : tr("Resume containing directories"),
|
|
parentWidget);
|
|
} else {
|
|
actions << new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")),
|
|
containingDirs.size() == 1 ? tr("Pause %1").arg(containingDirs.front()->displayName()) : tr("Pause containing directories"),
|
|
parentWidget);
|
|
}
|
|
if (s_connection.isConnected()) {
|
|
connect(actions.back(), &QAction::triggered,
|
|
bind(isPaused ? &SyncthingConnection::resumeDirectories : &SyncthingConnection::pauseDirectories, &s_connection, ids));
|
|
} else {
|
|
actions.back()->setEnabled(false);
|
|
}
|
|
}
|
|
|
|
// don't add any further actions if no relevant actions could be determined so far
|
|
if (actions.isEmpty()) {
|
|
return actions;
|
|
}
|
|
|
|
// add actions to show further information about directory if the selection is only about one particular Syncthing dir
|
|
if (detectedDirs.size() + containingDirs.size() == 1) {
|
|
auto *statusActions = new SyncthingDirActions(*lastDir, parentWidget);
|
|
connect(&s_connection, &SyncthingConnection::newDirs, statusActions,
|
|
static_cast<void (SyncthingDirActions::*)(const vector<SyncthingDir> &)>(&SyncthingDirActions::updateStatus));
|
|
connect(&s_connection, &SyncthingConnection::dirStatusChanged, statusActions,
|
|
static_cast<bool (SyncthingDirActions::*)(const SyncthingDir &)>(&SyncthingDirActions::updateStatus));
|
|
actions << *statusActions;
|
|
}
|
|
|
|
// about about action
|
|
QAction *separator = new QAction(parentWidget);
|
|
separator->setSeparator(true);
|
|
QAction *aboutAction = new QAction(QIcon::fromTheme(QStringLiteral("help-about")), tr("About"));
|
|
connect(aboutAction, &QAction::triggered, &SyncthingFileItemAction::showAboutDialog);
|
|
actions << separator << aboutAction;
|
|
|
|
return actions;
|
|
}
|
|
|
|
#include <syncthingfileitemaction.moc>
|