diff --git a/CMakeLists.txt b/CMakeLists.txt index 886f017..4b7c3a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ project(${META_PROJECT_NAME}) # options for partial build option(NO_CLI "specifies whether building CLI should be skipped" OFF) option(NO_TRAY "specifies whether building the tray should be skipped" OFF) +option(NO_FILE_ITEM_ACTION_PLUGIN "specifies whether building the file item action plugin should be skipped" OFF) option(NO_MODEL "specifies whether building models should be skipped, implies NO_TRAY" OFF) # add subdirectories @@ -32,3 +33,6 @@ if(NOT NO_MODEL) add_subdirectory(tray) endif() endif() +if(NOT NO_FILE_ITEM_ACTION_PLUGIN) + add_subdirectory(fileitemactionplugin) +endif() diff --git a/fileitemactionplugin/CMakeLists.txt b/fileitemactionplugin/CMakeLists.txt new file mode 100644 index 0000000..e32c1ad --- /dev/null +++ b/fileitemactionplugin/CMakeLists.txt @@ -0,0 +1,43 @@ +# metadata +set(META_PROJECT_NAME syncthingfileitemaction) +set(META_APP_NAME "Syncthing KIO-Plugin") +set(META_APP_DESCRIPTION "KIO file item action for Syncthing") +set(META_PROJECT_TYPE qtplugin) + +# add project files +set(HEADER_FILES + syncthingfileitemaction.h +) +set(SRC_FILES + syncthingfileitemaction.cpp +) + +# find c++utilities +find_package(c++utilities 4.6.0 REQUIRED) +list(APPEND CMAKE_MODULE_PATH ${CPP_UTILITIES_MODULE_DIRS}) + +# find qtutilities +find_package(qtutilities 5.0.0 REQUIRED) +use_qt_utilities() +#list(APPEND CMAKE_MODULE_PATH ${QT_UTILITIES_MODULE_DIRS}) + +# find backend libraries +find_package(syncthingconnector ${META_APP_VERSION} REQUIRED) +use_syncthingconnector() +find_package(syncthingmodel ${META_APP_VERSION} REQUIRED) +use_syncthingmodel() + +# link also explicitely against the following Qt 5 modules +list(APPEND ADDITIONAL_QT_MODULES Network) +list(APPEND ADDITIONAL_KF_MODULES KIO) + +# include modules to apply configuration +include(BasicConfig) +include(QtGuiConfig) +include(QtConfig) +include(WindowsResources) +# KIO libs can not be determined automatically +list(REMOVE_ITEM PRIVATE_LIBRARIES KF5::KIO) +list(APPEND PRIVATE_LIBRARIES KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::KIONTLM) +include(LibraryTarget) +include(ConfigHeader) diff --git a/fileitemactionplugin/global.h b/fileitemactionplugin/global.h new file mode 100644 index 0000000..8626762 --- /dev/null +++ b/fileitemactionplugin/global.h @@ -0,0 +1,27 @@ +// Created via CMake from template global.h.in +// WARNING! Any changes to this file will be overwritten by the next CMake run! + +#ifndef SYNCTHINGFILEITEMACTION_GLOBAL +#define SYNCTHINGFILEITEMACTION_GLOBAL + +#include + +#ifdef SYNCTHINGFILEITEMACTION_STATIC +# define SYNCTHINGFILEITEMACTION_EXPORT +# define SYNCTHINGFILEITEMACTION_IMPORT +#else +# define SYNCTHINGFILEITEMACTION_EXPORT LIB_EXPORT +# define SYNCTHINGFILEITEMACTION_IMPORT LIB_IMPORT +#endif + +/*! + * \def SYNCTHINGFILEITEMACTION_EXPORT + * \brief Marks the symbol to be exported by the syncthingfileitemaction library. + */ + +/*! + * \def SYNCTHINGFILEITEMACTION_IMPORT + * \brief Marks the symbol to be imported from the syncthingfileitemaction library. + */ + +#endif // SYNCTHINGFILEITEMACTION_GLOBAL diff --git a/fileitemactionplugin/syncthingfileitemaction.cpp b/fileitemactionplugin/syncthingfileitemaction.cpp new file mode 100644 index 0000000..1d4ca76 --- /dev/null +++ b/fileitemactionplugin/syncthingfileitemaction.cpp @@ -0,0 +1,264 @@ +#include "./syncthingfileitemaction.h" +#include "../model/syncthingicons.h" + +#include "../connector/syncthingconfig.h" +#include "../connector/syncthingconnectionsettings.h" +#include "../connector/syncthingdir.h" +#include "../connector/utils.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "resources/config.h" + +using namespace std; +using namespace Dialogs; +using namespace Data; + +K_PLUGIN_FACTORY(SyncthingFileItemActionFactory, registerPlugin();) + +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; + } +} + +SyncthingConnection SyncthingFileItemAction::s_connection; + +SyncthingFileItemAction::SyncthingFileItemAction(QObject *parent, const QVariantList &) : + KAbstractFileItemActionPlugin(parent) +{ + if(s_connection.apiKey().isEmpty()) { + // 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 SyncthingFileItemAction::actions(const KFileItemListProperties &fileItemInfo, QWidget *parentWidget) +{ + // check whether any directories are known + const auto &dirs = s_connection.dirInfo(); + if(dirs.empty()) { + return QList(); + } + + // 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(); + } + paths << item.localPath(); + } + + // determine relevant Syncthing dirs + QList detectedDirs; + QList containingDirs; + QList detectedItems; + const SyncthingDir *lastDir; + for(const SyncthingDir &dir : dirs) { + QStringRef dirPath(&dir.path); + while(dirPath.endsWith(QChar('/'))) { + dirPath.chop(1); + } + for(const QString &path : paths) { + if(path == dirPath) { + detectedDirs << (lastDir = &dir); + } else if(path.startsWith(dir.path)) { + detectedItems << SyncthingItem(&dir, path.mid(dir.path.size())); + containingDirs << (lastDir = &dir); + } + } + } + + // add actions for explicitely selected Syncthing dirs + QList actions; + if(!detectedDirs.isEmpty()) { + actions << new QAction( + QIcon::fromTheme(QStringLiteral("folder-sync")), + detectedDirs.size() == 1 + ? tr("Rescan ") + 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); + } + } + + // add actions for the Syncthing dirs containing selected items + if(!containingDirs.isEmpty()) { + actions << new QAction( + QIcon::fromTheme(QStringLiteral("folder-sync")), + containingDirs.size() == 1 + ? tr("Rescan ") + 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); + } + } + + // 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); + } + } + + // don't show anything if relevant actions could be determined + if(actions.isEmpty()) { + return actions; + } + + // create the menu + QAction *menuAction = new QAction(statusIcons().scanninig, tr("Syncthing"), this); + QMenu *menu = new QMenu(parentWidget); + menuAction->setMenu(menu); + menu->addActions(actions); + + // add action to show further information about directory if the selection is only about + // one particular Syncthing dir + if(detectedDirs.size() + containingDirs.size() == 1) { + QAction *infoAction = menu->addSeparator(); + infoAction->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); + infoAction->setText(tr("Directory")); + QAction *statusAction = menu->addAction(tr("Status: ") + statusString(lastDir->status)); + switch(lastDir->status) { + case SyncthingDirStatus::Unknown: + case SyncthingDirStatus::Unshared: + statusAction->setIcon(statusIcons().disconnected); + break; + case SyncthingDirStatus::Idle: + statusAction->setIcon(statusIcons().idling); + break; + case SyncthingDirStatus::Scanning: + statusAction->setIcon(statusIcons().scanninig); + break; + case SyncthingDirStatus::Synchronizing: + statusAction->setIcon(statusIcons().sync); + break; + case SyncthingDirStatus::Paused: + statusAction->setIcon(statusIcons().pause); + break; + case SyncthingDirStatus::OutOfSync: + statusAction->setIcon(statusIcons().error); + break; + } + menu->addAction(QIcon::fromTheme(QStringLiteral("accept_time_event")), + tr("Last scan time: ") + agoString(lastDir->lastScanTime))->setEnabled(false); + menu->addAction(tr("Rescan interval: %1 seconds").arg(lastDir->rescanInterval))->setEnabled(false); + } + + // about about action + menu->addSeparator(); + menu->addAction(QIcon::fromTheme(QStringLiteral("help-about")), tr("About"), &SyncthingFileItemAction::showAboutDialog); + + return QList() << menuAction; +} + +void SyncthingFileItemAction::logConnectionStatus() +{ + cerr << "Syncthing connection status changed to: " << s_connection.statusText().toLocal8Bit().data() << endl; +} + +void SyncthingFileItemAction::logConnectionError(const QString &errorMessage) +{ + 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), QStringLiteral(APP_VERSION), 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->setWindowFlags(static_cast(aboutDialog->windowFlags() | Qt::WA_DeleteOnClose)); + aboutDialog->show(); +} + + +#include diff --git a/fileitemactionplugin/syncthingfileitemaction.desktop b/fileitemactionplugin/syncthingfileitemaction.desktop new file mode 100644 index 0000000..950eb3e --- /dev/null +++ b/fileitemactionplugin/syncthingfileitemaction.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Service +Name=Trigger Syncthing scan +Name[de]="Syncthing-Rescan" im Servicemenü +X-KDE-Library=syncthingfileitemaction +X-KDE-Submenu=Syncthing + +Icon=syncthingtray +ServiceTypes=KFileItemAction/Plugin +MimeType=application/octet-stream;inode/directory diff --git a/fileitemactionplugin/syncthingfileitemaction.h b/fileitemactionplugin/syncthingfileitemaction.h new file mode 100644 index 0000000..ed9e06d --- /dev/null +++ b/fileitemactionplugin/syncthingfileitemaction.h @@ -0,0 +1,31 @@ +#ifndef SYNCTHINGFILEITEMACTION_H +#define SYNCTHINGFILEITEMACTION_H + +#include "../connector/syncthingconnection.h" + +#include + +QT_FORWARD_DECLARE_CLASS(QAction) +QT_FORWARD_DECLARE_CLASS(QWidget) + +class KFileItemListProperties; + +class SyncthingFileItemAction : public KAbstractFileItemActionPlugin +{ + Q_OBJECT + +public: + SyncthingFileItemAction(QObject* parent, const QVariantList &args); + QList actions(const KFileItemListProperties &fileItemInfo, QWidget *parentWidget) override; + +private Q_SLOTS: + static void logConnectionStatus(); + static void logConnectionError(const QString &errorMessage); + static void rescanDir(const QString &dirId, const QString &relpath = QString()); + static void showAboutDialog(); + +private: + static Data::SyncthingConnection s_connection; +}; + +#endif // SYNCTHINGFILEITEMACTION_H