Initial QSfera import
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
Digia Qt LGPL Exception version 1.1
|
||||
|
||||
As an additional permission to the GNU Lesser General Public License version
|
||||
2.1, the object code form of a "work that uses the Library" may incorporate
|
||||
material from a header file that is part of the Library. You may distribute
|
||||
such object code under terms of your choice, provided that:
|
||||
(i) the header files of the Library have not been modified; and
|
||||
(ii) the incorporated material is limited to numerical parameters, data
|
||||
structure layouts, accessors, macros, inline functions and
|
||||
templates; and
|
||||
(iii) you comply with the terms of Section 6 of the GNU Lesser General
|
||||
Public License version 2.1.
|
||||
|
||||
Moreover, you may apply this exception to a modified version of the Library,
|
||||
provided that such modification does not involve copying material from the
|
||||
Library into the modified Library's header files unless such material is
|
||||
limited to (i) numerical parameters; (ii) data structure layouts;
|
||||
(iii) accessors; and (iv) small macros, templates and inline functions of
|
||||
five lines or less in length.
|
||||
|
||||
Furthermore, you are not required to apply this additional permission to a
|
||||
modified version of the Library.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011 Morgan Leborgne
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2011 Morgan Leborgne
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "QProgressIndicator.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
QProgressIndicator::QProgressIndicator(QWidget* parent)
|
||||
: QWidget(parent),
|
||||
m_angle(0),
|
||||
m_timerId(-1),
|
||||
m_delay(40),
|
||||
m_displayedWhenStopped(false),
|
||||
m_color(Qt::black)
|
||||
{
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
}
|
||||
|
||||
bool QProgressIndicator::isAnimated () const
|
||||
{
|
||||
return (m_timerId != -1);
|
||||
}
|
||||
|
||||
void QProgressIndicator::setDisplayedWhenStopped(bool state)
|
||||
{
|
||||
m_displayedWhenStopped = state;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool QProgressIndicator::isDisplayedWhenStopped() const
|
||||
{
|
||||
return m_displayedWhenStopped;
|
||||
}
|
||||
|
||||
void QProgressIndicator::startAnimation()
|
||||
{
|
||||
m_angle = 0;
|
||||
|
||||
if (m_timerId == -1)
|
||||
m_timerId = startTimer(m_delay);
|
||||
}
|
||||
|
||||
void QProgressIndicator::stopAnimation()
|
||||
{
|
||||
if (m_timerId != -1)
|
||||
killTimer(m_timerId);
|
||||
|
||||
m_timerId = -1;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void QProgressIndicator::setAnimationDelay(int delay)
|
||||
{
|
||||
if (m_timerId != -1)
|
||||
killTimer(m_timerId);
|
||||
|
||||
m_delay = delay;
|
||||
|
||||
if (m_timerId != -1)
|
||||
m_timerId = startTimer(m_delay);
|
||||
}
|
||||
|
||||
void QProgressIndicator::setColor(const QColor & color)
|
||||
{
|
||||
m_color = color;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
QSize QProgressIndicator::sizeHint() const
|
||||
{
|
||||
return QSize(20,20);
|
||||
}
|
||||
|
||||
int QProgressIndicator::heightForWidth(int w) const
|
||||
{
|
||||
return w;
|
||||
}
|
||||
|
||||
void QProgressIndicator::timerEvent(QTimerEvent * /*event*/)
|
||||
{
|
||||
m_angle = (m_angle+30)%360;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void QProgressIndicator::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
if (!m_displayedWhenStopped && !isAnimated())
|
||||
return;
|
||||
|
||||
int width = qMin(this->width(), this->height());
|
||||
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
int outerRadius = (width-1)*0.5;
|
||||
int innerRadius = (width-1)*0.5*0.38;
|
||||
|
||||
int capsuleHeight = outerRadius - innerRadius;
|
||||
int capsuleWidth = (width > 32 ) ? capsuleHeight *.23 : capsuleHeight *.35;
|
||||
int capsuleRadius = capsuleWidth/2;
|
||||
|
||||
for (int i=0; i<12; i++)
|
||||
{
|
||||
QColor color = m_color;
|
||||
color.setAlphaF(1.0f - (i/12.0f));
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(color);
|
||||
p.save();
|
||||
p.translate(rect().center());
|
||||
p.rotate(m_angle - i*30.0f);
|
||||
p.drawRoundedRect(-capsuleWidth*0.5, -(innerRadius+capsuleHeight), capsuleWidth, capsuleHeight, capsuleRadius, capsuleRadius);
|
||||
p.restore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2011 Morgan Leborgne
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef QPROGRESSINDICATOR_H
|
||||
#define QPROGRESSINDICATOR_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QColor>
|
||||
|
||||
/*!
|
||||
\class QProgressIndicator
|
||||
\brief The QProgressIndicator class lets an application display a progress indicator to show that a lengthy task is under way.
|
||||
|
||||
Progress indicators are indeterminate and do nothing more than spin to show that the application is busy.
|
||||
\sa QProgressBar
|
||||
*/
|
||||
class QProgressIndicator : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int delay READ animationDelay WRITE setAnimationDelay)
|
||||
Q_PROPERTY(bool displayedWhenStopped READ isDisplayedWhenStopped WRITE setDisplayedWhenStopped)
|
||||
Q_PROPERTY(QColor color READ color WRITE setColor)
|
||||
public:
|
||||
QProgressIndicator(QWidget* parent = nullptr);
|
||||
|
||||
/*! Returns the delay between animation steps.
|
||||
\return The number of milliseconds between animation steps. By default, the animation delay is set to 40 milliseconds.
|
||||
\sa setAnimationDelay
|
||||
*/
|
||||
int animationDelay() const { return m_delay; }
|
||||
|
||||
/*! Returns a Boolean value indicating whether the component is currently animated.
|
||||
\return Animation state.
|
||||
\sa startAnimation stopAnimation
|
||||
*/
|
||||
bool isAnimated () const;
|
||||
|
||||
/*! Returns a Boolean value indicating whether the receiver shows itself even when it is not animating.
|
||||
\return Return true if the progress indicator shows itself even when it is not animating. By default, it returns false.
|
||||
\sa setDisplayedWhenStopped
|
||||
*/
|
||||
bool isDisplayedWhenStopped() const;
|
||||
|
||||
/*! Returns the color of the component.
|
||||
\sa setColor
|
||||
*/
|
||||
const QColor & color() const { return m_color; }
|
||||
|
||||
QSize sizeHint() const override;
|
||||
int heightForWidth(int w) const override;
|
||||
public Q_SLOTS:
|
||||
/*! Starts the spin animation.
|
||||
\sa stopAnimation isAnimated
|
||||
*/
|
||||
void startAnimation();
|
||||
|
||||
/*! Stops the spin animation.
|
||||
\sa startAnimation isAnimated
|
||||
*/
|
||||
void stopAnimation();
|
||||
|
||||
/*! Sets the delay between animation steps.
|
||||
Setting the \a delay to a value larger than 40 slows the animation, while setting the \a delay to a smaller value speeds it up.
|
||||
\param delay The delay, in milliseconds.
|
||||
\sa animationDelay
|
||||
*/
|
||||
void setAnimationDelay(int delay);
|
||||
|
||||
/*! Sets whether the component hides itself when it is not animating.
|
||||
\param state The animation state. Set false to hide the progress indicator when it is not animating; otherwise true.
|
||||
\sa isDisplayedWhenStopped
|
||||
*/
|
||||
void setDisplayedWhenStopped(bool state);
|
||||
|
||||
/*! Sets the color of the components to the given color.
|
||||
\sa color
|
||||
*/
|
||||
void setColor(const QColor & color);
|
||||
protected:
|
||||
void timerEvent(QTimerEvent * event) override;
|
||||
void paintEvent(QPaintEvent * event) override;
|
||||
private:
|
||||
int m_angle;
|
||||
int m_timerId;
|
||||
int m_delay;
|
||||
bool m_displayedWhenStopped;
|
||||
QColor m_color;
|
||||
};
|
||||
|
||||
#endif // QPROGRESSINDICATOR_H
|
||||
@@ -0,0 +1,14 @@
|
||||
## Description
|
||||
|
||||
The QProgressIndicator class lets an application display a progress indicator to show that a lengthy task is under way.
|
||||
Will work at any size.
|
||||
|
||||
<img src="https://raw.github.com/mojocorp/QProgressIndicator/master/screen-capture-1.png" >
|
||||
<img src="https://raw.github.com/mojocorp/QProgressIndicator/master/screen-capture-2.png" >
|
||||
|
||||
## Dependency
|
||||
Qt 4.4.x.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,56 @@
|
||||
if(WITH_CRASHREPORTER)
|
||||
find_package(CrashReporterQt REQUIRED)
|
||||
endif()
|
||||
|
||||
find_package(Qt6Keychain 0.13 REQUIRED)
|
||||
|
||||
# TODO: Mingw64 7.3 might also need to be excluded here as it seems to not automatically link libssp
|
||||
if(NOT WIN32)
|
||||
if(NOT (CMAKE_SYSTEM_PROCESSOR MATCHES "^(alpha|parisc|hppa)") AND NOT CMAKE_CROSSCOMPILING)
|
||||
if((CMAKE_CXX_COMPILER_ID MATCHES "GNU") AND (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9))
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector --param=ssp-buffer-size=4")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector --param=ssp-buffer-size=4")
|
||||
else()
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector-strong")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector-strong")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER)
|
||||
if(CMAKE_BUILD_TYPE_LOWER MATCHES "(release|relwithdebinfo|minsizerel)")
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_FORTIFY_SOURCE=2")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_FORTIFY_SOURCE=2")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# Enable DEP & ASLR
|
||||
if (MINGW)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase")
|
||||
endif()
|
||||
elseif(UNIX AND NOT APPLE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,relro -Wl,-z,now")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,relro -Wl,-z,now")
|
||||
endif()
|
||||
|
||||
add_subdirectory(resources)
|
||||
add_subdirectory(libsync)
|
||||
add_subdirectory(gui)
|
||||
add_subdirectory(cmd)
|
||||
|
||||
if (WITH_CRASHREPORTER)
|
||||
add_subdirectory(crashreporter)
|
||||
endif()
|
||||
|
||||
add_subdirectory(plugins)
|
||||
|
||||
install(EXPORT ${APPLICATION_SHORTNAME}Config DESTINATION "${KDE_INSTALL_CMAKEPACKAGEDIR}/${APPLICATION_SHORTNAME}" NAMESPACE QSfera::)
|
||||
|
||||
ecm_setup_version(PROJECT
|
||||
VARIABLE_PREFIX ${APPLICATION_SHORTNAME}
|
||||
PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_SHORTNAME}ConfigVersion.cmake"
|
||||
SOVERSION ${MIRALL_SOVERSION}
|
||||
)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_SHORTNAME}ConfigVersion.cmake" DESTINATION "${KDE_INSTALL_CMAKEPACKAGEDIR}/${APPLICATION_SHORTNAME}")
|
||||
@@ -0,0 +1,21 @@
|
||||
add_executable(cmd
|
||||
cmd.cpp
|
||||
tokencredentials.cpp
|
||||
)
|
||||
set_target_properties(cmd PROPERTIES OUTPUT_NAME "${APPLICATION_EXECUTABLE}cmd")
|
||||
ecm_mark_nongui_executable(cmd)
|
||||
|
||||
target_link_libraries(cmd libsync Qt::Core Qt::Network ZLIB::ZLIB)
|
||||
apply_common_target_settings(cmd)
|
||||
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(cmd PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<TARGET_FILE_DIR:qsfera>")
|
||||
else()
|
||||
install(TARGETS cmd ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
endif()
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
configure_file(${CMAKE_SOURCE_DIR}/qsferacmd.desktop.in ${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_EXECUTABLE}cmd.desktop)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_EXECUTABLE}cmd.desktop DESTINATION ${KDE_INSTALL_DATADIR}/applications)
|
||||
endif()
|
||||
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@owncloud.com>
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
* Copyright (C) by Daniel Heule <daniel.heule@gmail.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "account.h"
|
||||
#include "cmd/tokencredentials.h"
|
||||
#include "common/syncjournaldb.h"
|
||||
#include "common/version.h"
|
||||
#include "configfile.h" // ONLY ACCESS THE STATIC FUNCTIONS!
|
||||
#include "libsync/graphapi/spacesmanager.h"
|
||||
#include "libsync/logger.h"
|
||||
#include "libsync/theme.h"
|
||||
#include "networkjobs/checkserverjobfactory.h"
|
||||
#include "networkjobs/jsonjob.h"
|
||||
#include "platform.h"
|
||||
#include "syncengine.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QUrl>
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
namespace {
|
||||
// start in quiet mode
|
||||
bool logQuietMode = true;
|
||||
|
||||
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
||||
{
|
||||
if (std::strcmp(context.category, "default")) {
|
||||
if (logQuietMode) {
|
||||
return;
|
||||
}
|
||||
std::cerr << qPrintable(qFormatLogMessage(type, context, message)) << std::endl;
|
||||
} else {
|
||||
switch (type) {
|
||||
case QtInfoMsg:
|
||||
std::cout << qPrintable(message) << std::endl;
|
||||
return;
|
||||
case QtDebugMsg:
|
||||
// ignore debug message if we are in quiet mode
|
||||
if (logQuietMode) {
|
||||
return;
|
||||
}
|
||||
std::cerr << "Debug";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
std::cerr << "Warning";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
std::cerr << "Critical";
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
std::cerr << "Fatal: " << qPrintable(message) << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
std::cerr << ": " << qPrintable(message) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
struct CmdOptions
|
||||
{
|
||||
QString source_dir;
|
||||
QString space_id;
|
||||
QUrl server_url;
|
||||
QString remoteFolder;
|
||||
|
||||
QByteArray username;
|
||||
QByteArray token = qgetenv("QSFERA_TOKEN");
|
||||
|
||||
bool query = false;
|
||||
bool trustSSL = false;
|
||||
bool interactive = true;
|
||||
bool ignoreHiddenFiles = true;
|
||||
QString exclude;
|
||||
QString unsyncedfolders;
|
||||
int restartTimes = 3;
|
||||
int downlimit = 0;
|
||||
int uplimit = 0;
|
||||
};
|
||||
|
||||
struct SyncCTX
|
||||
{
|
||||
explicit SyncCTX(const CmdOptions &cmdOptions)
|
||||
: options{cmdOptions}
|
||||
, promptRemoveAllFiles(cmdOptions.interactive)
|
||||
{
|
||||
}
|
||||
CmdOptions options;
|
||||
bool promptRemoveAllFiles;
|
||||
AccountPtr account;
|
||||
};
|
||||
|
||||
/* If the selective sync list is different from before, we need to disable the read from db
|
||||
(The normal client does it in SelectiveSyncDialog::accept*)
|
||||
*/
|
||||
void selectiveSyncFixup(OCC::SyncJournalDb *journal, const QSet<QString> &newListSet)
|
||||
{
|
||||
SqlDatabase db;
|
||||
if (!db.openOrCreateReadWrite(journal->databaseFilePath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
|
||||
const auto oldBlackListSet = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
|
||||
if (ok) {
|
||||
const auto changes = (oldBlackListSet - newListSet) + (newListSet - oldBlackListSet);
|
||||
for (const auto &it : changes) {
|
||||
journal->schedulePathForRemoteDiscovery(it);
|
||||
}
|
||||
|
||||
journal->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, newListSet);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void sync(const SyncCTX &ctx, const QUrl &spaceUrl)
|
||||
{
|
||||
const auto selectiveSyncList = [&]() -> QSet<QString> {
|
||||
if (!ctx.options.unsyncedfolders.isEmpty()) {
|
||||
QFile f(ctx.options.unsyncedfolders);
|
||||
if (!f.open(QFile::ReadOnly)) {
|
||||
qCritical() << u"Could not open file containing the list of unsynced folders: " << ctx.options.unsyncedfolders;
|
||||
} else {
|
||||
// filter out empty lines and comments
|
||||
auto selectiveSyncList = QString::fromUtf8(f.readAll())
|
||||
.split(QLatin1Char('\n'))
|
||||
.filter(QRegularExpression(QStringLiteral("\\S+")))
|
||||
.filter(QRegularExpression(QStringLiteral("^[^#]")));
|
||||
|
||||
for (int i = 0; i < selectiveSyncList.count(); ++i) {
|
||||
if (!selectiveSyncList.at(i).endsWith(QLatin1Char('/'))) {
|
||||
selectiveSyncList[i].append(QLatin1Char('/'));
|
||||
}
|
||||
}
|
||||
return {selectiveSyncList.cbegin(), selectiveSyncList.cend()};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}();
|
||||
|
||||
const QString dbPath = ctx.options.source_dir + SyncJournalDb::makeDbName(ctx.options.source_dir);
|
||||
auto db = new SyncJournalDb(dbPath, qApp);
|
||||
if (!selectiveSyncList.empty()) {
|
||||
selectiveSyncFixup(db, selectiveSyncList);
|
||||
}
|
||||
|
||||
SyncOptions opt{QSharedPointer<Vfs>(VfsPluginManager::instance().createVfsFromPlugin(Vfs::Mode::Off).release())};
|
||||
auto engine = new SyncEngine(ctx.account, spaceUrl, ctx.options.source_dir, ctx.options.remoteFolder, db);
|
||||
engine->setSyncOptions(opt);
|
||||
engine->setParent(db);
|
||||
|
||||
QObject::connect(engine, &SyncEngine::finished, engine, [engine, ctx, restartCount = std::make_shared<int>(0)](bool result) {
|
||||
if (!result) {
|
||||
qWarning() << u"Failed to sync";
|
||||
exit(EXIT_FAILURE);
|
||||
} else {
|
||||
if (engine->isAnotherSyncNeeded()) {
|
||||
if (*restartCount < ctx.options.restartTimes) {
|
||||
(*restartCount)++;
|
||||
qDebug() << u"Restarting Sync, because another sync is needed" << *restartCount;
|
||||
engine->startSync();
|
||||
return;
|
||||
}
|
||||
qWarning() << u"Another sync is needed, but not done because restart count is exceeded" << *restartCount;
|
||||
} else {
|
||||
qInfo() << u"Sync succeeded";
|
||||
qApp->quit();
|
||||
}
|
||||
}
|
||||
});
|
||||
QObject::connect(engine, &SyncEngine::syncError, engine, [](const QString &error) { qWarning() << u"Sync error:" << error; });
|
||||
QObject::connect(engine, &SyncEngine::itemCompleted, engine, [](const SyncFileItemPtr &item) {
|
||||
if (item->hasErrorStatus()) {
|
||||
switch (item->_status) {
|
||||
case SyncFileItem::Excluded:
|
||||
qDebug() << u"Sync excluded file:" << item->_errorString;
|
||||
break;
|
||||
default:
|
||||
qWarning() << u"Sync error:" << item->_status << item->_errorString;
|
||||
}
|
||||
}
|
||||
});
|
||||
engine->setIgnoreHiddenFiles(ctx.options.ignoreHiddenFiles);
|
||||
engine->setNetworkLimits(ctx.options.uplimit, ctx.options.downlimit);
|
||||
|
||||
|
||||
// Always try to load the user-provided exclude list if one is specified
|
||||
if (!ctx.options.exclude.isEmpty()) {
|
||||
engine->addExcludeList(ctx.options.exclude);
|
||||
} else {
|
||||
engine->addExcludeList(ConfigFile::defaultExcludeFile());
|
||||
}
|
||||
|
||||
if (!engine->reloadExcludes()) {
|
||||
qCritical() << u"Cannot load system exclude list or list supplied via --exclude";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
engine->startSync();
|
||||
}
|
||||
|
||||
void setupCredentials(SyncCTX &ctx)
|
||||
{
|
||||
// Order of retrieval attempt (later attempts override earlier ones):
|
||||
// 1. From URL
|
||||
// 2. From options
|
||||
// 3. From prompt (if interactive)
|
||||
|
||||
// Pre-flight check: verify that the file specified by --unsyncedfolders can be read by us:
|
||||
if (!ctx.options.unsyncedfolders.isNull()) { // yes, isNull and not isEmpty because...:
|
||||
// ... if the user entered "--unsyncedfolders ''" on the command-line, opening that will
|
||||
// also fail
|
||||
QFile f(ctx.options.unsyncedfolders);
|
||||
if (!f.open(QFile::ReadOnly)) {
|
||||
qCritical() << u"Cannot read unsyncedfolders file '" << ctx.options.unsyncedfolders << u"': " << f.errorString();
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
ctx.account->setCredentials(new TokenCredentials(std::move(ctx.options.username), std::move(ctx.options.token)));
|
||||
if (ctx.options.trustSSL) {
|
||||
QObject::connect(ctx.account->accessManager(), &QNetworkAccessManager::sslErrors, qApp,
|
||||
[](QNetworkReply *reply, const QList<QSslError> &errors) { reply->ignoreSslErrors(errors); });
|
||||
} else {
|
||||
QObject::connect(ctx.account->accessManager(), &QNetworkAccessManager::sslErrors, qApp, [](QNetworkReply *reply, const QList<QSslError> &errors) {
|
||||
Q_UNUSED(reply)
|
||||
|
||||
qCritical() << u"SSL error encountered";
|
||||
for (const auto &e : errors) {
|
||||
qCritical() << e.errorString();
|
||||
}
|
||||
qCritical() << u"If you trust the certificate and want to ignore the errors, use the --trust option.";
|
||||
exit(EXIT_FAILURE);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QString hashSaceId(const QString &id)
|
||||
{
|
||||
auto adler = adler32_z(0, nullptr, 0);
|
||||
adler = adler32_z(adler, reinterpret_cast<Bytef *>(id.toUtf8().data()), id.size());
|
||||
return QStringLiteral("space:%1").arg(QString::number(adler, 16));
|
||||
}
|
||||
|
||||
void printSpaces(const QVector<GraphApi::Space *> &spaces)
|
||||
{
|
||||
auto printTable = [](const QString &a, const QString &b, const QString &c) {
|
||||
qInfo().noquote() << QStringLiteral("%1 | %2 | %3").arg(a, 15).arg(b, 20).arg(c);
|
||||
};
|
||||
qInfo() << u"Listing spaces:";
|
||||
printTable(QStringLiteral("Short ID"), QStringLiteral("DisplayName"), QStringLiteral("ID"));
|
||||
printTable(QString().fill(QLatin1Char('-'), 15), QString().fill(QLatin1Char('-'), 20), QString().fill(QLatin1Char('-'), 20));
|
||||
for (auto *s : spaces) {
|
||||
printTable(hashSaceId(s->id()), s->displayName(), s->id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CmdOptions parseOptions(const QStringList &app_args)
|
||||
{
|
||||
CmdOptions options;
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(QStringLiteral("%1 version %2 - command line client tool").arg(QCoreApplication::instance()->applicationName(), OCC::Version::displayString()));
|
||||
|
||||
// this little snippet saves a few lines below
|
||||
auto addOption = [&parser](QCommandLineOption &&option, std::optional<QCommandLineOption::Flag> &&flags = {}) {
|
||||
if (flags.has_value()) {
|
||||
option.setFlags(flags.value());
|
||||
}
|
||||
parser.addOption(option);
|
||||
return option;
|
||||
};
|
||||
auto userOption = addOption({{QStringLiteral("u"), QStringLiteral("user")}, QStringLiteral("Username"), QStringLiteral("name")});
|
||||
auto tokenOption = addOption(
|
||||
{{QStringLiteral("t"), QStringLiteral("token")}, QStringLiteral("Authentication token, you can also use $QSFERA_TOKEN"), QStringLiteral("token")});
|
||||
|
||||
auto remoteFolder =
|
||||
addOption({{QStringLiteral("remote-folder")}, QStringLiteral("The subdirectory of the space that is synchronized"), QStringLiteral("remote-folder")});
|
||||
auto trustOption = addOption({ { QStringLiteral("trust") }, QStringLiteral("Trust the SSL certification") });
|
||||
auto excludeOption = addOption({ { QStringLiteral("exclude") }, QStringLiteral("Path to an exclude list [file]"), QStringLiteral("file") });
|
||||
auto unsyncedfoldersOption = addOption(
|
||||
{{QStringLiteral("unsynced-folders")}, QStringLiteral("File containing the list of unsynced remote folders (selective sync)"), QStringLiteral("file")});
|
||||
|
||||
auto nonInterActiveOption = addOption({ { QStringLiteral("non-interactive") }, QStringLiteral("Do not block execution with interaction") });
|
||||
auto maxRetriesOption = addOption({{QStringLiteral("max-sync-retries")}, QStringLiteral("Retries maximum n times (defaults to 3)"), QStringLiteral("n")});
|
||||
auto uploadLimitOption = addOption({{QStringLiteral("upload-limit")}, QStringLiteral("Limit the upload speed of files to n KB/s"), QStringLiteral("n")});
|
||||
auto downloadLimitption =
|
||||
addOption({{QStringLiteral("download-limit")}, QStringLiteral("Limit the download speed of files to n KB/s"), QStringLiteral("n")});
|
||||
auto syncHiddenFilesOption = addOption({ { QStringLiteral("sync-hidden-files") }, QStringLiteral("Enables synchronization of hidden files") });
|
||||
|
||||
const auto testCrashReporter =
|
||||
addOption({{QStringLiteral("crash")}, QStringLiteral("Crash the client to test the crash reporter")}, QCommandLineOption::HiddenFromHelp);
|
||||
|
||||
auto verbosityOption = addOption({{QStringLiteral("verbose")},
|
||||
QStringLiteral("Specify the [verbosity]\n0: no logging (default)\n"
|
||||
"1: general logging\n"
|
||||
"2: all previous and http logs\n"
|
||||
"3: all previous and debug information"),
|
||||
QStringLiteral("verbosity"), QStringLiteral("0")});
|
||||
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
|
||||
parser.addPositionalArgument(
|
||||
QStringLiteral("server_url"), QStringLiteral("The URL to the QSfera installation on the server. This is usually the root path"));
|
||||
parser.addPositionalArgument(QStringLiteral("space_id"),
|
||||
QStringLiteral("The id, name or short id of the space to synchronize, if no [space_id] is provided or the [space_id] did not match any space, a list "
|
||||
"of spaces is printed."),
|
||||
QStringLiteral("[space_id]"));
|
||||
parser.addPositionalArgument(QStringLiteral("source_dir"), QStringLiteral("The source dir"), QStringLiteral("[source_dir]"));
|
||||
|
||||
parser.process(app_args);
|
||||
|
||||
|
||||
const int verbosity = parser.value(verbosityOption).toInt();
|
||||
if (verbosity >= 0 && verbosity <= 3) {
|
||||
logQuietMode = verbosity == 0;
|
||||
if (verbosity > 1) {
|
||||
Logger::instance()->addLogRule({QStringLiteral("sync.httplogger=true")});
|
||||
}
|
||||
if (verbosity > 2) {
|
||||
Logger::instance()->setLogDebug(true);
|
||||
}
|
||||
} else {
|
||||
qCritical() << u"Verbosity:" << verbosity << u"is not supported, valid verbosity level are 0, 1, 2";
|
||||
parser.showHelp(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
QStringList args = parser.positionalArguments();
|
||||
if (!args.isEmpty()) {
|
||||
options.server_url = QUrl::fromUserInput(args.takeFirst());
|
||||
if (!options.server_url.isValid()) {
|
||||
qCritical() << u"Invalid url: " << options.server_url.toString();
|
||||
parser.showHelp(EXIT_FAILURE);
|
||||
}
|
||||
} else {
|
||||
qCritical() << u"Please specify server_url";
|
||||
parser.showHelp(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (!args.isEmpty()) {
|
||||
options.space_id = args.takeFirst();
|
||||
} else {
|
||||
options.query = true;
|
||||
}
|
||||
|
||||
if (!args.isEmpty()) {
|
||||
options.source_dir = [&parser, arg = args.takeFirst()] {
|
||||
const QFileInfo fi(arg);
|
||||
if (!fi.exists()) {
|
||||
qCritical() << u"Source dir" << fi.filePath() << u"does not exist.";
|
||||
parser.showHelp(EXIT_FAILURE);
|
||||
}
|
||||
QString sourceDir = fi.absoluteFilePath();
|
||||
if (!sourceDir.endsWith(QLatin1Char('/'))) {
|
||||
sourceDir.append(QLatin1Char('/'));
|
||||
}
|
||||
return sourceDir;
|
||||
}();
|
||||
}
|
||||
|
||||
if (!args.isEmpty()) {
|
||||
qCritical() << u"Unhandled arguments" << args;
|
||||
parser.showHelp(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (parser.isSet(remoteFolder)) {
|
||||
options.remoteFolder = parser.value(remoteFolder);
|
||||
}
|
||||
|
||||
if (parser.isSet(trustOption)) {
|
||||
options.trustSSL = true;
|
||||
}
|
||||
if (parser.isSet(nonInterActiveOption)) {
|
||||
options.interactive = false;
|
||||
}
|
||||
if (parser.isSet(tokenOption)) {
|
||||
options.token = parser.value(tokenOption).toUtf8();
|
||||
} else if (options.token.isEmpty()) {
|
||||
qCritical() << u"Token not set";
|
||||
parser.showHelp(EXIT_FAILURE);
|
||||
}
|
||||
if (parser.isSet(userOption)) {
|
||||
options.username = parser.value(userOption).toUtf8();
|
||||
} else {
|
||||
qCritical() << u"Username not set";
|
||||
parser.showHelp(EXIT_FAILURE);
|
||||
}
|
||||
if (parser.isSet(excludeOption)) {
|
||||
options.exclude = parser.value(excludeOption);
|
||||
}
|
||||
if (parser.isSet(unsyncedfoldersOption)) {
|
||||
options.unsyncedfolders = parser.value(unsyncedfoldersOption);
|
||||
}
|
||||
if (parser.isSet(maxRetriesOption)) {
|
||||
options.restartTimes = parser.value(maxRetriesOption).toInt();
|
||||
}
|
||||
if (parser.isSet(uploadLimitOption)) {
|
||||
options.uplimit = parser.value(maxRetriesOption).toInt() * 1000;
|
||||
}
|
||||
if (parser.isSet(downloadLimitption)) {
|
||||
options.downlimit = parser.value(downloadLimitption).toInt() * 1000;
|
||||
}
|
||||
if (parser.isSet(syncHiddenFilesOption)) {
|
||||
options.ignoreHiddenFiles = false;
|
||||
}
|
||||
if (parser.isSet(testCrashReporter)) {
|
||||
// crash onc ethe main loop was started
|
||||
qCritical() << u"We'll soon crash on purpose";
|
||||
QTimer::singleShot(0, qApp, &Utility::crash);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
auto platform = OCC::Platform::create(Platform::Type::Terminal);
|
||||
qSetMessagePattern(Logger::loggerPattern());
|
||||
qInstallMessageHandler(messageHandler);
|
||||
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
platform->setApplication(&app);
|
||||
|
||||
app.setApplicationVersion(Theme::instance()->versionSwitchOutput());
|
||||
|
||||
SyncCTX ctx { parseOptions(app.arguments()) };
|
||||
|
||||
// start the main loop before we ask for the username etc
|
||||
QTimer::singleShot(0, &app, [&] {
|
||||
ctx.account = Account::create(QUuid::createUuid());
|
||||
|
||||
if (!ctx.account) {
|
||||
qCritical() << u"Could not initialize account!";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
setupCredentials(ctx);
|
||||
|
||||
// don't leak credentials more than needed
|
||||
ctx.options.server_url = ctx.options.server_url.adjusted(QUrl::RemoveUserInfo);
|
||||
ctx.account->setUrl(ctx.options.server_url);
|
||||
|
||||
QObject::connect(ctx.account->credentials(), &AbstractCredentials::authenticationFailed, qApp,
|
||||
[] { qFatal() << u"Authentication failed please verify your credentials"; });
|
||||
|
||||
auto *checkServerJob = CheckServerJobFactory(ctx.account->accessManager()).startJob(ctx.account->url(), qApp);
|
||||
|
||||
QObject::connect(checkServerJob, &CoreJob::finished, qApp, [ctx, checkServerJob] {
|
||||
if (checkServerJob->success()) {
|
||||
// Perform a call to get the capabilities.
|
||||
auto *capabilitiesJob = new JsonApiJob(ctx.account, QStringLiteral("ocs/v1.php/cloud/capabilities"), {}, {}, nullptr);
|
||||
QObject::connect(capabilitiesJob, &JsonApiJob::finishedSignal, qApp, [capabilitiesJob, ctx] {
|
||||
if (capabilitiesJob->reply()->error() != QNetworkReply::NoError || capabilitiesJob->httpStatusCode() != 200) {
|
||||
qCritical() << u"Error connecting to server";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
auto caps = capabilitiesJob->data()
|
||||
.value(QStringLiteral("ocs"))
|
||||
.toObject()
|
||||
.value(QStringLiteral("data"))
|
||||
.toObject()
|
||||
.value(QStringLiteral("capabilities"))
|
||||
.toObject();
|
||||
qDebug() << u"Server capabilities" << caps;
|
||||
ctx.account->setCapabilities({ctx.account->url(), caps.toVariantMap()});
|
||||
|
||||
switch (ctx.account->serverSupportLevel()) {
|
||||
case Account::ServerSupportLevel::Supported:
|
||||
break;
|
||||
case Account::ServerSupportLevel::Unknown:
|
||||
qWarning() << u"Failed to detect server version";
|
||||
break;
|
||||
case Account::ServerSupportLevel::Unsupported:
|
||||
qCritical() << u"Error unsupported server";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (ctx.options.query) {
|
||||
QObject::connect(ctx.account->spacesManager(), &GraphApi::SpacesManager::ready, qApp, [ctx] {
|
||||
printSpaces(ctx.account->spacesManager()->spaces());
|
||||
qApp->quit();
|
||||
});
|
||||
} else {
|
||||
QObject::connect(ctx.account->spacesManager(), &GraphApi::SpacesManager::ready, qApp, [ctx] {
|
||||
GraphApi::Space *space = nullptr;
|
||||
if (ctx.options.space_id.count(QLatin1Char('$')) == 1) {
|
||||
space = ctx.account->spacesManager()->space(ctx.options.space_id);
|
||||
} else {
|
||||
const auto spaces = ctx.account->spacesManager()->spaces();
|
||||
if (ctx.options.space_id.startsWith(QLatin1String("space:"))) {
|
||||
auto it = std::find_if(
|
||||
spaces.cbegin(), spaces.cend(), [&](const GraphApi::Space *s) { return hashSaceId(s->id()) == ctx.options.space_id; });
|
||||
if (it != spaces.cend()) {
|
||||
space = *it;
|
||||
}
|
||||
} else {
|
||||
auto it = std::find_if(
|
||||
spaces.cbegin(), spaces.cend(), [&](const GraphApi::Space *s) { return s->displayName() == ctx.options.space_id; });
|
||||
if (it != spaces.cend()) {
|
||||
space = *it;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!space) {
|
||||
qCritical() << u"No spaces found matching:" << ctx.options.space_id;
|
||||
printSpaces(ctx.account->spacesManager()->spaces());
|
||||
qApp->exit(EXIT_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
// much lower age than the default since this utility is usually made to be run right after a change in the tests
|
||||
SyncEngine::minimumFileAgeForUpload = std::chrono::seconds(0);
|
||||
sync(ctx, space->webdavUrl());
|
||||
});
|
||||
}
|
||||
|
||||
// announce we are ready
|
||||
Q_EMIT ctx.account->credentialsFetched();
|
||||
});
|
||||
capabilitiesJob->start();
|
||||
} else {
|
||||
if (checkServerJob->reply()->error() == QNetworkReply::OperationCanceledError) {
|
||||
qFatal() << u"Looking up " << ctx.account->url().toString() << u" timed out.";
|
||||
} else {
|
||||
qFatal() << u"Failed to resolve " << ctx.account->url().toString() << u" Error: " << checkServerJob->reply()->errorString();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "tokencredentials.h"
|
||||
|
||||
#include "creds/httpcredentials.h"
|
||||
#include "libsync/accessmanager.h"
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
class OCC::TokensAccessManager : public AccessManager
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
TokensAccessManager(QByteArray &&username, QByteArray &&token, QObject *parent)
|
||||
: AccessManager(parent)
|
||||
, _token("Basic " + QByteArray(username + ':' + token).toBase64())
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override
|
||||
{
|
||||
QNetworkRequest req(request);
|
||||
if (!req.attribute(HttpCredentials::DontAddCredentialsAttribute).toBool()) {
|
||||
req.setRawHeader("Authorization", _token);
|
||||
}
|
||||
return AccessManager::createRequest(op, req, outgoingData);
|
||||
}
|
||||
|
||||
private:
|
||||
QByteArray _token;
|
||||
};
|
||||
|
||||
TokenCredentials::TokenCredentials(QByteArray &&username, QByteArray &&token)
|
||||
: _accessManager(new TokensAccessManager(std::move(username), std::move(token), this))
|
||||
{
|
||||
connect(_accessManager, &QNetworkAccessManager::finished, this, [this](QNetworkReply *reply) {
|
||||
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
Q_EMIT authenticationFailed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
AccessManager *TokenCredentials::createAM() const
|
||||
{
|
||||
return _accessManager;
|
||||
}
|
||||
|
||||
bool TokenCredentials::ready() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void TokenCredentials::fetchFromKeychain() { }
|
||||
|
||||
void TokenCredentials::restartOauth() { }
|
||||
|
||||
void TokenCredentials::persist() { }
|
||||
|
||||
void TokenCredentials::invalidateToken() { }
|
||||
|
||||
void TokenCredentials::forgetSensitiveData() { }
|
||||
|
||||
|
||||
#include "tokencredentials.moc"
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/creds/abstractcredentials.h"
|
||||
|
||||
namespace OCC {
|
||||
class TokensAccessManager;
|
||||
|
||||
class TokenCredentials : public AbstractCredentials
|
||||
{
|
||||
private:
|
||||
Q_OBJECT
|
||||
public:
|
||||
TokenCredentials(QByteArray &&username, QByteArray &&token);
|
||||
|
||||
AccessManager *createAM() const override;
|
||||
bool ready() const override;
|
||||
void fetchFromKeychain() override;
|
||||
void restartOauth() override;
|
||||
void persist() override;
|
||||
void invalidateToken() override;
|
||||
void forgetSensitiveData() override;
|
||||
|
||||
private:
|
||||
TokensAccessManager *_accessManager;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# TODO: differentiate release channel
|
||||
# if(BUILD_RELEASE)
|
||||
# set(CRASHREPORTER_RELEASE_CHANNEL "release")
|
||||
# else()
|
||||
set(CRASHREPORTER_RELEASE_CHANNEL "nightly")
|
||||
# endif()
|
||||
|
||||
# Theme
|
||||
set(CRASHREPORTER_ICON_DIR "${OEM_THEME_DIR}/theme/colored")
|
||||
|
||||
set(CRASHREPORTER_ICON_FILENAME "${APPLICATION_ICON_NAME}-icon.svg")
|
||||
if (EXISTS "${OEM_THEME_DIR}/theme/colored/${CRASHREPORTER_ICON_FILENAME}")
|
||||
set(CRASHREPORTER_ICON ":/${CRASHREPORTER_ICON_FILENAME}")
|
||||
set(CRASHREPORTER_ICON_PATH "${CRASHREPORTER_ICON_DIR}/${CRASHREPORTER_ICON_FILENAME}")
|
||||
else()
|
||||
set(CRASHREPORTER_ICON_FILENAME "${APPLICATION_ICON_NAME}-icon.png")
|
||||
set(CRASHREPORTER_ICON ":/${CRASHREPORTER_ICON_FILENAME}")
|
||||
set(CRASHREPORTER_ICON_SIZE "128")
|
||||
set(CRASHREPORTER_ICON_PATH "${CRASHREPORTER_ICON_DIR}/${CRASHREPORTER_ICON_SIZE}-${CRASHREPORTER_ICON_FILENAME}")
|
||||
if (NOT EXISTS "${CRASHREPORTER_ICON_PATH}")
|
||||
set(CRASHREPORTER_ICON_PATH "${CRASHREPORTER_ICON_DIR}/${APPLICATION_ICON_NAME}-icon-${CRASHREPORTER_ICON_SIZE}.png")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/resources.qrc.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/resources.qrc)
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/CrashReporterConfig.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/CrashReporterConfig.h)
|
||||
|
||||
# Sources
|
||||
list(APPEND crashreporter_SOURCES main.cpp)
|
||||
list(APPEND crashreporter_RC "${CMAKE_CURRENT_BINARY_DIR}/resources.qrc")
|
||||
|
||||
add_executable(${CRASHREPORTER_EXECUTABLE}
|
||||
${crashreporter_SOURCES}
|
||||
${crashreporter_HEADERS_MOC}
|
||||
${crashreporter_UI_HEADERS}
|
||||
${crashreporter_RC}
|
||||
)
|
||||
apply_common_target_settings(${CRASHREPORTER_EXECUTABLE})
|
||||
|
||||
# This is a GUI Application without its own bundle
|
||||
set_target_properties(${CRASHREPORTER_EXECUTABLE}
|
||||
PROPERTIES
|
||||
MACOSX_BUNDLE FALSE
|
||||
)
|
||||
|
||||
target_include_directories(${CRASHREPORTER_EXECUTABLE} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
target_link_libraries(${CRASHREPORTER_EXECUTABLE}
|
||||
PRIVATE
|
||||
CrashReporterQt::Gui
|
||||
Qt::Core
|
||||
Qt::Widgets
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(${CRASHREPORTER_EXECUTABLE} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<TARGET_FILE_DIR:qsfera>")
|
||||
else()
|
||||
install(TARGETS ${CRASHREPORTER_EXECUTABLE} ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
endif()
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef CRASHREPORTERCONFIG_H
|
||||
#define CRASHREPORTERCONFIG_H
|
||||
|
||||
#define CRASHREPORTER_BUILD_ID "@CMAKE_DATESTAMP_YEAR@@CMAKE_DATESTAMP_MONTH@@CMAKE_DATESTAMP_DAY@000000"
|
||||
|
||||
#define CRASHREPORTER_RELEASE_CHANNEL "@CRASHREPORTER_RELEASE_CHANNEL@"
|
||||
|
||||
#define CRASHREPORTER_PRODUCT_NAME "@APPLICATION_SHORTNAME@"
|
||||
|
||||
#define CRASHREPORTER_VERSION_STRING "@MIRALL_VERSION_STRING@"
|
||||
|
||||
#define CRASHREPORTER_SUBMIT_URL "@CRASHREPORTER_SUBMIT_URL@"
|
||||
|
||||
#define CRASHREPORTER_ICON "@CRASHREPORTER_ICON@"
|
||||
|
||||
#endif // CRASHREPORTERCONFIG_H
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) by Dominik Schmidt <domme@tomahawk-player.org>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
#include "CrashReporterConfig.h"
|
||||
|
||||
#include <libcrashreporter-gui/CrashReporter.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QApplication::setStyle(QStringLiteral("fusion"));
|
||||
#endif // Q_OS_WIN
|
||||
|
||||
if (app.arguments().size() != 2) {
|
||||
qDebug() << u"You need to pass the .dmp file path as only argument";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TODO: install socorro ....
|
||||
CrashReporter reporter(QUrl(QStringLiteral(CRASHREPORTER_SUBMIT_URL)), app.arguments());
|
||||
|
||||
#ifdef CRASHREPORTER_ICON
|
||||
reporter.setLogo(QPixmap(QStringLiteral(CRASHREPORTER_ICON)));
|
||||
#endif
|
||||
reporter.setWindowTitle(QStringLiteral(CRASHREPORTER_PRODUCT_NAME));
|
||||
reporter.setText(QStringLiteral("<html><head/><body><p><span style=\" font-weight:600;\">Sorry!</span> " CRASHREPORTER_PRODUCT_NAME
|
||||
" crashed. Please tell us about it! " CRASHREPORTER_PRODUCT_NAME
|
||||
" has created an error report for you that can help improve the stability in the future. You can now send this report "
|
||||
"directly to the " CRASHREPORTER_PRODUCT_NAME " developers.</p></body></html>"));
|
||||
|
||||
const QFileInfo crashLog(QDir::tempPath() + QStringLiteral("/" CRASHREPORTER_PRODUCT_NAME "-crash.log"));
|
||||
if (crashLog.exists()) {
|
||||
QFile inFile(crashLog.filePath());
|
||||
if (inFile.open(QFile::ReadOnly)) {
|
||||
reporter.setComment(QString::fromUtf8(inFile.readAll()));
|
||||
}
|
||||
}
|
||||
|
||||
reporter.setReportData("BuildID", CRASHREPORTER_BUILD_ID);
|
||||
reporter.setReportData("ProductName", CRASHREPORTER_PRODUCT_NAME);
|
||||
reporter.setReportData("Version", CRASHREPORTER_VERSION_STRING);
|
||||
reporter.setReportData("ReleaseChannel", CRASHREPORTER_RELEASE_CHANNEL);
|
||||
|
||||
//reporter.setReportData( "timestamp", QByteArray::number( QDateTime::currentDateTime().toTime_t() ) );
|
||||
|
||||
|
||||
// add parameters
|
||||
|
||||
// << Pair("InstallTime", "1357622062")
|
||||
// << Pair("Theme", "classic/1.0")
|
||||
// << Pair("Version", "30")
|
||||
// << Pair("id", "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}")
|
||||
// << Pair("Vendor", "Mozilla")
|
||||
// << Pair("EMCheckCompatibility", "true")
|
||||
// << Pair("Throttleable", "0")
|
||||
// << Pair("URL", "http://code.google.com/p/crashme/")
|
||||
// << Pair("version", "20.0a1")
|
||||
// << Pair("CrashTime", "1357770042")
|
||||
// << Pair("submitted_timestamp", "2013-01-09T22:21:18.646733+00:00")
|
||||
// << Pair("buildid", "20130107030932")
|
||||
// << Pair("timestamp", "1357770078.646789")
|
||||
// << Pair("Notes", "OpenGL: NVIDIA Corporation -- GeForce 8600M GT/PCIe/SSE2 -- 3.3.0 NVIDIA 313.09 -- texture_from_pixmap\r\n")
|
||||
// << Pair("StartupTime", "1357769913")
|
||||
// << Pair("FramePoisonSize", "4096")
|
||||
// << Pair("FramePoisonBase", "7ffffffff0dea000")
|
||||
// << Pair("Add-ons", "%7B972ce4c6-7e08-4474-a285-3208198ce6fd%7D:20.0a1,crashme%40ted.mielczarek.org:0.4")
|
||||
// << Pair("SecondsSinceLastCrash", "1831736")
|
||||
// << Pair("ProductName", "WaterWolf")
|
||||
// << Pair("legacy_processing", "0")
|
||||
// << Pair("ProductID", "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}")
|
||||
|
||||
;
|
||||
|
||||
// TODO:
|
||||
// send log
|
||||
// QFile logFile( INSERT_FILE_PATH_HERE );
|
||||
// logFile.open( QFile::ReadOnly );
|
||||
// reporter.setReportData( "upload_file_miralllog", qCompress( logFile.readAll() ), "application/x-gzip", QFileInfo( INSERT_FILE_PATH_HERE ).fileName().toUtf8());
|
||||
// logFile.close();
|
||||
|
||||
reporter.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file alias="@CRASHREPORTER_ICON_FILENAME@">@CRASHREPORTER_ICON_PATH@</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,224 @@
|
||||
include(ECMAddAppIcon)
|
||||
|
||||
find_package(KDSingleApplication-qt6 1.0.0 REQUIRED)
|
||||
|
||||
if (WIN32)
|
||||
find_package(LibSnoreToast)
|
||||
set_package_properties(LibSnoreToast PROPERTIES
|
||||
URL https://invent.kde.org/libraries/snoretoast
|
||||
DESCRIPTION "Command-line application capable of creating Windows Toast notifications"
|
||||
TYPE OPTIONAL
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
add_library(QSferaGui SHARED)
|
||||
|
||||
target_sources(QSferaGui PRIVATE
|
||||
aboutdialog.ui
|
||||
accountsettings.ui
|
||||
generalsettings.ui
|
||||
ignorelisteditor.ui
|
||||
networksettings.ui
|
||||
protocolwidget.ui
|
||||
issueswidget.ui
|
||||
settingsdialog.ui
|
||||
tlserrordialog.ui
|
||||
logbrowser.ui
|
||||
)
|
||||
|
||||
target_sources(QSferaGui PRIVATE
|
||||
aboutdialog.cpp
|
||||
accountmanager.cpp
|
||||
accountsettings.cpp
|
||||
|
||||
accountmodalwidget.cpp
|
||||
accountmodalwidget.ui
|
||||
|
||||
application.cpp
|
||||
fetchserversettings.cpp
|
||||
commonstrings.cpp
|
||||
connectionvalidator.cpp
|
||||
folder.cpp
|
||||
folderdefinition.cpp
|
||||
folderman.cpp
|
||||
folderstatusmodel.cpp
|
||||
folderwatcher.cpp
|
||||
generalsettings.cpp
|
||||
ignorelisteditor.cpp
|
||||
lockwatcher.cpp
|
||||
logbrowser.cpp
|
||||
fonticonmessagebox.cpp
|
||||
networkinformation.cpp
|
||||
networksettings.cpp
|
||||
notifications.cpp
|
||||
openfilemanager.cpp
|
||||
protocolwidget.cpp
|
||||
protocolitem.cpp
|
||||
issueswidget.cpp
|
||||
activitywidget.cpp
|
||||
selectivesyncwidget.cpp
|
||||
settingsdialog.cpp
|
||||
tlserrordialog.cpp
|
||||
syncrunfilelog.cpp
|
||||
systray.cpp
|
||||
accountstate.cpp
|
||||
guiutility.cpp
|
||||
elidedlabel.cpp
|
||||
translations.cpp
|
||||
creds/httpcredentialsgui.cpp
|
||||
creds/qmlcredentials.cpp
|
||||
updateurldialog.cpp
|
||||
updatenotifier.cpp
|
||||
|
||||
models/expandingheaderview.cpp
|
||||
models/models.cpp
|
||||
models/protocolitemmodel.cpp
|
||||
|
||||
scheduling/syncscheduler.cpp
|
||||
scheduling/etagwatcher.cpp
|
||||
|
||||
notifications/systemnotification.cpp
|
||||
notifications/systemnotificationmanager.cpp
|
||||
notifications/systemnotificationbackend.cpp
|
||||
|
||||
qmlutils.cpp
|
||||
)
|
||||
|
||||
# 3rd party code
|
||||
target_sources(QSferaGui PRIVATE ../3rdparty/QProgressIndicator/QProgressIndicator.cpp)
|
||||
|
||||
add_subdirectory(newwizard)
|
||||
add_subdirectory(folderwizard)
|
||||
|
||||
set_target_properties(QSferaGui PROPERTIES
|
||||
OUTPUT_NAME "QSferaGui"
|
||||
AUTOUIC ON
|
||||
AUTORCC ON
|
||||
)
|
||||
# for the generated qml module
|
||||
target_include_directories(QSferaGui PRIVATE models spaces creds)
|
||||
target_link_libraries(QSferaGui
|
||||
PUBLIC
|
||||
Qt::Widgets Qt::Network Qt::Xml Qt::Quick Qt::QuickWidgets Qt::QuickControls2
|
||||
libsync
|
||||
Qt6Keychain::Qt6Keychain
|
||||
)
|
||||
|
||||
apply_common_target_settings(QSferaGui)
|
||||
ecm_add_qml_module(QSferaGui
|
||||
URI eu.QSfera.gui
|
||||
VERSION 1.0
|
||||
NAMESPACE OCC
|
||||
GENERATE_PLUGIN_SOURCE
|
||||
QML_FILES
|
||||
qml/AccountBar.qml
|
||||
qml/AccountButton.qml
|
||||
qml/FolderDelegate.qml
|
||||
qml/FolderError.qml
|
||||
|
||||
qml/UpdateUrlDialog.qml
|
||||
|
||||
qml/credentials/Credentials.qml
|
||||
qml/credentials/OAuthCredentials.qml
|
||||
|
||||
spaces/qml/SpaceDelegate.qml
|
||||
spaces/qml/SpacesView.qml
|
||||
)
|
||||
|
||||
generate_export_header(QSferaGui
|
||||
EXPORT_MACRO_NAME QSFERA_GUI_EXPORT
|
||||
EXPORT_FILE_NAME qsferaguilib.h
|
||||
STATIC_DEFINE QSFERA_BUILT_AS_STATIC
|
||||
)
|
||||
add_subdirectory(spaces)
|
||||
|
||||
|
||||
add_subdirectory(socketapi)
|
||||
|
||||
target_include_directories(QSferaGui PUBLIC
|
||||
${CMAKE_SOURCE_DIR}/src/3rdparty/QProgressIndicator
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
IF( APPLE )
|
||||
target_sources(QSferaGui PRIVATE
|
||||
notifications/macnotifications.mm
|
||||
settingsdialog_mac.mm
|
||||
guiutility_mac.mm
|
||||
folderwatcher_mac.cpp)
|
||||
set_source_files_properties(guiutility_mac.mm PROPERTIES COMPILE_DEFINITIONS SOCKETAPI_TEAM_IDENTIFIER_PREFIX="${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}")
|
||||
elseif( WIN32 )
|
||||
target_sources(QSferaGui PRIVATE
|
||||
guiutility_win.cpp
|
||||
folderwatcher_win.cpp
|
||||
navigationpanehelper.cpp
|
||||
)
|
||||
if (TARGET SnoreToast::SnoreToastActions)
|
||||
target_sources(QSferaGui PRIVATE
|
||||
notifications/snoretoast.cpp)
|
||||
target_compile_definitions(QSferaGui PRIVATE WITH_SNORE_TOAST)
|
||||
target_link_libraries(QSferaGui PRIVATE SnoreToast::SnoreToastActions)
|
||||
endif()
|
||||
elseif(UNIX AND NOT APPLE)
|
||||
## handle DBUS for Fdo notifications
|
||||
target_link_libraries(QSferaGui PUBLIC Qt::DBus)
|
||||
target_sources(QSferaGui PRIVATE
|
||||
folderwatcher_linux.cpp
|
||||
guiutility_unix.cpp
|
||||
notifications/dbusnotifications.cpp
|
||||
)
|
||||
qt_add_dbus_interface(notifications_dbus_SRCS notifications/org.freedesktop.Notifications.xml dbusnotifications_interface)
|
||||
target_sources(QSferaGui PRIVATE ${notifications_dbus_SRCS})
|
||||
endif()
|
||||
|
||||
if(WITH_AUTO_UPDATER)
|
||||
add_subdirectory(updater)
|
||||
target_compile_definitions(QSferaGui PUBLIC $<BUILD_INTERFACE:WITH_AUTO_UPDATER>)
|
||||
endif()
|
||||
|
||||
add_executable(qsfera main.cpp)
|
||||
set_target_properties(qsfera PROPERTIES
|
||||
OUTPUT_NAME "${APPLICATION_EXECUTABLE}"
|
||||
AUTOUIC ON
|
||||
AUTORCC ON
|
||||
)
|
||||
apply_common_target_settings(qsfera)
|
||||
target_link_libraries(qsfera PUBLIC QSferaGui QSferaResources KDAB::kdsingleapplication )
|
||||
|
||||
MESSAGE(STATUS "QSFERA_SIDEBAR_ICONS: ${APPLICATION_ICON_NAME}: ${QSFERA_SIDEBAR_ICONS}")
|
||||
|
||||
ecm_add_app_icon(appIcons ICONS "${QSFERA_ICONS}" SIDEBAR_ICONS "${QSFERA_SIDEBAR_ICONS}" OUTFILE_BASENAME "${APPLICATION_ICON_NAME}")
|
||||
target_sources(qsfera PRIVATE ${appIcons})
|
||||
|
||||
if(NOT APPLE)
|
||||
if(WIN32)
|
||||
target_sources(qsfera PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/qsfera.exe.manifest)
|
||||
else()
|
||||
file(GLOB _icons "${OEM_THEME_DIR}/theme/colored/*-${APPLICATION_ICON_NAME}-icon.png")
|
||||
foreach(_file ${_icons})
|
||||
string(REPLACE "${OEM_THEME_DIR}/theme/colored/" "" _res ${_file})
|
||||
string(REPLACE "-${APPLICATION_ICON_NAME}-icon.png" "" _res ${_res})
|
||||
install(FILES ${_file} RENAME ${APPLICATION_ICON_NAME}.png DESTINATION ${KDE_INSTALL_DATADIR}/icons/hicolor/${_res}x${_res}/apps)
|
||||
endforeach(_file)
|
||||
endif()
|
||||
|
||||
else()
|
||||
target_sources(qsfera PRIVATE ${QSFERA_BUNDLED_RESOURCES})
|
||||
|
||||
set_source_files_properties(
|
||||
${QSFERA_BUNDLED_RESOURCES}
|
||||
PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
set_target_properties(qsfera PROPERTIES OUTPUT_NAME "${APPLICATION_SHORTNAME}" MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/MacOSXBundleInfo.plist)
|
||||
endif()
|
||||
|
||||
install(TARGETS qsfera QSferaGui ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
ecm_finalize_qml_module(QSferaGui DESTINATION ${KDE_INSTALL_QMLDIR})
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
configure_file(${CMAKE_SOURCE_DIR}/qsfera.desktop.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_EXECUTABLE}.desktop)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_EXECUTABLE}.desktop DESTINATION ${KDE_INSTALL_DATADIR}/applications )
|
||||
endif()
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${APPLICATION_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${APPLICATION_SHORTNAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${APPLICATION_REV_DOMAIN}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLongVersionString</key>
|
||||
<string>${APPLICATION_NAME} ${MIRALL_VERSION_STRING}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${MIRALL_VERSION_FULL}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${MIRALL_VERSION_STRING}</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>(C) 2025 OpenCloud GmbH
|
||||
(C) 2014-${MIRALL_VERSION_YEAR} ownCloud GmbH</string>
|
||||
<key>SUShowReleaseNotes</key>
|
||||
<false/>
|
||||
<key>SUPublicDSAKeyFile</key>
|
||||
<string>dsa_pub.pem</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>F9ZGRXJE7XbQb2Kt36hwaBO4rYjkXYHiNS5hd+MkyKY=</string>
|
||||
<key>SUAllowsAutomaticUpdates</key>
|
||||
<string>NO</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>CFBundleAllowMixedLocalizations</key>
|
||||
<true/>
|
||||
<key>UIDesignRequiresCompatibility</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#include "aboutdialog.h"
|
||||
#include "ui_aboutdialog.h"
|
||||
|
||||
#include "gui/guiutility.h"
|
||||
#include "libsync/theme.h"
|
||||
|
||||
#ifdef WITH_AUTO_UPDATER
|
||||
#include "libsync/configfile.h"
|
||||
#include "updater/ocupdater.h"
|
||||
#ifdef Q_OS_MAC
|
||||
// FIXME We should unify those, but Sparkle does everything behind the scene transparently
|
||||
#include "updater/sparkleupdater.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
#ifdef WITH_AUTO_UPDATER
|
||||
bool isTestPilotCloudTheme()
|
||||
{
|
||||
return OCC::Theme::instance()->appName() == QLatin1String("testpilotcloud");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
AboutDialog::AboutDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, ui(new Ui::AboutDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->aboutText->setText(Theme::instance()->about());
|
||||
ui->icon->setPixmap(Theme::instance()->aboutIcon().pixmap(256));
|
||||
ui->versionInfo->setText(Theme::instance()->aboutVersions(Theme::VersionFormat::RichText));
|
||||
|
||||
connect(ui->versionInfo, &QTextBrowser::anchorClicked, this, &AboutDialog::openBrowserFromUrl);
|
||||
connect(ui->aboutText, &QLabel::linkActivated, this, &AboutDialog::openBrowser);
|
||||
|
||||
setupUpdaterWidget();
|
||||
}
|
||||
|
||||
AboutDialog::~AboutDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void AboutDialog::openBrowser(const QString &s)
|
||||
{
|
||||
Utility::openBrowser(QUrl(s), this);
|
||||
}
|
||||
|
||||
void AboutDialog::openBrowserFromUrl(const QUrl &s)
|
||||
{
|
||||
return openBrowser(s.toString());
|
||||
}
|
||||
|
||||
void AboutDialog::setupUpdaterWidget()
|
||||
{
|
||||
#ifdef WITH_AUTO_UPDATER
|
||||
// non-standard update channels are only supported by the vanilla theme and the testpilotcloud theme
|
||||
if (!Resources::isVanillaTheme() && !isTestPilotCloudTheme()) {
|
||||
if (Utility::isMac()) {
|
||||
// Because we don't have any statusString from the SparkleUpdater anyway we can hide the whole thing
|
||||
ui->updaterWidget->hide();
|
||||
} else {
|
||||
ui->updateChannelLabel->hide();
|
||||
ui->updateChannel->hide();
|
||||
if (ConfigFile().updateChannel() != QLatin1String("stable")) {
|
||||
ConfigFile().setUpdateChannel(QStringLiteral("stable"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// we want to attach the known english identifiers which are also used within the configuration file as user data inside the data model
|
||||
// that way, when we intend to reset to the original selection when the dialog, we can look up the config file's stored value in the data model
|
||||
ui->updateChannel->addItem(QStringLiteral("stable"), QStringLiteral("stable"));
|
||||
if (!Resources::isVanillaTheme()) {
|
||||
ui->updateChannel->addItem(tr("beta"), QStringLiteral("beta"));
|
||||
}
|
||||
|
||||
if (!ConfigFile().skipUpdateCheck() && Updater::instance()) {
|
||||
// Channel selection
|
||||
ui->updateChannel->setCurrentIndex(ui->updateChannel->findData(ConfigFile().updateChannel()));
|
||||
connect(ui->updateChannel, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &AboutDialog::slotUpdateChannelChanged);
|
||||
|
||||
// Note: the sparkle-updater is not an OCUpdater
|
||||
if (auto *ocupdater = qobject_cast<OCUpdater *>(Updater::instance())) {
|
||||
auto updateInfo = [ocupdater, this] {
|
||||
QString statusString = ocupdater->statusString();
|
||||
switch (ocupdater->downloadState()) {
|
||||
case OCUpdater::Unknown:
|
||||
[[fallthrough]];
|
||||
case OCUpdater::CheckingServer:
|
||||
[[fallthrough]];
|
||||
case OCUpdater::UpToDate:
|
||||
// No update, leave the status string as is.
|
||||
break;
|
||||
case OCUpdater::Downloading:
|
||||
[[fallthrough]];
|
||||
case OCUpdater::DownloadComplete:
|
||||
[[fallthrough]];
|
||||
case OCUpdater::DownloadFailed:
|
||||
[[fallthrough]];
|
||||
case OCUpdater::DownloadTimedOut:
|
||||
[[fallthrough]];
|
||||
case OCUpdater::UpdateOnlyAvailableThroughSystem:
|
||||
statusString = QStringLiteral("Version %1 is available. %2").arg(ocupdater->availableVersionString(), statusString);
|
||||
break;
|
||||
}
|
||||
|
||||
ui->updateStateLabel->setText(statusString);
|
||||
ui->restartButton->setVisible(ocupdater->downloadState() == OCUpdater::DownloadComplete);
|
||||
};
|
||||
connect(ocupdater, &OCUpdater::downloadStateChanged, this, updateInfo);
|
||||
connect(ui->restartButton, &QAbstractButton::clicked, ocupdater, &Updater::applyUpdateAndRestart);
|
||||
updateInfo();
|
||||
}
|
||||
#ifdef HAVE_SPARKLE
|
||||
if (SparkleUpdater *sparkleUpdater = qobject_cast<SparkleUpdater *>(Updater::instance())) {
|
||||
ui->updateStateLabel->setText(sparkleUpdater->statusString());
|
||||
ui->restartButton->setVisible(false);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
ui->updaterWidget->hide();
|
||||
}
|
||||
#else
|
||||
ui->updaterWidget->hide();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AboutDialog::slotUpdateChannelChanged([[maybe_unused]] int index)
|
||||
{
|
||||
#ifdef WITH_AUTO_UPDATER
|
||||
QString channel;
|
||||
if (index < 0) {
|
||||
// invalid index, reset to stable
|
||||
channel = QStringLiteral("stable");
|
||||
} else {
|
||||
channel = ui->updateChannel->itemData(index).toString();
|
||||
}
|
||||
if (channel == ConfigFile().updateChannel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto msgBox = new QMessageBox(QMessageBox::Warning, tr("Change update channel?"),
|
||||
tr("<html>The update channel determines which client updates will be offered for installation.<ul>"
|
||||
"<li>\"stable\" contains only upgrades that are considered reliable</li>"
|
||||
"%1"
|
||||
"</ul>"
|
||||
"<br>⚠️Downgrades are not supported. If you switch to a stable channel this change will only be applied with the next major release.</html>")
|
||||
.arg(
|
||||
isTestPilotCloudTheme() ? tr("<li>\"beta\" may contain newer features and bugfixes, but have not yet been tested thoroughly</li>") : QString()),
|
||||
QMessageBox::NoButton, this);
|
||||
auto acceptButton = msgBox->addButton(tr("Change update channel"), QMessageBox::AcceptRole);
|
||||
msgBox->addButton(tr("Cancel"), QMessageBox::RejectRole);
|
||||
connect(msgBox, &QMessageBox::finished, msgBox, [this, channel, msgBox, acceptButton] {
|
||||
msgBox->deleteLater();
|
||||
if (msgBox->clickedButton() == acceptButton) {
|
||||
ConfigFile().setUpdateChannel(channel);
|
||||
if (OCUpdater *updater = qobject_cast<OCUpdater *>(Updater::instance())) {
|
||||
updater->setUpdateUrl(Updater::updateUrl());
|
||||
updater->checkForUpdate();
|
||||
}
|
||||
#if defined(Q_OS_MAC) && defined(HAVE_SPARKLE)
|
||||
else if (SparkleUpdater *updater = qobject_cast<SparkleUpdater *>(Updater::instance())) {
|
||||
updater->setUpdateUrl(Updater::updateUrl());
|
||||
updater->checkForUpdate();
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
const auto oldChannel = ui->updateChannel->findData(ConfigFile().updateChannel());
|
||||
Q_ASSERT(oldChannel >= 0);
|
||||
Q_ASSERT(oldChannel <= 1);
|
||||
ui->updateChannel->setCurrentIndex(oldChannel);
|
||||
}
|
||||
});
|
||||
msgBox->open();
|
||||
#endif
|
||||
}
|
||||
|
||||
} // OCC namespace
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#ifndef ABOUTDIALOG_H
|
||||
#define ABOUTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace Ui {
|
||||
class AboutDialog;
|
||||
}
|
||||
|
||||
class AboutDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AboutDialog(QWidget *parent = nullptr);
|
||||
~AboutDialog();
|
||||
|
||||
private:
|
||||
void openBrowser(const QString &s);
|
||||
void openBrowserFromUrl(const QUrl &s);
|
||||
void setupUpdaterWidget();
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotUpdateChannelChanged(int index);
|
||||
|
||||
private:
|
||||
Ui::AboutDialog *ui;
|
||||
};
|
||||
|
||||
}
|
||||
#endif // ABOUTDIALOG_H
|
||||
@@ -0,0 +1,218 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>OCC::AboutDialog</class>
|
||||
<widget class="QDialog" name="OCC::AboutDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>751</width>
|
||||
<height>391</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string notr="true">About</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
<string>About</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="icon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>256</width>
|
||||
<height>256</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="aboutText">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="updaterWidget">
|
||||
<property name="title">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="updateChannelLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Update Channel</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>updateChannel</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="updateChannel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="updateStateLabel"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="restartButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Restart && Update</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>Versions</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="versionInfo">
|
||||
<property name="openExternalLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="openLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::StandardButton::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>tabWidget</tabstop>
|
||||
<tabstop>versionInfo</tabstop>
|
||||
<tabstop>updateChannel</tabstop>
|
||||
<tabstop>restartButton</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>OCC::AboutDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>OCC::AboutDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "account.h"
|
||||
#include "configfile.h"
|
||||
#include "creds/credentialmanager.h"
|
||||
#include "guiutility.h"
|
||||
#include <creds/httpcredentialsgui.h>
|
||||
#include <theme.h>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "common/utility_win.h"
|
||||
#include "gui/navigationpanehelper.h"
|
||||
#endif
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
auto urlC()
|
||||
{
|
||||
return QStringLiteral("url");
|
||||
}
|
||||
|
||||
auto defaultSyncRootC()
|
||||
{
|
||||
return QStringLiteral("default_sync_root");
|
||||
}
|
||||
|
||||
const QString davUserDisplyNameC()
|
||||
{
|
||||
return QStringLiteral("display-name");
|
||||
}
|
||||
|
||||
const QString userUUIDC()
|
||||
{
|
||||
return QStringLiteral("uuid");
|
||||
}
|
||||
|
||||
auto caCertsKeyC()
|
||||
{
|
||||
return QStringLiteral("CaCertificates");
|
||||
}
|
||||
|
||||
auto accountsC()
|
||||
{
|
||||
return QStringLiteral("Accounts");
|
||||
}
|
||||
|
||||
auto capabilitesC()
|
||||
{
|
||||
return QStringLiteral("capabilities");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcAccountManager, "gui.account.manager", QtInfoMsg)
|
||||
|
||||
AccountManager *AccountManager::instance()
|
||||
{
|
||||
static AccountManager instance;
|
||||
return &instance;
|
||||
}
|
||||
|
||||
AccountManager *AccountManager::create(QQmlEngine *qmlEngine, QJSEngine *)
|
||||
{
|
||||
Q_ASSERT(qmlEngine->thread() == AccountManager::instance()->thread());
|
||||
QJSEngine::setObjectOwnership(AccountManager::instance(), QJSEngine::CppOwnership);
|
||||
return instance();
|
||||
}
|
||||
|
||||
bool AccountManager::restore()
|
||||
{
|
||||
auto settings = ConfigFile::makeQSettings();
|
||||
if (settings.status() != QSettings::NoError) {
|
||||
qCWarning(lcAccountManager) << u"Could not read settings from" << settings.fileName() << settings.status();
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto size = settings.beginReadArray(accountsC());
|
||||
for (auto i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
auto urlConfig = settings.value(urlC());
|
||||
if (!urlConfig.isValid()) {
|
||||
// No URL probably means a corrupted entry in the account settings
|
||||
qCWarning(lcAccountManager) << u"No URL for account " << settings.group();
|
||||
continue;
|
||||
}
|
||||
|
||||
auto acc = createAccount(settings.value(userUUIDC(), QVariant::fromValue(QUuid::createUuid())).toUuid());
|
||||
|
||||
acc->setUrl(urlConfig.toUrl());
|
||||
|
||||
acc->_displayName = settings.value(davUserDisplyNameC()).toString();
|
||||
|
||||
auto capabilities = settings.value(capabilitesC()).value<QVariantMap>();
|
||||
capabilities.insert(u"cached"_s, true); // mark as cached capabilities, this will trigger a capabilitiesChanged signal once we got real capabilities
|
||||
|
||||
acc->setCapabilities({acc->url(), capabilities});
|
||||
acc->setDefaultSyncRoot(settings.value(defaultSyncRootC()).toString());
|
||||
|
||||
acc->setCredentials(new HttpCredentialsGui);
|
||||
|
||||
// now the server cert
|
||||
const auto certs = QSslCertificate::fromData(settings.value(caCertsKeyC()).toByteArray());
|
||||
qCInfo(lcAccountManager) << u"Restored: " << certs.count() << u" unknown certs.";
|
||||
acc->setApprovedCerts(certs);
|
||||
|
||||
if (auto accState = AccountState::loadFromSettings(acc, settings)) {
|
||||
addAccountState(std::move(accState));
|
||||
}
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AccountManager::save()
|
||||
{
|
||||
auto settings = ConfigFile::makeQSettings();
|
||||
settings.remove(accountsC());
|
||||
settings.beginWriteArray(accountsC(), _accounts.size());
|
||||
|
||||
int i = 0;
|
||||
for (const auto &accountState : std::as_const(_accounts)) {
|
||||
settings.setArrayIndex(i++);
|
||||
auto account = accountState->account();
|
||||
qCDebug(lcAccountManager) << u"Saving account" << account->url().toString();
|
||||
settings.setValue(urlC(), account->_url.toString());
|
||||
settings.setValue(davUserDisplyNameC(), account->_displayName);
|
||||
settings.setValue(userUUIDC(), account->uuid());
|
||||
if (account->hasCapabilities()) {
|
||||
settings.setValue(capabilitesC(), account->capabilities().raw());
|
||||
}
|
||||
if (account->hasDefaultSyncRoot()) {
|
||||
settings.setValue(defaultSyncRootC(), account->defaultSyncRoot());
|
||||
}
|
||||
|
||||
// Save accepted certificates.
|
||||
qCInfo(lcAccountManager) << u"Saving " << account->approvedCerts().count() << u" unknown certs.";
|
||||
const auto approvedCerts = account->approvedCerts();
|
||||
QByteArray certs;
|
||||
for (const auto &cert : approvedCerts) {
|
||||
certs += cert.toPem() + '\n';
|
||||
}
|
||||
if (!certs.isEmpty()) {
|
||||
settings.setValue(caCertsKeyC(), certs);
|
||||
}
|
||||
|
||||
// save the account state
|
||||
accountState->writeToSettings(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
qCInfo(lcAccountManager) << u"Saved all account settings";
|
||||
}
|
||||
|
||||
QStringList AccountManager::accountNames() const
|
||||
{
|
||||
QStringList accounts;
|
||||
accounts.reserve(AccountManager::instance()->accounts().size());
|
||||
for (const auto &a : AccountManager::instance()->accounts()) {
|
||||
accounts << a->account()->displayNameWithHost();
|
||||
}
|
||||
std::sort(accounts.begin(), accounts.end());
|
||||
return accounts;
|
||||
}
|
||||
|
||||
QList<AccountState *> AccountManager::accountsRaw() const
|
||||
{
|
||||
QList<AccountState *> out;
|
||||
out.reserve(_accounts.size());
|
||||
for (auto &x : _accounts.values()) {
|
||||
out.append(x);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
AccountStatePtr AccountManager::account(const QUuid uuid)
|
||||
{
|
||||
const auto acc = Utility::optionalFind(_accounts, uuid);
|
||||
if (OC_ENSURE(acc.has_value())) {
|
||||
return acc->value();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
AccountStatePtr AccountManager::addAccount(const AccountPtr &newAccount)
|
||||
{
|
||||
return addAccountState(AccountState::fromNewAccount(newAccount));
|
||||
}
|
||||
|
||||
void AccountManager::deleteAccount(AccountStatePtr account)
|
||||
{
|
||||
auto it = std::find(_accounts.begin(), _accounts.end(), account);
|
||||
if (it == _accounts.end()) {
|
||||
return;
|
||||
}
|
||||
// The argument keeps a strong reference to the AccountState, so we can safely remove other
|
||||
// AccountStatePtr occurrences:
|
||||
_accounts.erase(it);
|
||||
|
||||
if (account->account()->hasDefaultSyncRoot()) {
|
||||
Utility::unmarkDirectoryAsSyncRoot(account->account()->defaultSyncRoot());
|
||||
}
|
||||
|
||||
// Forget account credentials, cookies
|
||||
account->account()->credentials()->forgetSensitiveData();
|
||||
account->account()->credentialManager()->clear();
|
||||
|
||||
Q_EMIT accountRemoved(account);
|
||||
Q_EMIT accountsChanged();
|
||||
account->deleteLater();
|
||||
save();
|
||||
}
|
||||
|
||||
AccountPtr AccountManager::createAccount(const QUuid &uuid)
|
||||
{
|
||||
AccountPtr acc = Account::create(uuid);
|
||||
return acc;
|
||||
}
|
||||
|
||||
void AccountManager::shutdown()
|
||||
{
|
||||
const auto accounts = std::move(_accounts);
|
||||
for (const auto &acc : accounts) {
|
||||
Q_EMIT accountRemoved(acc);
|
||||
}
|
||||
qDeleteAll(accounts);
|
||||
}
|
||||
|
||||
AccountStatePtr AccountManager::addAccountState(std::unique_ptr<AccountState> &&accountState)
|
||||
{
|
||||
auto *rawAccount = accountState->account().data();
|
||||
connect(rawAccount, &Account::wantsAccountSaved, this, &AccountManager::save);
|
||||
|
||||
AccountStatePtr statePtr = accountState.release();
|
||||
_accounts.insert(statePtr->account()->uuid(), statePtr);
|
||||
Q_EMIT accountAdded(statePtr);
|
||||
Q_EMIT accountsChanged();
|
||||
return statePtr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include "account.h"
|
||||
#include "accountstate.h"
|
||||
|
||||
#include <QtQmlIntegration/QtQmlIntegration>
|
||||
|
||||
|
||||
class QJSEngine;
|
||||
class QQmlEngine;
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
@brief The AccountManager class
|
||||
@ingroup gui
|
||||
*/
|
||||
class QSFERA_GUI_EXPORT AccountManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QList<AccountState *> accounts READ accountsRaw() NOTIFY accountsChanged)
|
||||
QML_SINGLETON
|
||||
QML_ELEMENT
|
||||
public:
|
||||
static AccountManager *instance();
|
||||
|
||||
static AccountManager *create(QQmlEngine *qmlEngine, QJSEngine *);
|
||||
~AccountManager() override {}
|
||||
|
||||
/**
|
||||
* Saves the accounts
|
||||
*/
|
||||
void save();
|
||||
|
||||
/**
|
||||
* Creates account objects from settings.
|
||||
*
|
||||
* Returns false if there was an error reading the settings,
|
||||
* but note that settings not existing is not an error.
|
||||
*/
|
||||
bool restore();
|
||||
|
||||
/**
|
||||
* Add this account in the list of saved accounts.
|
||||
* Typically called from the wizard
|
||||
*/
|
||||
AccountStatePtr addAccount(const AccountPtr &newAccount);
|
||||
|
||||
/**
|
||||
* remove all accounts
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* Return a list of all accounts.
|
||||
*/
|
||||
const QList<AccountStatePtr> accounts() { return _accounts.values(); }
|
||||
|
||||
/**
|
||||
* Return the account state pointer for an account identified by its display name
|
||||
*/
|
||||
AccountStatePtr account(const QUuid uuid);
|
||||
|
||||
/**
|
||||
* Delete the AccountState
|
||||
*/
|
||||
void deleteAccount(AccountStatePtr account);
|
||||
|
||||
|
||||
/**
|
||||
* Creates an account and sets up some basic handlers.
|
||||
* Does *not* add the account to the account manager just yet.
|
||||
*/
|
||||
static AccountPtr createAccount(const QUuid &uuid);
|
||||
|
||||
/**
|
||||
* Returns a sorted list of displayNames
|
||||
*/
|
||||
QStringList accountNames() const;
|
||||
|
||||
private:
|
||||
// expose raw pointers to qml
|
||||
QList<AccountState *> accountsRaw() const;
|
||||
|
||||
// Adds an account to the tracked list, emitting accountAdded()
|
||||
AccountStatePtr addAccountState(std::unique_ptr<AccountState> &&accountState);
|
||||
|
||||
Q_SIGNALS:
|
||||
void accountAdded(AccountStatePtr account);
|
||||
void accountRemoved(AccountStatePtr account);
|
||||
void accountsChanged();
|
||||
|
||||
private:
|
||||
AccountManager() {}
|
||||
|
||||
QMap<QUuid, AccountStatePtr> _accounts;
|
||||
/// Account ids from settings that weren't read
|
||||
QSet<QString> _additionalBlockedAccountIds;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "accountmodalwidget.h"
|
||||
#include "ui_accountmodalwidget.h"
|
||||
|
||||
#include "gui/qmlutils.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
AccountModalWidget::AccountModalWidget(const QString &title, QWidget *widget, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, ui(new Ui::AccountModalWidget)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->groupBox->setTitle(title);
|
||||
ui->groupBox->layout()->addWidget(widget);
|
||||
|
||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &AccountModalWidget::accept);
|
||||
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &AccountModalWidget::reject);
|
||||
}
|
||||
|
||||
|
||||
AccountModalWidget::AccountModalWidget(const QString &title, const QUrl &qmlSource, QObject *qmlContext, QWidget *parent)
|
||||
: AccountModalWidget(
|
||||
title,
|
||||
[&] {
|
||||
auto *out = new QmlUtils::OCQuickWidget;
|
||||
out->setOCContext(qmlSource, parent, qmlContext, QJSEngine::JavaScriptOwnership);
|
||||
return out;
|
||||
}(),
|
||||
parent)
|
||||
{
|
||||
}
|
||||
void AccountModalWidget::setStandardButtons(QDialogButtonBox::StandardButtons buttons)
|
||||
{
|
||||
ui->buttonBox->setStandardButtons(buttons);
|
||||
}
|
||||
|
||||
QPushButton *AccountModalWidget::addButton(const QString &text, QDialogButtonBox::ButtonRole role)
|
||||
{
|
||||
return ui->buttonBox->addButton(text, role);
|
||||
}
|
||||
|
||||
void AccountModalWidget::accept()
|
||||
{
|
||||
Q_EMIT accepted();
|
||||
Q_EMIT finished(Result::Accepted);
|
||||
}
|
||||
|
||||
void AccountModalWidget::reject()
|
||||
{
|
||||
Q_EMIT rejected();
|
||||
Q_EMIT finished(Result::Rejected);
|
||||
}
|
||||
|
||||
} // OCC
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qmlutils.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace Ui {
|
||||
class AccountModalWidget;
|
||||
}
|
||||
|
||||
class AccountModalWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AccountModalWidget(const QString &title, QWidget *widget, QWidget *parent);
|
||||
AccountModalWidget(const QString &title, const QUrl &qmlSource, QObject *qmlContext, QWidget *parent);
|
||||
|
||||
enum class Result { Rejected, Accepted };
|
||||
Q_ENUM(Result)
|
||||
|
||||
void setStandardButtons(QDialogButtonBox::StandardButtons buttons);
|
||||
QPushButton *addButton(const QString &text, QDialogButtonBox::ButtonRole role);
|
||||
|
||||
public Q_SLOTS:
|
||||
void accept();
|
||||
void reject();
|
||||
|
||||
Q_SIGNALS:
|
||||
void accepted();
|
||||
void rejected();
|
||||
void finished(Result result);
|
||||
|
||||
private:
|
||||
Ui::AccountModalWidget *ui;
|
||||
};
|
||||
|
||||
} // OCC
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>OCC::AccountModalWidget</class>
|
||||
<widget class="QWidget" name="OCC::AccountModalWidget">
|
||||
<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="leftMargin">
|
||||
<number>25</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>25</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>GroupBox</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2"/>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::StandardButton::NoButton</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,519 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
#include "accountsettings.h"
|
||||
#include "ui_accountsettings.h"
|
||||
|
||||
|
||||
#include "account.h"
|
||||
#include "accountmanager.h"
|
||||
#include "accountstate.h"
|
||||
#include "application.h"
|
||||
#include "common/restartmanager.h"
|
||||
#include "commonstrings.h"
|
||||
#include "configfile.h"
|
||||
#include "folderman.h"
|
||||
#include "folderstatusmodel.h"
|
||||
#include "folderwizard/folderwizard.h"
|
||||
#include "gui/accountmodalwidget.h"
|
||||
#include "gui/fonticonmessagebox.h"
|
||||
#include "gui/models/models.h"
|
||||
#include "gui/networkinformation.h"
|
||||
#include "gui/notifications/systemnotificationmanager.h"
|
||||
#include "gui/qmlutils.h"
|
||||
#include "gui/selectivesyncwidget.h"
|
||||
#include "gui/spaces/spaceimageprovider.h"
|
||||
#include "gui/updateurldialog.h"
|
||||
#include "libsync/graphapi/spacesmanager.h"
|
||||
#include "libsync/syncresult.h"
|
||||
#include "networkjobs/jsonjob.h"
|
||||
#include "resources/fonticon.h"
|
||||
#include "scheduling/syncscheduler.h"
|
||||
#include "settingsdialog.h"
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QtQuickWidgets/QtQuickWidgets>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
constexpr auto modalWidgetStretchedMarginC = 50;
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcAccountSettings, "gui.account.settings", QtInfoMsg)
|
||||
|
||||
AccountSettings::AccountSettings(const AccountStatePtr &accountState, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, ui(new Ui::AccountSettings)
|
||||
, _accountState(accountState)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
_model = new FolderStatusModel(this);
|
||||
_model->setAccountState(_accountState);
|
||||
|
||||
auto weightedModel = new QSortFilterProxyModel(this);
|
||||
weightedModel->setSourceModel(_model);
|
||||
weightedModel->setSortRole(static_cast<int>(FolderStatusModel::Roles::Priority));
|
||||
weightedModel->sort(0, Qt::DescendingOrder);
|
||||
|
||||
_sortModel = weightedModel;
|
||||
|
||||
ui->quickWidget->engine()->addImageProvider(QStringLiteral("space"), new Spaces::SpaceImageProvider(_accountState->account()));
|
||||
ui->quickWidget->setOCContext(QUrl(QStringLiteral("qrc:/qt/qml/eu/QSfera/gui/qml/FolderDelegate.qml")), this);
|
||||
|
||||
connect(FolderMan::instance(), &FolderMan::folderListChanged, _model, &FolderStatusModel::resetFolders);
|
||||
|
||||
|
||||
connect(_accountState->account().data(), &Account::requestUrlUpdate, this, [this](const QUrl &newUrl) {
|
||||
if (_updateUrlDialog) {
|
||||
return;
|
||||
}
|
||||
auto *updateUrlDialog = UpdateUrlDialog::fromAccount(_accountState->account(), newUrl, ocApp()->settingsDialog());
|
||||
_updateUrlDialog = updateUrlDialog;
|
||||
|
||||
connect(updateUrlDialog, &UpdateUrlDialog::accepted, this, [newUrl, this]() {
|
||||
_accountState->account()->setUrl(newUrl);
|
||||
Q_EMIT _accountState->account()->wantsAccountSaved(_accountState->account().data());
|
||||
// reload the spaces
|
||||
RestartManager::requestRestart();
|
||||
});
|
||||
auto *modalWidget = new AccountModalWidget(updateUrlDialog->windowTitle(), updateUrlDialog, ocApp()->settingsDialog());
|
||||
connect(updateUrlDialog, &UpdateUrlDialog::accepted, modalWidget, &AccountModalWidget::accept);
|
||||
connect(updateUrlDialog, &UpdateUrlDialog::rejected, modalWidget, &AccountModalWidget::reject);
|
||||
addModalWidget(modalWidget);
|
||||
});
|
||||
|
||||
connect(_accountState.data(), &AccountState::stateChanged, this, &AccountSettings::slotAccountStateChanged);
|
||||
slotAccountStateChanged();
|
||||
|
||||
connect(_accountState.get(), &AccountState::isSettingUpChanged, this, [this] {
|
||||
if (_accountState->isSettingUp()) {
|
||||
ui->spinner->startAnimation();
|
||||
ui->stackedWidget->setCurrentWidget(ui->loadingPage);
|
||||
} else {
|
||||
ui->spinner->stopAnimation();
|
||||
ui->stackedWidget->setCurrentWidget(ui->quickWidget);
|
||||
}
|
||||
});
|
||||
ui->stackedWidget->setCurrentWidget(ui->quickWidget);
|
||||
|
||||
auto *notificationsPollTimer = new QTimer(_accountState);
|
||||
notificationsPollTimer->setInterval(1min);
|
||||
notificationsPollTimer->start();
|
||||
connect(notificationsPollTimer, &QTimer::timeout, this, &AccountSettings::updateNotifications);
|
||||
}
|
||||
|
||||
void AccountSettings::slotToggleSignInState()
|
||||
{
|
||||
if (_accountState->isSignedOut()) {
|
||||
_accountState->signIn();
|
||||
} else {
|
||||
_accountState->signOutByUi();
|
||||
}
|
||||
}
|
||||
|
||||
void AccountSettings::markNotificationsRead()
|
||||
{
|
||||
if (!_notifications.isEmpty()) {
|
||||
auto *job = Notification::dismissAllNotifications(_accountState->account(), _notifications, this);
|
||||
connect(job, &JsonApiJob::finishedSignal, this, &AccountSettings::updateNotifications);
|
||||
job->start();
|
||||
}
|
||||
}
|
||||
|
||||
void AccountSettings::showSelectiveSyncDialog(Folder *folder)
|
||||
{
|
||||
auto *selectiveSync = new SelectiveSyncWidget(_accountState->account(), this);
|
||||
selectiveSync->setDavUrl(folder->webDavUrl());
|
||||
bool ok;
|
||||
selectiveSync->setFolderInfo(folder->displayName(), folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok));
|
||||
Q_ASSERT(ok);
|
||||
|
||||
auto *modalWidget = new AccountModalWidget(tr("Choose what to sync"), selectiveSync, this);
|
||||
modalWidget->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
|
||||
connect(modalWidget, &AccountModalWidget::accepted, this, [selectiveSync, folder, this] {
|
||||
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, selectiveSync->createBlackList());
|
||||
doForceSyncCurrentFolder(folder);
|
||||
});
|
||||
addModalWidget(modalWidget);
|
||||
}
|
||||
|
||||
void AccountSettings::slotAddFolder()
|
||||
{
|
||||
FolderMan::instance()->setSyncEnabled(false); // do not start more syncs.
|
||||
|
||||
FolderWizard *folderWizard = new FolderWizard(_accountState, this);
|
||||
folderWizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
connect(folderWizard, &QDialog::accepted, this, &AccountSettings::slotFolderWizardAccepted);
|
||||
connect(folderWizard, &QDialog::rejected, this, [] {
|
||||
qCInfo(lcAccountSettings) << u"Folder wizard cancelled";
|
||||
FolderMan::instance()->setSyncEnabled(true);
|
||||
});
|
||||
|
||||
addModalLegacyDialog(folderWizard, AccountSettings::ModalWidgetSizePolicy::Expanding);
|
||||
}
|
||||
|
||||
|
||||
void AccountSettings::slotFolderWizardAccepted()
|
||||
{
|
||||
FolderWizard *folderWizard = qobject_cast<FolderWizard *>(sender());
|
||||
qCInfo(lcAccountSettings) << u"Folder wizard completed";
|
||||
|
||||
const auto config = folderWizard->result();
|
||||
|
||||
auto folder = FolderMan::instance()->addFolderFromFolderWizardResult(_accountState, config);
|
||||
|
||||
if (!config.selectiveSyncBlackList.isEmpty() && OC_ENSURE(folder && !config.useVirtualFiles)) {
|
||||
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, config.selectiveSyncBlackList);
|
||||
|
||||
// The user already accepted the selective sync dialog. everything is in the white list
|
||||
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, {QLatin1String("/")});
|
||||
}
|
||||
FolderMan::instance()->setSyncEnabled(true);
|
||||
FolderMan::instance()->scheduleAllFolders();
|
||||
}
|
||||
|
||||
void AccountSettings::slotRemoveCurrentFolder(Folder *folder)
|
||||
{
|
||||
// TODO: move to qml
|
||||
qCInfo(lcAccountSettings) << u"Remove Folder " << folder->path();
|
||||
auto messageBox = new FontIconMessageBox({Resources::FontIcon::DefaultGlyphes::Question}, tr("Confirm removal of Space"),
|
||||
tr("<p>Do you really want to stop syncing the Space »%1«?</p>"
|
||||
"<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
|
||||
.arg(folder->displayName()),
|
||||
QMessageBox::NoButton, ocApp()->settingsDialog());
|
||||
messageBox->setAttribute(Qt::WA_DeleteOnClose);
|
||||
QPushButton *yesButton = messageBox->addButton(tr("Remove Space"), QMessageBox::YesRole);
|
||||
messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
|
||||
connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, this] {
|
||||
if (messageBox->clickedButton() == yesButton) {
|
||||
FolderMan::instance()->removeFolder(folder);
|
||||
QTimer::singleShot(0, this, &AccountSettings::slotSpacesUpdated);
|
||||
}
|
||||
});
|
||||
messageBox->open();
|
||||
}
|
||||
|
||||
|
||||
void AccountSettings::showConnectionLabel(const QString &message, SyncResult::Status status, QStringList errors)
|
||||
{
|
||||
if (errors.isEmpty()) {
|
||||
_connectionLabel = message;
|
||||
} else {
|
||||
errors.prepend(message);
|
||||
const QString msg = errors.join(QLatin1String("\n"));
|
||||
qCDebug(lcAccountSettings) << msg;
|
||||
_connectionLabel = msg;
|
||||
}
|
||||
_accountStateIconGlype = SyncResult(status).glype();
|
||||
Q_EMIT connectionLabelChanged();
|
||||
}
|
||||
|
||||
void AccountSettings::slotEnableCurrentFolder(Folder *folder, bool terminate)
|
||||
{
|
||||
Q_ASSERT(folder);
|
||||
qCInfo(lcAccountSettings) << u"Application: enable folder" << folder->path();
|
||||
|
||||
// this sets the folder status to disabled but does not interrupt it.
|
||||
const bool currentlyPaused = folder->isSyncPaused();
|
||||
if (!currentlyPaused && !terminate) {
|
||||
// check if a sync is still running and if so, ask if we should terminate.
|
||||
if (FolderMan::instance()->scheduler()->currentSync() == folder) { // it's still running
|
||||
auto msgbox = new FontIconMessageBox(
|
||||
{u''}, tr("Sync Running"), tr("The sync operation is running.<br/>Do you want to stop it?"), QMessageBox::Yes | QMessageBox::No, this);
|
||||
msgbox->setAttribute(Qt::WA_DeleteOnClose);
|
||||
msgbox->setDefaultButton(QMessageBox::Yes);
|
||||
connect(msgbox, &QMessageBox::accepted, this, [folder = QPointer<Folder>(folder), this] {
|
||||
if (folder) {
|
||||
slotEnableCurrentFolder(folder, true);
|
||||
}
|
||||
});
|
||||
msgbox->open();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentlyPaused) {
|
||||
// ensure we don't forget about local errors
|
||||
folder->slotNextSyncFullLocalDiscovery();
|
||||
folder->setSyncPaused(false);
|
||||
FolderMan::instance()->scheduler()->enqueueFolder(folder);
|
||||
} else {
|
||||
// set paused to prevent reschedule
|
||||
folder->setSyncPaused(true);
|
||||
// terminate the run if we previously where syncing
|
||||
if (FolderMan::instance()->scheduler()->currentSync() == folder && terminate) {
|
||||
FolderMan::instance()->scheduler()->terminateCurrentSync(tr("Sync paused by user"));
|
||||
}
|
||||
}
|
||||
_model->slotUpdateFolderState(folder);
|
||||
}
|
||||
|
||||
void AccountSettings::slotForceSyncCurrentFolder(Folder *folder)
|
||||
{
|
||||
if (NetworkInformation::instance()->isMetered() && ConfigFile().pauseSyncWhenMetered()) {
|
||||
auto messageBox = new FontIconMessageBox({Resources::FontIcon::DefaultGlyphes::Question}, tr("Internet connection is metered"),
|
||||
tr("Synchronization is paused because the Internet connection is a metered connection"
|
||||
"<p>Do you really want to force a Synchronization now?"),
|
||||
QMessageBox::Yes | QMessageBox::No, ocApp()->settingsDialog());
|
||||
messageBox->setAttribute(Qt::WA_DeleteOnClose);
|
||||
connect(messageBox, &QMessageBox::accepted, this, [folder = QPointer<Folder>(folder), this] {
|
||||
if (folder) {
|
||||
doForceSyncCurrentFolder(folder);
|
||||
}
|
||||
});
|
||||
ocApp()->showSettings();
|
||||
messageBox->open();
|
||||
} else {
|
||||
doForceSyncCurrentFolder(folder);
|
||||
}
|
||||
}
|
||||
|
||||
void AccountSettings::doForceSyncCurrentFolder(Folder *selectedFolder)
|
||||
{
|
||||
// Prevent new sync starts
|
||||
FolderMan::instance()->scheduler()->stop();
|
||||
|
||||
// Terminate and reschedule any running sync
|
||||
if (auto *currentSync = FolderMan::instance()->scheduler()->currentSync()) {
|
||||
FolderMan::instance()->scheduler()->terminateCurrentSync(tr("User triggered force sync"));
|
||||
FolderMan::instance()->scheduler()->enqueueFolder(currentSync);
|
||||
}
|
||||
|
||||
selectedFolder->slotWipeErrorBlacklist(); // issue #6757
|
||||
selectedFolder->slotNextSyncFullLocalDiscovery(); // ensure we don't forget about local errors
|
||||
|
||||
// Insert the selected folder at the front of the queue
|
||||
FolderMan::instance()->scheduler()->enqueueFolder(selectedFolder, SyncScheduler::Priority::High);
|
||||
|
||||
// Restart scheduler
|
||||
FolderMan::instance()->scheduler()->start();
|
||||
}
|
||||
void AccountSettings::updateNotifications()
|
||||
{
|
||||
if (_accountState->isConnected()) {
|
||||
auto *job = Notification::createNotificationsJob(_accountState->account(), this);
|
||||
connect(job, &JsonApiJob::finishedSignal, this, [job, this] {
|
||||
const auto oldNotifications = _notifications;
|
||||
_notifications = Notification::getNotifications(job);
|
||||
auto newNotifications = _notifications;
|
||||
newNotifications.subtract(oldNotifications);
|
||||
for (const auto ¬ification : newNotifications) {
|
||||
SystemNotificationRequest notificationRequest(notification.title, notification.message, Resources::FontIcon(u''));
|
||||
notificationRequest.setButtons({tr("Mark as read")});
|
||||
if (auto *n = ocApp()->systemNotificationManager()->notify(std::move(notificationRequest))) {
|
||||
connect(n, &SystemNotification::buttonClicked, this, [notification, this](const QString &) {
|
||||
// we only have one button so we don't need to check which was clicked
|
||||
auto *job = Notification::dismissAllNotifications(_accountState->account(), {notification}, this);
|
||||
connect(job, &JsonApiJob::finishedSignal, this, &AccountSettings::updateNotifications);
|
||||
job->start();
|
||||
});
|
||||
}
|
||||
}
|
||||
Q_EMIT notificationsChanged();
|
||||
});
|
||||
job->start();
|
||||
}
|
||||
}
|
||||
|
||||
void AccountSettings::slotAccountStateChanged()
|
||||
{
|
||||
const AccountState::State state = _accountState->state();
|
||||
const AccountPtr account = _accountState->account();
|
||||
qCDebug(lcAccountSettings) << u"Account state changed to" << state << u"for account" << account;
|
||||
|
||||
FolderMan *folderMan = FolderMan::instance();
|
||||
for (auto *folder : folderMan->folders()) {
|
||||
_model->slotUpdateFolderState(folder);
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case AccountState::Connected: {
|
||||
QStringList errors;
|
||||
auto icon = SyncResult::Success;
|
||||
if (account->serverSupportLevel() != Account::ServerSupportLevel::Supported) {
|
||||
errors << tr("The server version %1 is unsupported! Proceed at your own risk.").arg(account->capabilities().status().versionString());
|
||||
icon = SyncResult::Problem;
|
||||
}
|
||||
showConnectionLabel(tr("Connected"), icon, errors);
|
||||
connect(
|
||||
accountsState()->account()->spacesManager(), &GraphApi::SpacesManager::updated, this, &AccountSettings::slotSpacesUpdated, Qt::UniqueConnection);
|
||||
slotSpacesUpdated();
|
||||
updateNotifications();
|
||||
break;
|
||||
}
|
||||
case AccountState::SignedOut:
|
||||
showConnectionLabel(tr("Signed out"), SyncResult::Offline);
|
||||
break;
|
||||
case AccountState::Connecting:
|
||||
if (NetworkInformation::instance()->isBehindCaptivePortal()) {
|
||||
showConnectionLabel(tr("Captive portal prevents connections to the server."), SyncResult::Offline);
|
||||
} else if (NetworkInformation::instance()->isMetered() && ConfigFile().pauseSyncWhenMetered()) {
|
||||
showConnectionLabel(tr("Sync is paused due to metered internet connection"), SyncResult::Offline);
|
||||
} else {
|
||||
showConnectionLabel(tr("Connecting..."), SyncResult::Undefined);
|
||||
}
|
||||
break;
|
||||
case AccountState::Disconnected:
|
||||
showConnectionLabel(tr("Disconnected"), SyncResult::Offline);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AccountSettings::slotSpacesUpdated()
|
||||
{
|
||||
const uint64_t enabledSpaces = std::ranges::count_if(accountsState()->account()->spacesManager()->spaces(), [](const auto *s) { return !s->disabled(); });
|
||||
const uint64_t syncedSpaces = std::ranges::count_if(
|
||||
FolderMan::instance()->folders(), [this](const auto *f) { return f->accountState() == accountsState() && f->space() && !f->space()->disabled(); });
|
||||
const auto unsyncedSpaces = enabledSpaces - syncedSpaces;
|
||||
|
||||
if (_unsyncedSpaces != unsyncedSpaces) {
|
||||
_unsyncedSpaces = unsyncedSpaces;
|
||||
Q_EMIT unsyncedSpacesChanged();
|
||||
}
|
||||
if (_syncedSpaces != syncedSpaces) {
|
||||
_syncedSpaces = syncedSpaces;
|
||||
Q_EMIT syncedSpacesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
AccountSettings::~AccountSettings()
|
||||
{
|
||||
_goingDown = true;
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void AccountSettings::addModalLegacyDialog(QWidget *widget, ModalWidgetSizePolicy sizePolicy)
|
||||
{
|
||||
if (!widget->testAttribute(Qt::WA_DeleteOnClose)) { // DEBUG CODE! See https://github.com/owncloud/client/issues/11673
|
||||
// Early check to see if the attribute gets unset before the second/real check below
|
||||
qCWarning(lcAccountSettings) << u"Missing WA_DeleteOnClose! (1)" << widget->metaObject() << widget;
|
||||
}
|
||||
|
||||
// create a widget filling the stacked widget
|
||||
// this widget contains a wrapping group box with widget as content
|
||||
auto *outerWidget = new QWidget;
|
||||
auto *groupBox = new QGroupBox;
|
||||
|
||||
switch (sizePolicy) {
|
||||
case ModalWidgetSizePolicy::Expanding: {
|
||||
auto *outerLayout = new QHBoxLayout(outerWidget);
|
||||
outerLayout->setContentsMargins(modalWidgetStretchedMarginC, modalWidgetStretchedMarginC, modalWidgetStretchedMarginC, modalWidgetStretchedMarginC);
|
||||
outerLayout->addWidget(groupBox);
|
||||
auto *layout = new QHBoxLayout(groupBox);
|
||||
layout->addWidget(widget);
|
||||
} break;
|
||||
case ModalWidgetSizePolicy::Minimum: {
|
||||
auto *outerLayout = new QGridLayout(outerWidget);
|
||||
outerLayout->addWidget(groupBox, 0, 0, Qt::AlignCenter);
|
||||
auto *layout = new QHBoxLayout(groupBox);
|
||||
layout->addWidget(widget);
|
||||
} break;
|
||||
}
|
||||
groupBox->setTitle(widget->windowTitle());
|
||||
|
||||
ui->stackedWidget->addWidget(outerWidget);
|
||||
ui->stackedWidget->setCurrentWidget(outerWidget);
|
||||
|
||||
// the widget is supposed to behave like a dialog and we connect to its destuction
|
||||
if (!widget->testAttribute(Qt::WA_DeleteOnClose)) { // DEBUG CODE! See https://github.com/owncloud/client/issues/11673
|
||||
qCWarning(lcAccountSettings) << u"Missing WA_DeleteOnClose! (2)" << widget->metaObject() << widget;
|
||||
}
|
||||
Q_ASSERT(widget->testAttribute(Qt::WA_DeleteOnClose));
|
||||
connect(widget, &QWidget::destroyed, this, [this, outerWidget] {
|
||||
outerWidget->deleteLater();
|
||||
if (!_goingDown) {
|
||||
ocApp()->settingsDialog()->ceaseModality(_accountState->account().get());
|
||||
}
|
||||
});
|
||||
widget->setVisible(true);
|
||||
ocApp()->settingsDialog()->requestModality(_accountState->account().get());
|
||||
}
|
||||
|
||||
void AccountSettings::addModalWidget(AccountModalWidget *widget)
|
||||
{
|
||||
ui->stackedWidget->addWidget(widget);
|
||||
ui->stackedWidget->setCurrentWidget(widget);
|
||||
|
||||
connect(widget, &AccountModalWidget::finished, this, [widget, this] {
|
||||
widget->deleteLater();
|
||||
ocApp()->settingsDialog()->ceaseModality(_accountState->account().get());
|
||||
});
|
||||
ocApp()->settingsDialog()->requestModality(_accountState->account().get());
|
||||
}
|
||||
|
||||
uint64_t AccountSettings::unsyncedSpaces() const
|
||||
{
|
||||
return _unsyncedSpaces;
|
||||
}
|
||||
|
||||
uint64_t AccountSettings::syncedSpaces() const
|
||||
{
|
||||
return _syncedSpaces;
|
||||
}
|
||||
|
||||
auto AccountSettings::model() const
|
||||
{
|
||||
return _sortModel;
|
||||
}
|
||||
|
||||
QString AccountSettings::connectionLabel()
|
||||
{
|
||||
return _connectionLabel;
|
||||
}
|
||||
|
||||
QChar AccountSettings::accountStateIconGlype()
|
||||
{
|
||||
return _accountStateIconGlype;
|
||||
}
|
||||
|
||||
const QSet<Notification> &AccountSettings::notifications() const
|
||||
{
|
||||
return _notifications;
|
||||
}
|
||||
|
||||
void AccountSettings::slotDeleteAccount()
|
||||
{
|
||||
// Deleting the account potentially deletes 'this', so
|
||||
// the QMessageBox should be destroyed before that happens.
|
||||
auto messageBox = new FontIconMessageBox({Resources::FontIcon::DefaultGlyphes::Question}, tr("Confirm Account Removal"),
|
||||
tr("<p>Do you really want to remove the connection to the account %1«?</p>"
|
||||
"<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
|
||||
.arg(_accountState->account()->displayNameWithHost()),
|
||||
QMessageBox::NoButton, this);
|
||||
auto yesButton = messageBox->addButton(tr("Remove connection"), QMessageBox::YesRole);
|
||||
messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
|
||||
messageBox->setAttribute(Qt::WA_DeleteOnClose);
|
||||
connect(messageBox, &QMessageBox::finished, this, [this, messageBox, yesButton]{
|
||||
if (messageBox->clickedButton() == yesButton) {
|
||||
auto manager = AccountManager::instance();
|
||||
// sign out to stop new syncs from being scheduled
|
||||
_accountState->signOutByUi();
|
||||
if (FolderMan::instance()->scheduler()->currentSync() && FolderMan::instance()->scheduler()->currentSync()->accountState() == _accountState) {
|
||||
// the message is usually not user visible
|
||||
FolderMan::instance()->scheduler()->terminateCurrentSync(u"Account about to be removed"_s);
|
||||
}
|
||||
manager->deleteAccount(_accountState);
|
||||
}
|
||||
});
|
||||
messageBox->open();
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include "folder.h"
|
||||
#include "gui/notifications.h"
|
||||
#include "gui/qmlutils.h"
|
||||
#include "progressdispatcher.h"
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QWidget>
|
||||
|
||||
class QModelIndex;
|
||||
class QNetworkReply;
|
||||
class QLabel;
|
||||
|
||||
namespace OCC {
|
||||
class AccountModalWidget;
|
||||
|
||||
namespace Ui {
|
||||
class AccountSettings;
|
||||
}
|
||||
|
||||
class FolderMan;
|
||||
|
||||
class Account;
|
||||
class AccountState;
|
||||
class FolderStatusModel;
|
||||
class FolderStatusDelegate;
|
||||
|
||||
/**
|
||||
* @brief The AccountSettings class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class QSFERA_GUI_EXPORT AccountSettings : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(AccountState *accountState MEMBER _accountState CONSTANT)
|
||||
Q_PROPERTY(QSortFilterProxyModel *model MEMBER _sortModel CONSTANT)
|
||||
Q_PROPERTY(uint64_t unsyncedSpaces READ unsyncedSpaces NOTIFY unsyncedSpacesChanged)
|
||||
Q_PROPERTY(uint64_t syncedSpaces READ syncedSpaces NOTIFY syncedSpacesChanged)
|
||||
Q_PROPERTY(QString connectionLabel READ connectionLabel NOTIFY connectionLabelChanged)
|
||||
Q_PROPERTY(QChar accountStateIconGlype READ accountStateIconGlype NOTIFY connectionLabelChanged)
|
||||
Q_PROPERTY(QSet<Notification> notifications READ notifications NOTIFY notificationsChanged)
|
||||
OC_DECLARE_WIDGET_FOCUS
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("C++ only")
|
||||
|
||||
public:
|
||||
enum class ModalWidgetSizePolicy { Minimum = QSizePolicy::Minimum, Expanding = QSizePolicy::Expanding };
|
||||
Q_ENUM(ModalWidgetSizePolicy)
|
||||
|
||||
explicit AccountSettings(const AccountStatePtr &accountState, QWidget *parent = nullptr);
|
||||
~AccountSettings() override;
|
||||
|
||||
AccountStatePtr accountsState() const { return _accountState; }
|
||||
|
||||
void addModalLegacyDialog(QWidget *widget, ModalWidgetSizePolicy sizePolicy);
|
||||
void addModalWidget(AccountModalWidget *widget);
|
||||
|
||||
uint64_t unsyncedSpaces() const;
|
||||
uint64_t syncedSpaces() const;
|
||||
|
||||
auto model() const;
|
||||
|
||||
QString connectionLabel();
|
||||
QChar accountStateIconGlype();
|
||||
|
||||
const QSet<Notification> ¬ifications() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void showIssuesList();
|
||||
void unsyncedSpacesChanged();
|
||||
void syncedSpacesChanged();
|
||||
void connectionLabelChanged();
|
||||
void notificationsChanged();
|
||||
|
||||
public Q_SLOTS:
|
||||
void slotAccountStateChanged();
|
||||
void slotSpacesUpdated();
|
||||
|
||||
protected Q_SLOTS:
|
||||
void slotAddFolder();
|
||||
void slotEnableCurrentFolder(Folder *folder, bool terminate = false);
|
||||
void slotForceSyncCurrentFolder(Folder *folder);
|
||||
void slotRemoveCurrentFolder(Folder *folder);
|
||||
void showSelectiveSyncDialog(Folder *folder);
|
||||
void slotFolderWizardAccepted();
|
||||
void slotDeleteAccount();
|
||||
void slotToggleSignInState();
|
||||
void markNotificationsRead();
|
||||
|
||||
private:
|
||||
void showConnectionLabel(const QString &message, SyncResult::Status status, QStringList errors = {});
|
||||
|
||||
void doForceSyncCurrentFolder(Folder *selectedFolder);
|
||||
|
||||
void updateNotifications();
|
||||
|
||||
Ui::AccountSettings *ui;
|
||||
|
||||
FolderStatusModel *_model;
|
||||
QSortFilterProxyModel *_sortModel;
|
||||
AccountStatePtr _accountState;
|
||||
// are we already in the destructor
|
||||
bool _goingDown = false;
|
||||
uint64_t _syncedSpaces = 0;
|
||||
uint64_t _unsyncedSpaces = 0;
|
||||
QString _connectionLabel;
|
||||
QChar _accountStateIconGlype;
|
||||
|
||||
QSet<Notification> _notifications;
|
||||
|
||||
QPointer<QWidget> _updateUrlDialog;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>OCC::AccountSettings</class>
|
||||
<widget class="QWidget" name="OCC::AccountSettings">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>678</width>
|
||||
<height>557</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<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="QStackedWidget" name="stackedWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>5</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="loadingPage">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Preparing the account</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QProgressIndicator" name="spinner" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="OCC::QmlUtils::OCQuickWidget" name="quickWidget">
|
||||
<property name="accessibleName">
|
||||
<string>Sync connections</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>OCC::QmlUtils::OCQuickWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>gui/qmlutils.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QProgressIndicator</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>QProgressIndicator.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,541 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "accountstate.h"
|
||||
#include "account.h"
|
||||
#include "application.h"
|
||||
#include "configfile.h"
|
||||
#include "fetchserversettings.h"
|
||||
#include "fonticon.h"
|
||||
#include "guiutility.h"
|
||||
|
||||
#include "libsync/creds/abstractcredentials.h"
|
||||
#include "libsync/creds/httpcredentials.h"
|
||||
|
||||
#include "gui/folderman.h"
|
||||
#include "gui/fonticonmessagebox.h"
|
||||
#include "gui/networkinformation.h"
|
||||
#include "gui/settingsdialog.h"
|
||||
#include "gui/tlserrordialog.h"
|
||||
|
||||
#include "logger.h"
|
||||
#include "socketapi/socketapi.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QRandomGenerator>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
|
||||
using namespace std::chrono;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
|
||||
// How often we check the server for changed settings
|
||||
auto fetchSettingsTimeout = 1h;
|
||||
|
||||
const QLatin1String userExplicitlySignedOutC()
|
||||
{
|
||||
return QLatin1String("userExplicitlySignedOut");
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcAccountState, "gui.account.state", QtInfoMsg)
|
||||
|
||||
|
||||
AccountState::AccountState(AccountPtr account)
|
||||
: QObject()
|
||||
, _account(account)
|
||||
, _queueGuard(_account->jobQueue())
|
||||
, _state(AccountState::Disconnected)
|
||||
, _connectionStatus(ConnectionValidator::Undefined)
|
||||
, _waitingForNewCredentials(false)
|
||||
{
|
||||
qRegisterMetaType<AccountState *>("AccountState*");
|
||||
|
||||
connect(account.data(), &Account::invalidCredentials,
|
||||
this, &AccountState::slotInvalidCredentials);
|
||||
connect(account.data(), &Account::credentialsFetched,
|
||||
this, &AccountState::slotCredentialsFetched);
|
||||
connect(account.data(), &Account::credentialsAsked,
|
||||
this, &AccountState::slotCredentialsAsked);
|
||||
connect(account.data(), &Account::unknownConnectionState, this, [this] { checkConnectivity(true); });
|
||||
|
||||
connect(account.data(), &Account::capabilitiesChanged, this, [this] {
|
||||
if (_account->capabilities().checkForUpdates() && isOcApp()) {
|
||||
ocApp()->updateNotifier()->checkForUpdates(_account);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
connect(NetworkInformation::instance(), &NetworkInformation::reachabilityChanged, this, [this](NetworkInformation::Reachability reachability) {
|
||||
switch (reachability) {
|
||||
case NetworkInformation::Reachability::Online:
|
||||
[[fallthrough]];
|
||||
case NetworkInformation::Reachability::Site:
|
||||
[[fallthrough]];
|
||||
case NetworkInformation::Reachability::Unknown:
|
||||
// the connection might not yet be established
|
||||
QTimer::singleShot(0, this, [this] { checkConnectivity(false); });
|
||||
break;
|
||||
case NetworkInformation::Reachability::Disconnected:
|
||||
// explicitly set disconnected, this way a successful checkConnectivity call above will trigger a local discover
|
||||
if (state() != State::SignedOut) {
|
||||
setState(State::Disconnected);
|
||||
}
|
||||
[[fallthrough]];
|
||||
case NetworkInformation::Reachability::Local:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
connect(NetworkInformation::instance(), &NetworkInformation::isMeteredChanged, this, [this](bool isMetered) {
|
||||
if (ConfigFile().pauseSyncWhenMetered()) {
|
||||
if (state() == State::Connected && isMetered) {
|
||||
qCInfo(lcAccountState) << u"Network switched to a metered connection, setting account state to PausedDueToMetered";
|
||||
setState(State::Connecting);
|
||||
} else if (state() == State::Connecting && !isMetered) {
|
||||
qCInfo(lcAccountState) << u"Network switched to a NON-metered connection, setting account state to Connected";
|
||||
setState(State::Connected);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connect(NetworkInformation::instance(), &NetworkInformation::isBehindCaptivePortalChanged, this, [this](bool onoff) {
|
||||
if (onoff) {
|
||||
// Block jobs from starting: they will fail because of the captive portal.
|
||||
// Note: this includes the `Drives` jobs started periodically by the `SpacesManager`.
|
||||
_queueGuard.block();
|
||||
} else {
|
||||
// Empty the jobs queue before unblocking it. The client might have been behind a captive
|
||||
// portal for hours, so a whole bunch of jobs might have queued up. If we wouldn't
|
||||
// clear the queue, unleashing all those jobs might look like a DoS attack. Most of them
|
||||
// are also not very useful anymore (e.g. `Drives` jobs), and the important ones (PUT jobs)
|
||||
// will be rescheduled by a directory scan.
|
||||
_account->jobQueue()->clear();
|
||||
_queueGuard.unblock();
|
||||
}
|
||||
|
||||
// A direct connect is not possible, because then the state parameter of `isBehindCaptivePortalChanged`
|
||||
// would become the `verifyServerState` argument to `checkConnectivity`.
|
||||
// The call is also made for when we "go behind" a captive portal. That ensures that not
|
||||
// only the status is set to `Connecting`, but also makes the UI show that syncing is paused.
|
||||
QTimer::singleShot(0, this, [this] { checkConnectivity(false); });
|
||||
});
|
||||
if (NetworkInformation::instance()->isBehindCaptivePortal()) {
|
||||
_queueGuard.block();
|
||||
}
|
||||
|
||||
// as a fallback and to recover after server issues we also poll
|
||||
auto timer = new QTimer(this);
|
||||
timer->setInterval(ConnectionValidator::DefaultCallingInterval);
|
||||
connect(timer, &QTimer::timeout, this, [this] { checkConnectivity(false); });
|
||||
timer->start();
|
||||
|
||||
connect(account->credentials(), &AbstractCredentials::requestLogout, this, [this] {
|
||||
setState(State::SignedOut);
|
||||
});
|
||||
|
||||
if (FolderMan::instance()) {
|
||||
FolderMan::instance()->socketApi()->registerAccount(account);
|
||||
}
|
||||
|
||||
connect(account.data(), &Account::appProviderErrorOccured, this, [](const QString &error) {
|
||||
QMessageBox *msgBox =
|
||||
new FontIconMessageBox({Resources::FontIcon::DefaultGlyphes::Warning}, Theme::instance()->appNameGUI(), error, {}, ocApp()->settingsDialog());
|
||||
msgBox->setAttribute(Qt::WA_DeleteOnClose);
|
||||
ocApp()->showSettings();
|
||||
msgBox->open();
|
||||
});
|
||||
}
|
||||
|
||||
AccountState::~AccountState() { }
|
||||
|
||||
std::unique_ptr<AccountState> AccountState::loadFromSettings(AccountPtr account, const QSettings &settings)
|
||||
{
|
||||
auto accountState = std::unique_ptr<AccountState>(new AccountState(account));
|
||||
const bool userExplicitlySignedOut = settings.value(userExplicitlySignedOutC(), false).toBool();
|
||||
if (userExplicitlySignedOut) {
|
||||
// see writeToSettings below
|
||||
accountState->setState(SignedOut);
|
||||
}
|
||||
return accountState;
|
||||
}
|
||||
|
||||
std::unique_ptr<AccountState> AccountState::fromNewAccount(AccountPtr account)
|
||||
{
|
||||
return std::unique_ptr<AccountState>(new AccountState(account));
|
||||
}
|
||||
|
||||
void AccountState::writeToSettings(QSettings &settings) const
|
||||
{
|
||||
// The SignedOut state is the only state where the client should *not* ask for credentials, nor
|
||||
// try to connect to the server. All other states should transition to Connected by either
|
||||
// (re-)trying to make a connection, or by authenticating (AskCredentials). So we save the
|
||||
// SignedOut state to indicate that the client should not try to re-connect the next time it
|
||||
// is started.
|
||||
settings.setValue(userExplicitlySignedOutC(), _state == SignedOut);
|
||||
}
|
||||
|
||||
AccountPtr AccountState::account() const
|
||||
{
|
||||
return _account;
|
||||
}
|
||||
|
||||
ConnectionValidator::Status AccountState::connectionStatus() const
|
||||
{
|
||||
return _connectionStatus;
|
||||
}
|
||||
|
||||
QStringList AccountState::connectionErrors() const
|
||||
{
|
||||
return _connectionErrors;
|
||||
}
|
||||
|
||||
AccountState::State AccountState::state() const
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
|
||||
void AccountState::setState(State state)
|
||||
{
|
||||
const bool stateHasChanged = state != _state;
|
||||
if (stateHasChanged) {
|
||||
const State oldState = _state;
|
||||
qCInfo(lcAccountState) << u"AccountState state change: " << _state << u"->" << state;
|
||||
_state = state;
|
||||
|
||||
if (_state == SignedOut) {
|
||||
_connectionStatus = ConnectionValidator::Undefined;
|
||||
_connectionErrors.clear();
|
||||
} else if (oldState == SignedOut && _state == Disconnected) {
|
||||
// If we stop being voluntarily signed-out, try to connect and
|
||||
// auth right now!
|
||||
checkConnectivity();
|
||||
} else if (_state == Connected) {
|
||||
if ((NetworkInformation::instance()->isMetered() && ConfigFile().pauseSyncWhenMetered())
|
||||
|| NetworkInformation::instance()->isBehindCaptivePortal()) {
|
||||
_state = Connecting;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// might not have changed but the underlying _connectionErrors might have
|
||||
if (state == Connected) {
|
||||
QTimer::singleShot(0, this, [stateHasChanged, this] {
|
||||
// ensure the connection validator is done
|
||||
_queueGuard.unblock();
|
||||
|
||||
// update capabilites and fetch relevant settings
|
||||
if (!_fetchServerSettingsJob && _fetchCapabilitiesElapsedTimer.duration() > fetchSettingsTimeout) {
|
||||
_fetchServerSettingsJob = new FetchServerSettingsJob(account(), this);
|
||||
connect(_fetchServerSettingsJob, &FetchServerSettingsJob::finishedSignal, this, [stateHasChanged, this] {
|
||||
_fetchServerSettingsJob->deleteLater();
|
||||
// clear the guard to make readyForSync return true
|
||||
_fetchServerSettingsJob.clear();
|
||||
_fetchCapabilitiesElapsedTimer.reset();
|
||||
if (stateHasChanged) {
|
||||
Q_EMIT isConnectedChanged();
|
||||
}
|
||||
});
|
||||
_fetchServerSettingsJob->start();
|
||||
} else if (stateHasChanged) {
|
||||
Q_EMIT isConnectedChanged();
|
||||
}
|
||||
});
|
||||
} else if (stateHasChanged) {
|
||||
Q_EMIT isConnectedChanged();
|
||||
}
|
||||
|
||||
if (stateHasChanged) {
|
||||
Q_EMIT stateChanged(_state);
|
||||
}
|
||||
}
|
||||
|
||||
bool AccountState::isSignedOut() const
|
||||
{
|
||||
return _state == SignedOut;
|
||||
}
|
||||
|
||||
void AccountState::signOutByUi()
|
||||
{
|
||||
account()->credentials()->forgetSensitiveData();
|
||||
account()->clearCookieJar();
|
||||
setState(SignedOut);
|
||||
// persist that we are signed out
|
||||
Q_EMIT account()->wantsAccountSaved(account().data());
|
||||
}
|
||||
|
||||
void AccountState::signIn()
|
||||
{
|
||||
if (_state == SignedOut) {
|
||||
_waitingForNewCredentials = false;
|
||||
setState(Disconnected);
|
||||
// persist that we are no longer signed out
|
||||
Q_EMIT account()->wantsAccountSaved(account().data());
|
||||
}
|
||||
}
|
||||
|
||||
bool AccountState::isConnected() const
|
||||
{
|
||||
return _state == Connected;
|
||||
}
|
||||
|
||||
void AccountState::tagLastSuccessfullETagRequest(const QDateTime &tp)
|
||||
{
|
||||
_timeOfLastETagCheck = tp;
|
||||
}
|
||||
|
||||
void AccountState::checkConnectivity(bool blockJobs)
|
||||
{
|
||||
if (isSignedOut() || _waitingForNewCredentials) {
|
||||
return;
|
||||
}
|
||||
qCInfo(lcAccountState) << u"checkConnectivity blocking:" << blockJobs << account()->displayNameWithHost();
|
||||
if (_state != Connected) {
|
||||
setState(Connecting);
|
||||
}
|
||||
if (_tlsDialog) {
|
||||
qCDebug(lcAccountState) << u"Skip checkConnectivity, waiting for tls dialog";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (_connectionValidator && blockJobs && !_queueGuard.queue()->isBlocked()) {
|
||||
// abort already running non blocking validator
|
||||
_connectionValidator->deleteLater();
|
||||
_connectionValidator.clear();
|
||||
}
|
||||
if (_connectionValidator) {
|
||||
qCWarning(lcAccountState) << u"ConnectionValidator already running, ignoring" << account()->displayNameWithHost() << u"Queue is blocked:"
|
||||
<< _queueGuard.queue()->isBlocked();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we never fetched credentials, do that now - otherwise connection attempts
|
||||
// make little sense.
|
||||
if (!account()->credentials()->wasFetched()) {
|
||||
_waitingForNewCredentials = true;
|
||||
account()->credentials()->fetchFromKeychain();
|
||||
}
|
||||
if (account()->hasCapabilities()) {
|
||||
// IF the account is connected the connection check can be skipped
|
||||
// if the last successful etag check job is not so long ago.
|
||||
// TODO: https://github.com/owncloud/client/issues/10935
|
||||
const auto pta = account()->capabilities().remotePollInterval();
|
||||
const auto polltime = duration_cast<seconds>(ConfigFile().remotePollInterval(pta));
|
||||
const auto elapsed = _timeOfLastETagCheck.secsTo(QDateTime::currentDateTimeUtc());
|
||||
if (!blockJobs && isConnected() && _timeOfLastETagCheck.isValid()
|
||||
&& elapsed <= polltime.count()) {
|
||||
qCDebug(lcAccountState) << account()->displayNameWithHost() << u"The last ETag check succeeded within the last " << polltime.count() << u"s ("
|
||||
<< elapsed << u"s). No connection check needed!";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (blockJobs) {
|
||||
_queueGuard.block();
|
||||
}
|
||||
_connectionValidator = new ConnectionValidator(account());
|
||||
connect(_connectionValidator, &ConnectionValidator::connectionResult,
|
||||
this, &AccountState::slotConnectionValidatorResult);
|
||||
|
||||
connect(_connectionValidator, &ConnectionValidator::sslErrors, this, [blockJobs, this](const QList<QSslError> &errors) {
|
||||
if (NetworkInformation::instance()->isBehindCaptivePortal()) {
|
||||
return;
|
||||
}
|
||||
if (!_tlsDialog) {
|
||||
// ignore errors for already accepted certificates
|
||||
auto filteredErrors = _account->accessManager()->filterSslErrors(errors);
|
||||
if (!filteredErrors.isEmpty()) {
|
||||
_tlsDialog = new TlsErrorDialog(filteredErrors, _account->url().host(), ocApp()->settingsDialog());
|
||||
_tlsDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
QSet<QSslCertificate> certs;
|
||||
certs.reserve(filteredErrors.size());
|
||||
for (const auto &error : std::as_const(filteredErrors)) {
|
||||
certs << error.certificate();
|
||||
}
|
||||
connect(_tlsDialog, &TlsErrorDialog::accepted, _tlsDialog, [certs, blockJobs, this]() {
|
||||
_account->addApprovedCerts(certs);
|
||||
_tlsDialog.clear();
|
||||
// force a new _connectionValidator
|
||||
if (_connectionValidator) {
|
||||
_connectionValidator->deleteLater();
|
||||
_connectionValidator.clear();
|
||||
}
|
||||
checkConnectivity(blockJobs);
|
||||
});
|
||||
connect(_tlsDialog, &TlsErrorDialog::rejected, this, [certs, this]() {
|
||||
setState(SignedOut);
|
||||
});
|
||||
|
||||
ocApp()->showSettings();
|
||||
_tlsDialog->open();
|
||||
}
|
||||
}
|
||||
});
|
||||
ConnectionValidator::ValidationMode mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate;
|
||||
if (isConnected()) {
|
||||
// Use a small authed propfind as a minimal ping when we're
|
||||
// already connected.
|
||||
if (blockJobs) {
|
||||
_connectionValidator->setClearCookies(true);
|
||||
}
|
||||
mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate;
|
||||
} else {
|
||||
// Check the server and then the auth.
|
||||
if (_waitingForNewCredentials) {
|
||||
mode = ConnectionValidator::ValidationMode::ValidateServer;
|
||||
} else {
|
||||
_connectionValidator->setClearCookies(true);
|
||||
mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate;
|
||||
}
|
||||
}
|
||||
_connectionValidator->checkServer(mode);
|
||||
}
|
||||
|
||||
void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors)
|
||||
{
|
||||
if (isSignedOut()) {
|
||||
qCWarning(lcAccountState) << u"Signed out, ignoring" << status << _account->url().toString();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (status == ConnectionValidator::Connected && !_account->hasCapabilities()) {
|
||||
// this code should only be needed when upgrading from a < 3.0 release where capabilities where not cached
|
||||
// The last check was _waitingForNewCredentials = true so we only checked ValidateServer
|
||||
// now check again and fetch capabilities
|
||||
_connectionValidator->deleteLater();
|
||||
_connectionValidator.clear();
|
||||
checkConnectivity();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_connectionStatus != status) {
|
||||
qCInfo(lcAccountState) << u"AccountState connection status change: " << _connectionStatus << u"->" << status;
|
||||
_connectionStatus = status;
|
||||
}
|
||||
_connectionErrors = errors;
|
||||
switch (status) {
|
||||
case ConnectionValidator::Connected:
|
||||
setState(Connected);
|
||||
break;
|
||||
case ConnectionValidator::Undefined:
|
||||
[[fallthrough]];
|
||||
case ConnectionValidator::ServiceUnavailable:
|
||||
[[fallthrough]];
|
||||
case ConnectionValidator::Timeout:
|
||||
[[fallthrough]];
|
||||
case ConnectionValidator::StatusNotFound:
|
||||
// This can happen either because the server does not exist
|
||||
// or because we are having network issues. The latter one is
|
||||
// much more likely, so keep trying to connect.
|
||||
setState(Disconnected);
|
||||
break;
|
||||
case ConnectionValidator::CredentialsWrong:
|
||||
[[fallthrough]];
|
||||
case ConnectionValidator::CredentialsNotReady:
|
||||
slotInvalidCredentials();
|
||||
break;
|
||||
case ConnectionValidator::SslError:
|
||||
// handled with the tlsDialog
|
||||
break;
|
||||
case ConnectionValidator::CaptivePortal:
|
||||
setState(Connecting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AccountState::slotInvalidCredentials()
|
||||
{
|
||||
if (!_waitingForNewCredentials) {
|
||||
qCInfo(lcAccountState) << u"Invalid credentials for" << _account->url().toString();
|
||||
|
||||
_waitingForNewCredentials = true;
|
||||
if (account()->credentials()->ready()) {
|
||||
account()->credentials()->invalidateToken();
|
||||
}
|
||||
if (auto creds = qobject_cast<HttpCredentials *>(account()->credentials())) {
|
||||
qCInfo(lcAccountState) << u"refreshing oauth";
|
||||
if (creds->refreshAccessToken()) {
|
||||
return;
|
||||
}
|
||||
qCInfo(lcAccountState) << u"refreshing oauth failed";
|
||||
}
|
||||
qCInfo(lcAccountState) << u"restart oauth";
|
||||
account()->credentials()->restartOauth();
|
||||
setState(Connecting);
|
||||
}
|
||||
}
|
||||
|
||||
void AccountState::slotCredentialsFetched()
|
||||
{
|
||||
// Make a connection attempt, no matter whether the credentials are
|
||||
// ready or not - we want to check whether we can get an SSL connection
|
||||
// going before bothering the user for a password.
|
||||
qCInfo(lcAccountState) << u"Fetched credentials for" << _account->url().toString() << u"attempting to connect";
|
||||
_waitingForNewCredentials = false;
|
||||
checkConnectivity();
|
||||
}
|
||||
|
||||
void AccountState::slotCredentialsAsked()
|
||||
{
|
||||
qCInfo(lcAccountState) << u"Credentials asked for" << _account->url().toString() << u"are they ready?" << _account->credentials()->ready();
|
||||
|
||||
_waitingForNewCredentials = false;
|
||||
|
||||
if (!_account->credentials()->ready()) {
|
||||
// User canceled the connection or did not give a password
|
||||
setState(SignedOut);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_connectionValidator) {
|
||||
// When new credentials become available we always want to restart the
|
||||
// connection validation, even if it's currently running.
|
||||
_connectionValidator->deleteLater();
|
||||
_connectionValidator.clear();
|
||||
}
|
||||
|
||||
checkConnectivity();
|
||||
}
|
||||
|
||||
Account *AccountState::accountForQml() const
|
||||
{
|
||||
return _account.data();
|
||||
}
|
||||
|
||||
bool AccountState::isSettingUp() const
|
||||
{
|
||||
return _settingUp;
|
||||
}
|
||||
|
||||
void AccountState::setSettingUp(bool settingUp)
|
||||
{
|
||||
if (_settingUp != settingUp) {
|
||||
_settingUp = settingUp;
|
||||
Q_EMIT isSettingUpChanged();
|
||||
}
|
||||
}
|
||||
bool AccountState::readyForSync() const
|
||||
{
|
||||
return !_fetchServerSettingsJob && isConnected();
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include "connectionvalidator.h"
|
||||
#include "creds/abstractcredentials.h"
|
||||
#include "jobqueue.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QPointer>
|
||||
#include <QtQmlIntegration/QtQmlIntegration>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QDialog;
|
||||
class QMessageBox;
|
||||
class QSettings;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class Account;
|
||||
class TlsErrorDialog;
|
||||
class FetchServerSettingsJob;
|
||||
|
||||
class QSFERA_GUI_EXPORT AccountState : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(Account *account READ accountForQml CONSTANT)
|
||||
Q_PROPERTY(bool isConnected READ isConnected NOTIFY isConnectedChanged)
|
||||
Q_PROPERTY(AccountState::State state READ state NOTIFY stateChanged)
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Only created by AccountManager")
|
||||
|
||||
public:
|
||||
enum State : uint8_t {
|
||||
/// Not even attempting to connect, most likely because the
|
||||
/// user explicitly signed out or cancelled a credential dialog.
|
||||
SignedOut,
|
||||
|
||||
/// Account would like to be connected but hasn't heard back yet.
|
||||
Disconnected,
|
||||
|
||||
/// The account is successfully talking to the server.
|
||||
Connected,
|
||||
|
||||
Connecting
|
||||
};
|
||||
Q_ENUM(State)
|
||||
|
||||
|
||||
~AccountState() override;
|
||||
|
||||
/** Creates an account state from settings and an Account object.
|
||||
*
|
||||
* Use from AccountManager with a prepared QSettings object only.
|
||||
*/
|
||||
static std::unique_ptr<AccountState> loadFromSettings(AccountPtr account, const QSettings &settings);
|
||||
|
||||
static std::unique_ptr<AccountState> fromNewAccount(AccountPtr account);
|
||||
|
||||
/** Writes account state information to settings.
|
||||
*
|
||||
* It does not write the Account data.
|
||||
*/
|
||||
void writeToSettings(QSettings &settings) const;
|
||||
|
||||
AccountPtr account() const;
|
||||
|
||||
ConnectionValidator::Status connectionStatus() const;
|
||||
QStringList connectionErrors() const;
|
||||
|
||||
State state() const;
|
||||
|
||||
bool isSignedOut() const;
|
||||
|
||||
[[nodiscard]] bool readyForSync() const;
|
||||
|
||||
/** A user-triggered sign out which disconnects, stops syncs
|
||||
* for the account and forgets the password. */
|
||||
void signOutByUi();
|
||||
|
||||
/// Move from SignedOut state to Disconnected (attempting to connect)
|
||||
void signIn();
|
||||
|
||||
bool isConnected() const;
|
||||
|
||||
/** Mark the timestamp when the last successful ETag check happened for
|
||||
* this account.
|
||||
* The checkConnectivity() method uses the timestamp to save a call to
|
||||
* the server to validate the connection if the last successful etag job
|
||||
* was not so long ago.
|
||||
*/
|
||||
void tagLastSuccessfullETagRequest(const QDateTime &tp);
|
||||
|
||||
/***
|
||||
* The account is setup for the first time, this may take some time
|
||||
*/
|
||||
bool isSettingUp() const;
|
||||
void setSettingUp(bool settingUp);
|
||||
|
||||
public Q_SLOTS:
|
||||
/// Triggers a ping to the server to update state and
|
||||
/// connection status and errors.
|
||||
/// verifyServerState indicates that we must check the server
|
||||
void checkConnectivity(bool verifyServerState = false);
|
||||
|
||||
private:
|
||||
/// Use the account as parent
|
||||
explicit AccountState(AccountPtr account);
|
||||
|
||||
void setState(State state);
|
||||
|
||||
Q_SIGNALS:
|
||||
void stateChanged(State state);
|
||||
void isConnectedChanged();
|
||||
void isSettingUpChanged();
|
||||
|
||||
protected Q_SLOTS:
|
||||
void slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors);
|
||||
void slotInvalidCredentials();
|
||||
void slotCredentialsFetched();
|
||||
void slotCredentialsAsked();
|
||||
|
||||
private:
|
||||
Account *accountForQml() const;
|
||||
AccountPtr _account;
|
||||
JobQueueGuard _queueGuard;
|
||||
State _state;
|
||||
ConnectionValidator::Status _connectionStatus;
|
||||
QStringList _connectionErrors;
|
||||
bool _waitingForNewCredentials;
|
||||
QDateTime _timeOfLastETagCheck;
|
||||
QPointer<ConnectionValidator> _connectionValidator;
|
||||
QPointer<TlsErrorDialog> _tlsDialog;
|
||||
|
||||
bool _settingUp = false;
|
||||
|
||||
Utility::ChronoElapsedTimer _fetchCapabilitiesElapsedTimer = {false};
|
||||
// guard against multiple fetches
|
||||
QPointer<FetchServerSettingsJob> _fetchServerSettingsJob;
|
||||
};
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::AccountState *)
|
||||
Q_DECLARE_METATYPE(OCC::AccountStatePtr)
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "activitywidget.h"
|
||||
|
||||
|
||||
#include "issueswidget.h"
|
||||
#include "protocolwidget.h"
|
||||
#include "resources/fonticon.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include <QtGui>
|
||||
#include <QtWidgets>
|
||||
|
||||
|
||||
using namespace std::chrono;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
ActivitySettings::ActivitySettings(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
QHBoxLayout *hbox = new QHBoxLayout(this);
|
||||
setLayout(hbox);
|
||||
|
||||
// create a tab widget for the three activity views
|
||||
_tab = new QTabWidget(this);
|
||||
hbox->addWidget(_tab);
|
||||
|
||||
_protocolWidget = new ProtocolWidget(this);
|
||||
_tab->addTab(_protocolWidget, Resources::FontIcon(u''), tr("Local Activity"));
|
||||
|
||||
_issuesWidget = new IssuesWidget(this);
|
||||
const int issueTabId = _tab->addTab(_issuesWidget, Resources::FontIcon(u''), tr("Not Synced"));
|
||||
connect(_issuesWidget, &IssuesWidget::issueCountUpdated, this, [issueTabId, this](int issueCount) {
|
||||
QString cntText = tr("Not Synced");
|
||||
if (issueCount) {
|
||||
//: %1 is the number of not synced files.
|
||||
cntText = tr("Not Synced (%1)").arg(issueCount);
|
||||
}
|
||||
_tab->setTabText(issueTabId, cntText);
|
||||
});
|
||||
}
|
||||
|
||||
void ActivitySettings::slotShowIssuesTab()
|
||||
{
|
||||
_tab->setCurrentWidget(_issuesWidget);
|
||||
}
|
||||
|
||||
ActivitySettings::~ActivitySettings()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "account.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QTabWidget;
|
||||
|
||||
namespace OCC {
|
||||
class ProtocolWidget;
|
||||
class IssuesWidget;
|
||||
|
||||
|
||||
/**
|
||||
* @brief The ActivitySettings class
|
||||
* @ingroup gui
|
||||
*
|
||||
* Implements a tab for the settings dialog, displaying the three activity
|
||||
* lists.
|
||||
*/
|
||||
class ActivitySettings : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ActivitySettings(QWidget *parent = nullptr);
|
||||
~ActivitySettings() override;
|
||||
|
||||
public Q_SLOTS:
|
||||
void slotShowIssuesTab();
|
||||
|
||||
private:
|
||||
QTabWidget *_tab;
|
||||
|
||||
ProtocolWidget *_protocolWidget;
|
||||
IssuesWidget *_issuesWidget;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "application.h"
|
||||
|
||||
#include "account.h"
|
||||
#include "accountmanager.h"
|
||||
#include "accountstate.h"
|
||||
#include "common/version.h"
|
||||
#include "configfile.h"
|
||||
#include "folder.h"
|
||||
#include "folderman.h"
|
||||
#include "gui/aboutdialog.h"
|
||||
#include "gui/accountsettings.h"
|
||||
#include "gui/fetchserversettings.h"
|
||||
#include "gui/folderwizard/folderwizard.h"
|
||||
#include "gui/newwizard/setupwizardcontroller.h"
|
||||
#include "gui/notifications/systemnotification.h"
|
||||
#include "gui/notifications/systemnotificationmanager.h"
|
||||
#include "gui/systray.h"
|
||||
#include "libsync/graphapi/spacesmanager.h"
|
||||
#include "libsync/vfs/vfs.h"
|
||||
#include "resources/fonticon.h"
|
||||
#include "settingsdialog.h"
|
||||
#include "socketapi/socketapi.h"
|
||||
#include "theme.h"
|
||||
|
||||
#ifdef WITH_AUTO_UPDATER
|
||||
#include "updater/ocupdater.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include "gui/navigationpanehelper.h"
|
||||
#include <qt_windows.h>
|
||||
#endif
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QMenuBar>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
using namespace OCC;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcApplication, "gui.application", QtInfoMsg)
|
||||
|
||||
namespace {
|
||||
|
||||
void setUpInitialSyncFolder(AccountStatePtr accountStatePtr, bool useVfs)
|
||||
{
|
||||
// saves a bit of duplicate code
|
||||
auto addFolder = [accountStatePtr, useVfs](const QString &localFolder, const QUrl &davUrl, const QString &spaceId = {}, const QString &displayName = {}) {
|
||||
auto def = FolderDefinition{accountStatePtr->account()->uuid(), davUrl, spaceId, displayName};
|
||||
def.setLocalPath(localFolder);
|
||||
return FolderMan::instance()->addFolderFromWizard(accountStatePtr, std::move(def), useVfs);
|
||||
};
|
||||
|
||||
auto finalize = [accountStatePtr] {
|
||||
accountStatePtr->checkConnectivity();
|
||||
FolderMan::instance()->setSyncEnabled(true);
|
||||
FolderMan::instance()->scheduleAllFolders();
|
||||
};
|
||||
|
||||
QObject::connect(
|
||||
accountStatePtr->account()->spacesManager(), &GraphApi::SpacesManager::ready, accountStatePtr,
|
||||
[accountStatePtr, addFolder, finalize] {
|
||||
auto spaces = accountStatePtr->account()->spacesManager()->spaces();
|
||||
// we do not want to set up folder sync connections for disabled spaces (#10173)
|
||||
spaces.erase(std::remove_if(spaces.begin(), spaces.end(), [](auto *space) { return space->disabled(); }), spaces.end());
|
||||
|
||||
if (!spaces.isEmpty()) {
|
||||
const QString localDir(accountStatePtr->account()->defaultSyncRoot());
|
||||
FileSystem::setFolderMinimumPermissions(localDir);
|
||||
Utility::setupFavLink(localDir);
|
||||
for (const auto *space : spaces) {
|
||||
const QString name = space->displayName();
|
||||
const QString folderName = FolderMan::instance()->findGoodPathForNewSyncFolder(
|
||||
localDir, name, FolderMan::NewFolderType::SpacesFolder, accountStatePtr->account()->uuid());
|
||||
auto folder = addFolder(folderName, QUrl(space->drive().getRoot().getWebDavUrl()), space->drive().getRoot().getId(), name);
|
||||
folder->setPriority(space->priority());
|
||||
}
|
||||
finalize();
|
||||
}
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
accountStatePtr->account()->spacesManager()->checkReady();
|
||||
}
|
||||
}
|
||||
|
||||
QString Application::displayLanguage() const
|
||||
{
|
||||
return _displayLanguage;
|
||||
}
|
||||
|
||||
Application *Application::_instance = nullptr;
|
||||
|
||||
Application::Application(const QString &displayLanguage, bool debugMode)
|
||||
: _debugMode(debugMode)
|
||||
, _displayLanguage(displayLanguage)
|
||||
, _updateNotifier(new UpdateNotifier(this))
|
||||
{
|
||||
// ensure the singleton works
|
||||
{
|
||||
_instance = this;
|
||||
_settingsDialog = new SettingsDialog();
|
||||
_systray = new Systray(this);
|
||||
_systemNotificationManager = new SystemNotificationManager(this);
|
||||
}
|
||||
qCInfo(lcApplication) << u"Plugin search paths:" << qApp->libraryPaths();
|
||||
|
||||
// Check vfs plugins
|
||||
if (VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::Mode::Off) {
|
||||
qCWarning(lcApplication) << u"Theme wants to show vfs mode, but no vfs plugins are available";
|
||||
}
|
||||
qCInfo(lcApplication) << VfsPluginManager::instance().bestAvailableVfsMode() << u"plugin is available";
|
||||
|
||||
ConfigFile cfg;
|
||||
|
||||
// this should be called once during application startup to make sure we don't miss any messages
|
||||
cfg.configureHttpLogging();
|
||||
|
||||
// The timeout is initialized with an environment variable, if not, override with the value from the config
|
||||
if (AbstractNetworkJob::httpTimeout == AbstractNetworkJob::DefaultHttpTimeout) {
|
||||
AbstractNetworkJob::httpTimeout = cfg.timeout();
|
||||
}
|
||||
|
||||
qApp->setQuitOnLastWindowClosed(false);
|
||||
|
||||
connect(AccountManager::instance(), &AccountManager::accountAdded, this, &Application::slotAccountStateAdded);
|
||||
for (const auto &ai : AccountManager::instance()->accounts()) {
|
||||
slotAccountStateAdded(ai);
|
||||
}
|
||||
connect(_systemNotificationManager, &SystemNotificationManager::unknownNotificationClicked, this, &Application::showSettings);
|
||||
connect(
|
||||
_systemNotificationManager, &SystemNotificationManager::notificationFinished, this, [this](SystemNotification *, SystemNotification::Result result) {
|
||||
if (result == SystemNotification::Result::Clicked) {
|
||||
showSettings();
|
||||
}
|
||||
});
|
||||
|
||||
#ifdef WITH_AUTO_UPDATER
|
||||
// Update checks
|
||||
UpdaterScheduler *updaterScheduler = new UpdaterScheduler(this, this);
|
||||
// the updater scheduler takes care of connecting its GUI bits to other components
|
||||
(void)updaterScheduler;
|
||||
#endif
|
||||
|
||||
// Cleanup at Quit.
|
||||
connect(qApp, &QCoreApplication::aboutToQuit, this, &Application::slotCleanup);
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
// add About to the global menu
|
||||
QMenuBar *menuBar = new QMenuBar(nullptr);
|
||||
// the menu name is not displayed
|
||||
auto *menu = menuBar->addMenu(QString());
|
||||
// the actual name is provided by mac
|
||||
menu->addAction(QStringLiteral("About"), this, &Application::showAbout)->setMenuRole(QAction::AboutRole);
|
||||
#endif
|
||||
#ifdef Q_OS_WIN
|
||||
// update the existing sidebar entries
|
||||
NavigationPaneHelper::removeLegacyCloudStorageRegistry();
|
||||
#endif
|
||||
}
|
||||
|
||||
Application::~Application()
|
||||
{
|
||||
// Make sure all folders are gone, otherwise removing the
|
||||
// accounts will remove the associated folders from the settings.
|
||||
FolderMan::instance()->unloadAndDeleteAllFolders();
|
||||
}
|
||||
|
||||
void Application::slotAccountStateAdded(AccountStatePtr accountState) const
|
||||
{
|
||||
connect(accountState->account().data(), &Account::serverVersionChanged, ocApp(), [account = accountState->account().data()] {
|
||||
if (account->serverSupportLevel() != Account::ServerSupportLevel::Supported) {
|
||||
ocApp()->systemNotificationManager()->notify({tr("Unsupported Server Version"),
|
||||
tr("The server on account »%1« runs an unsupported version %2. "
|
||||
"Using this client with unsupported server versions is untested and "
|
||||
"potentially dangerous. Proceed at your own risk.")
|
||||
.arg(account->displayNameWithHost(), account->capabilities().status().versionString()),
|
||||
Resources::FontIcon(u'')});
|
||||
}
|
||||
});
|
||||
|
||||
// Hook up the folder manager slots to the account state's Q_SIGNALS:
|
||||
connect(accountState.data(), &AccountState::isConnectedChanged, FolderMan::instance(), &FolderMan::slotIsConnectedChanged);
|
||||
connect(accountState->account().data(), &Account::serverVersionChanged, FolderMan::instance(),
|
||||
[account = accountState->account().data()] { FolderMan::instance()->slotServerVersionChanged(account); });
|
||||
accountState->checkConnectivity();
|
||||
}
|
||||
|
||||
void Application::slotCleanup()
|
||||
{
|
||||
ConfigFile().saveGeometry(_settingsDialog);
|
||||
delete _settingsDialog;
|
||||
|
||||
// by now the credentials are supposed to be persisted
|
||||
// don't start async credentials jobs during shutdown
|
||||
AccountManager::instance()->save();
|
||||
|
||||
FolderMan::instance()->scheduler()->stop();
|
||||
FolderMan::instance()->scheduler()->terminateCurrentSync(tr("Application is shutting down"));
|
||||
FolderMan::instance()->unloadAndDeleteAllFolders();
|
||||
|
||||
// Remove the account from the account manager so it can be deleted.
|
||||
AccountManager::instance()->shutdown();
|
||||
}
|
||||
|
||||
AccountStatePtr Application::addNewAccount(AccountPtr newAccount)
|
||||
{
|
||||
auto *accountMan = AccountManager::instance();
|
||||
|
||||
// first things first: we need to add the new account
|
||||
auto accountStatePtr = accountMan->addAccount(newAccount);
|
||||
|
||||
// if one account is configured: enable autostart
|
||||
bool shouldSetAutoStart = (accountMan->accounts().size() == 1);
|
||||
#ifdef Q_OS_MAC
|
||||
// Don't auto start when not being 'installed'
|
||||
shouldSetAutoStart = shouldSetAutoStart && QCoreApplication::applicationDirPath().startsWith(QLatin1String("/Applications/"));
|
||||
#endif
|
||||
if (shouldSetAutoStart) {
|
||||
Utility::setLaunchOnStartup(Theme::instance()->appName(), Theme::instance()->appNameGUI(), true);
|
||||
}
|
||||
|
||||
// showing the UI to show the user that the account has been added successfully
|
||||
showSettings();
|
||||
|
||||
return accountStatePtr;
|
||||
}
|
||||
|
||||
void Application::showSettings()
|
||||
{
|
||||
auto window = ocApp()->settingsDialog();
|
||||
window->show();
|
||||
window->raise();
|
||||
window->activateWindow();
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
// Windows disallows raising a Window when you're not the active application.
|
||||
// Use a common hack to attach to the active application
|
||||
const auto activeProcessId = GetWindowThreadProcessId(GetForegroundWindow(), nullptr);
|
||||
if (activeProcessId != qApp->applicationPid()) {
|
||||
const auto threadId = GetCurrentThreadId();
|
||||
// don't step here with a debugger...
|
||||
if (AttachThreadInput(threadId, activeProcessId, true)) {
|
||||
const auto hwnd = reinterpret_cast<HWND>(window->winId());
|
||||
SetForegroundWindow(hwnd);
|
||||
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
AttachThreadInput(threadId, activeProcessId, false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SettingsDialog *Application::settingsDialog() const
|
||||
{
|
||||
return _settingsDialog;
|
||||
}
|
||||
|
||||
void Application::showAbout()
|
||||
{
|
||||
if (!_aboutDialog) {
|
||||
_aboutDialog = new AboutDialog(_settingsDialog);
|
||||
_aboutDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
_settingsDialog->addModalWidget(_aboutDialog);
|
||||
}
|
||||
}
|
||||
|
||||
SystemNotificationManager *Application::systemNotificationManager() const
|
||||
{
|
||||
return _systemNotificationManager;
|
||||
}
|
||||
|
||||
void Application::runNewAccountWizard()
|
||||
{
|
||||
// passing the settings dialog as parent makes sure the wizard will be shown above it
|
||||
// as the settingsDialog's lifetime spans across the entire application but the dialog will live much shorter,
|
||||
// we have to clean it up manually when finished() is emitted
|
||||
auto *wizardController = new Wizard::SetupWizardController(this->settingsDialog());
|
||||
|
||||
// while the wizard is shown, new syncs are disabled
|
||||
FolderMan::instance()->setSyncEnabled(false);
|
||||
|
||||
connect(
|
||||
wizardController, &Wizard::SetupWizardController::finished, this, [wizardController, this](const AccountPtr &newAccount, Wizard::SyncMode syncMode) {
|
||||
// note: while the wizard is shown, we disable the folder synchronization
|
||||
// previously we could perform this just here, but now we have to postpone this depending on whether selective sync was chosen
|
||||
// see also #9497
|
||||
|
||||
// when the dialog is closed before it has finished, there won't be a new account to set up
|
||||
// the wizard controller signalizes this by passing a null pointer
|
||||
if (!newAccount.isNull()) {
|
||||
// finally, call the slot that finalizes the setup
|
||||
auto accountStatePtr = ocApp()->addNewAccount(newAccount);
|
||||
accountStatePtr->setSettingUp(true);
|
||||
|
||||
_settingsDialog->setCurrentAccount(accountStatePtr->account().data());
|
||||
|
||||
// fetch server settings
|
||||
auto fetchServerSettings = new FetchServerSettingsJob(accountStatePtr->account(), accountStatePtr->account().data());
|
||||
|
||||
connect(fetchServerSettings, &FetchServerSettingsJob::finishedSignal, accountStatePtr.data(), [accountStatePtr, syncMode, this] {
|
||||
// saving once after adding makes sure the account is stored in the config in a working state
|
||||
// this is needed to ensure a consistent state in the config file upon unexpected terminations of the client
|
||||
// (for instance, when running from a debugger and stopping the process from there)
|
||||
AccountManager::instance()->save();
|
||||
|
||||
// the account is now ready, emulate a normal account loading and Q_EMIT that the credentials are ready
|
||||
Q_EMIT accountStatePtr->account()->credentialsFetched();
|
||||
|
||||
switch (syncMode) {
|
||||
case Wizard::SyncMode::SyncEverything:
|
||||
case Wizard::SyncMode::UseVfs: {
|
||||
bool useVfs = syncMode == Wizard::SyncMode::UseVfs;
|
||||
setUpInitialSyncFolder(accountStatePtr, useVfs);
|
||||
accountStatePtr->setSettingUp(false);
|
||||
break;
|
||||
}
|
||||
case Wizard::SyncMode::ConfigureUsingFolderWizard: {
|
||||
Q_ASSERT(!accountStatePtr->account()->hasDefaultSyncRoot());
|
||||
|
||||
auto *folderWizard = new FolderWizard(accountStatePtr, ocApp()->settingsDialog());
|
||||
folderWizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
// TODO: duplication of AccountSettings
|
||||
// adapted from AccountSettings::slotFolderWizardAccepted()
|
||||
connect(folderWizard, &QDialog::accepted, accountStatePtr.data(), [accountStatePtr, folderWizard]() {
|
||||
FolderMan *folderMan = FolderMan::instance();
|
||||
|
||||
qCInfo(lcApplication) << u"Folder wizard completed";
|
||||
const auto config = folderWizard->result();
|
||||
|
||||
auto folder = folderMan->addFolderFromFolderWizardResult(accountStatePtr, config);
|
||||
|
||||
if (!config.selectiveSyncBlackList.isEmpty() && OC_ENSURE(folder && !config.useVirtualFiles)) {
|
||||
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, config.selectiveSyncBlackList);
|
||||
|
||||
// The user already accepted the selective sync dialog. everything is in the white list
|
||||
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, {QLatin1String("/")});
|
||||
}
|
||||
|
||||
folderMan->setSyncEnabled(true);
|
||||
folderMan->scheduleAllFolders();
|
||||
accountStatePtr->setSettingUp(false);
|
||||
});
|
||||
|
||||
connect(folderWizard, &QDialog::rejected, accountStatePtr.data(), [accountStatePtr]() {
|
||||
qCInfo(lcApplication) << u"Folder wizard cancelled";
|
||||
FolderMan::instance()->setSyncEnabled(true);
|
||||
accountStatePtr->setSettingUp(false);
|
||||
});
|
||||
|
||||
_settingsDialog->accountSettings(accountStatePtr->account().get())
|
||||
->addModalLegacyDialog(folderWizard, AccountSettings::ModalWidgetSizePolicy::Expanding);
|
||||
break;
|
||||
}
|
||||
case OCC::Wizard::SyncMode::Invalid:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
});
|
||||
fetchServerSettings->start();
|
||||
} else {
|
||||
FolderMan::instance()->setSyncEnabled(true);
|
||||
}
|
||||
|
||||
// make sure the wizard is cleaned up eventually
|
||||
wizardController->deleteLater();
|
||||
});
|
||||
|
||||
// all we have to do is show the dialog...
|
||||
settingsDialog()->addModalWidget(wizardController->window());
|
||||
}
|
||||
|
||||
QSystemTrayIcon *Application::systemTrayIcon() const
|
||||
{
|
||||
return _systray;
|
||||
}
|
||||
|
||||
UpdateNotifier *Application::updateNotifier() const
|
||||
{
|
||||
return _updateNotifier;
|
||||
}
|
||||
|
||||
bool Application::debugMode()
|
||||
{
|
||||
return _debugMode;
|
||||
}
|
||||
|
||||
std::unique_ptr<Application> Application::createInstance(const QString &displayLanguage, bool debugMode)
|
||||
{
|
||||
Q_ASSERT(!_instance);
|
||||
// _instance will be set in the constructor
|
||||
new Application(displayLanguage, debugMode);
|
||||
return std::unique_ptr<Application>(_instance);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
#include "gui/updatenotifier.h"
|
||||
|
||||
#include "common/asserts.h"
|
||||
#include "libsync/accountfwd.h"
|
||||
|
||||
#include <QPointer>
|
||||
#include <QSystemTrayIcon>
|
||||
|
||||
namespace CrashReporter {
|
||||
class Handler;
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class SettingsDialog;
|
||||
class AboutDialog;
|
||||
class Systray;
|
||||
class SystemNotificationManager;
|
||||
|
||||
class QSFERA_GUI_EXPORT Application : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static std::unique_ptr<Application> createInstance(const QString &displayLanguage, bool debugMode);
|
||||
~Application();
|
||||
|
||||
bool debugMode();
|
||||
|
||||
QString displayLanguage() const;
|
||||
|
||||
AccountStatePtr addNewAccount(AccountPtr newAccount);
|
||||
|
||||
void showSettings();
|
||||
|
||||
SettingsDialog *settingsDialog() const;
|
||||
|
||||
void showAbout();
|
||||
|
||||
SystemNotificationManager *systemNotificationManager() const;
|
||||
|
||||
void runNewAccountWizard();
|
||||
|
||||
QSystemTrayIcon *systemTrayIcon() const;
|
||||
|
||||
UpdateNotifier *updateNotifier() const;
|
||||
|
||||
protected Q_SLOTS:
|
||||
void slotCleanup();
|
||||
void slotAccountStateAdded(AccountStatePtr accountState) const;
|
||||
|
||||
private:
|
||||
explicit Application(const QString &displayLanguage, bool debugMode);
|
||||
|
||||
const bool _debugMode = false;
|
||||
SettingsDialog *_settingsDialog = nullptr;
|
||||
|
||||
QString _displayLanguage;
|
||||
Systray *_systray;
|
||||
|
||||
UpdateNotifier *_updateNotifier;
|
||||
|
||||
SystemNotificationManager *_systemNotificationManager = nullptr;
|
||||
|
||||
// keeping a pointer on those dialogs allows us to make sure they will be shown only once
|
||||
QPointer<AboutDialog> _aboutDialog;
|
||||
|
||||
static Application *_instance;
|
||||
friend Application *ocApp();
|
||||
friend bool isOcApp();
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @return whether the Application singleton is available, this must always be true unless we are a unit test...
|
||||
*/
|
||||
inline bool isOcApp()
|
||||
{
|
||||
return Application::_instance;
|
||||
}
|
||||
|
||||
inline Application *ocApp()
|
||||
{
|
||||
Q_ASSERT(isOcApp());
|
||||
return Application::_instance;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#include "commonstrings.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
QString CommonStrings::fileBrowser()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
return QStringLiteral("Explorer");
|
||||
#elif defined(Q_OS_MAC)
|
||||
return QStringLiteral("Finder");
|
||||
#else
|
||||
return tr("file manager");
|
||||
#endif
|
||||
}
|
||||
|
||||
QString CommonStrings::showInFileBrowser(const QString &path)
|
||||
{
|
||||
if (path.isEmpty()) {
|
||||
return tr("Show in %1").arg(fileBrowser());
|
||||
}
|
||||
return tr("Show »%1« in %2").arg(path, fileBrowser());
|
||||
}
|
||||
|
||||
QString CommonStrings::showInWebBrowser()
|
||||
{
|
||||
return tr("Show in web browser");
|
||||
}
|
||||
|
||||
QString CommonStrings::copyToClipBoard()
|
||||
{
|
||||
return tr("Copy");
|
||||
}
|
||||
|
||||
QString CommonStrings::filterButtonText(int filterCount)
|
||||
{
|
||||
return tr("%n Filter(s)", nullptr, filterCount);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QtQml/QQmlEngine>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class CommonStrings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_SINGLETON
|
||||
QML_ELEMENT
|
||||
public:
|
||||
Q_INVOKABLE static QString fileBrowser();
|
||||
Q_INVOKABLE static QString showInFileBrowser(const QString &path = {});
|
||||
Q_INVOKABLE static QString showInWebBrowser();
|
||||
Q_INVOKABLE static QString copyToClipBoard();
|
||||
Q_INVOKABLE static QString filterButtonText(int filterCount);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#include "gui/connectionvalidator.h"
|
||||
|
||||
#include "gui/fetchserversettings.h"
|
||||
#include "gui/networkinformation.h"
|
||||
#include "libsync/account.h"
|
||||
#include "libsync/creds/abstractcredentials.h"
|
||||
#include "libsync/networkjobs.h"
|
||||
#include "libsync/networkjobs/checkserverjobfactory.h"
|
||||
#include "libsync/networkjobs/jsonjob.h"
|
||||
#include "libsync/theme.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QLoggingCategory>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QNetworkReply>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
|
||||
auto fetchSettingsTimeout()
|
||||
{
|
||||
return std::min<std::chrono::milliseconds>(20s, OCC::AbstractNetworkJob::httpTimeout);
|
||||
}
|
||||
}
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcConnectionValidator, "sync.connectionvalidator", QtInfoMsg)
|
||||
|
||||
|
||||
ConnectionValidator::ConnectionValidator(AccountPtr account, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _account(account)
|
||||
{
|
||||
// TODO: 6.0 abort validator on 5min timeout
|
||||
auto timer = new QTimer(this);
|
||||
timer->setInterval(30s);
|
||||
connect(timer, &QTimer::timeout, this,
|
||||
[this] { qCInfo(lcConnectionValidator) << u"ConnectionValidator" << _account->displayNameWithHost() << u"still running after" << _duration; });
|
||||
timer->start();
|
||||
}
|
||||
|
||||
void ConnectionValidator::setClearCookies(bool clearCookies)
|
||||
{
|
||||
_clearCookies = clearCookies;
|
||||
}
|
||||
|
||||
void ConnectionValidator::checkServer(ConnectionValidator::ValidationMode mode)
|
||||
{
|
||||
_mode = mode;
|
||||
qCDebug(lcConnectionValidator) << u"Checking server and authentication";
|
||||
|
||||
auto checkServerFactory = CheckServerJobFactory::createFromAccount(_account, _clearCookies, this);
|
||||
auto checkServerJob = checkServerFactory.startJob(_account->url(), this);
|
||||
|
||||
connect(checkServerJob->reply()->manager(), &AccessManager::sslErrors, this, [this](QNetworkReply *reply, const QList<QSslError> &errors) {
|
||||
Q_UNUSED(reply)
|
||||
Q_EMIT sslErrors(errors);
|
||||
});
|
||||
|
||||
connect(checkServerJob, &CoreJob::finished, this, [checkServerJob, this]() {
|
||||
if (checkServerJob->success()) {
|
||||
const auto result = checkServerJob->result().value<CheckServerJobResult>();
|
||||
|
||||
// adopt the new cookies
|
||||
_account->accessManager()->setCookieJar(checkServerJob->reply()->manager()->cookieJar());
|
||||
|
||||
slotStatusFound(result.serverUrl(), result.statusObject());
|
||||
} else {
|
||||
switch (checkServerJob->reply()->error()) {
|
||||
case QNetworkReply::OperationCanceledError:
|
||||
[[fallthrough]];
|
||||
case QNetworkReply::TimeoutError:
|
||||
qCWarning(lcConnectionValidator) << checkServerJob;
|
||||
_errors.append(tr("timeout"));
|
||||
reportResult(Timeout);
|
||||
return;
|
||||
case QNetworkReply::SslHandshakeFailedError:
|
||||
reportResult(NetworkInformation::instance()->isBehindCaptivePortal() ? CaptivePortal : SslError);
|
||||
return;
|
||||
case QNetworkReply::TooManyRedirectsError:
|
||||
reportResult(StatusNotFound);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
_account->credentials()->checkCredentials(checkServerJob->reply());
|
||||
_errors.append(checkServerJob->errorMessage());
|
||||
reportResult(StatusNotFound);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ConnectionValidator::slotStatusFound(const QUrl &url, const QJsonObject &info)
|
||||
{
|
||||
// status.php was found.
|
||||
qCInfo(lcConnectionValidator) << u"** Application found: " << url << u" with version " << info.value(QLatin1String("productversion")).toString();
|
||||
|
||||
// Update server URL in case of redirection
|
||||
if (_account->url() != url) {
|
||||
if (Utility::urlEqual(_account->url(), url)) {
|
||||
qCInfo(lcConnectionValidator()) << u"status.php was redirected to" << url.toString() << u"updating the account url";
|
||||
_account->setUrl(url);
|
||||
} else {
|
||||
qCInfo(lcConnectionValidator()) << u"status.php was redirected to" << url.toString() << u"asking user to accept and abort for now";
|
||||
Q_EMIT _account->requestUrlUpdate(url);
|
||||
reportResult(StatusNotFound);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AbstractCredentials *creds = _account->credentials();
|
||||
if (!creds->ready()) {
|
||||
reportResult(CredentialsNotReady);
|
||||
return;
|
||||
}
|
||||
// now check the authentication
|
||||
if (_mode != ConnectionValidator::ValidationMode::ValidateServer) {
|
||||
// the endpoint requires authentication
|
||||
auto *userJob = new JsonJob(_account, _account->url(), u"graph/v1.0/me"_s, "GET", nullptr);
|
||||
userJob->setAuthenticationJob(true);
|
||||
userJob->setTimeout(fetchSettingsTimeout());
|
||||
connect(userJob, &JsonApiJob::finishedSignal, this, [userJob, this] {
|
||||
if (userJob->timedOut()) {
|
||||
reportResult(ConnectionValidator::Timeout);
|
||||
} else if (userJob->httpStatusCode() == 200) {
|
||||
reportResult(ConnectionValidator::Connected);
|
||||
} else if (userJob->httpStatusCode() == 401) {
|
||||
reportResult(ConnectionValidator::CredentialsWrong);
|
||||
} else if (userJob->httpStatusCode() == 503) {
|
||||
reportResult(ConnectionValidator::ServiceUnavailable);
|
||||
} else if (userJob->reply()->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||
reportResult(NetworkInformation::instance()->isBehindCaptivePortal() ? ConnectionValidator::CaptivePortal : ConnectionValidator::SslError);
|
||||
} else {
|
||||
reportResult(ConnectionValidator::Undefined);
|
||||
}
|
||||
});
|
||||
userJob->start();
|
||||
return;
|
||||
} else {
|
||||
reportResult(Connected);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectionValidator::reportResult(Status status)
|
||||
{
|
||||
if (OC_ENSURE(!_finished)) {
|
||||
_finished = true;
|
||||
qCDebug(lcConnectionValidator) << status << _duration;
|
||||
Q_EMIT connectionResult(status, _errors);
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include "common/chronoelapsedtimer.h"
|
||||
#include "gui/guiutility.h"
|
||||
#include "libsync/accountfwd.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class QSFERA_GUI_EXPORT ConnectionValidator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ConnectionValidator(AccountPtr account, QObject *parent = nullptr);
|
||||
|
||||
enum class ValidationMode { ValidateServer, ValidateAuthAndUpdate };
|
||||
Q_ENUM(ValidationMode)
|
||||
|
||||
enum Status : uint8_t {
|
||||
Undefined,
|
||||
Connected,
|
||||
CredentialsNotReady, // Credentials aren't ready
|
||||
CredentialsWrong, // AuthenticationRequiredError
|
||||
SslError, // SSL handshake error, certificate rejected by user?
|
||||
StatusNotFound, // Error retrieving status.php
|
||||
ServiceUnavailable, // 503 on authed request
|
||||
Timeout, // actually also used for other errors on the authed request
|
||||
CaptivePortal, // We're stuck behind a captive portal and (will) get SSL certificate problems
|
||||
};
|
||||
Q_ENUM(Status)
|
||||
|
||||
// How often should the Application ask this object to check for the connection?
|
||||
static constexpr auto DefaultCallingInterval = std::chrono::seconds(62);
|
||||
|
||||
|
||||
/** Whether to clear the cookies before we start the CheckServerJob job
|
||||
* This option also depends on Theme::instance()->connectionValidatorClearCookies()
|
||||
*/
|
||||
void setClearCookies(bool clearCookies);
|
||||
|
||||
public Q_SLOTS:
|
||||
/// Checks the server and the authentication.
|
||||
void checkServer(ConnectionValidator::ValidationMode mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate);
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionResult(ConnectionValidator::Status status, const QStringList &errors);
|
||||
|
||||
void sslErrors(const QList<QSslError> &errors);
|
||||
|
||||
protected Q_SLOTS:
|
||||
void slotStatusFound(const QUrl &url, const QJsonObject &info);
|
||||
|
||||
private:
|
||||
void reportResult(Status status);
|
||||
|
||||
QStringList _errors;
|
||||
AccountPtr _account;
|
||||
bool _clearCookies = false;
|
||||
|
||||
Utility::ChronoElapsedTimer _duration;
|
||||
bool _finished = false;
|
||||
|
||||
ConnectionValidator::ValidationMode _mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@kde.org>
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "creds/httpcredentialsgui.h"
|
||||
#include "account.h"
|
||||
#include "accountmodalwidget.h"
|
||||
#include "application.h"
|
||||
#include "common/asserts.h"
|
||||
#include "creds/qmlcredentials.h"
|
||||
#include "gui/accountsettings.h"
|
||||
#include "networkjobs.h"
|
||||
#include "settingsdialog.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QDesktopServices>
|
||||
#include <QTimer>
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcHttpCredentialsGui, "sync.credentials.http.gui", QtInfoMsg)
|
||||
|
||||
HttpCredentialsGui::HttpCredentialsGui(const QString &accessToken, const QString &refreshToken)
|
||||
: HttpCredentials(accessToken)
|
||||
{
|
||||
_refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
void HttpCredentialsGui::restartOauth()
|
||||
{
|
||||
qCDebug(lcHttpCredentialsGui) << u"showing modal dialog asking user to log in again via OAuth2";
|
||||
if (_asyncAuth) {
|
||||
return;
|
||||
}
|
||||
if (!OC_ENSURE_NOT(_modalWidget)) {
|
||||
_modalWidget->deleteLater();
|
||||
}
|
||||
_asyncAuth.reset(new AccountBasedOAuth(_account->sharedFromThis(), this));
|
||||
connect(_asyncAuth.data(), &OAuth::result, this, &HttpCredentialsGui::asyncAuthResult);
|
||||
|
||||
auto *oauthCredentials = new QmlOAuthCredentials(_asyncAuth.data(), _account->url(), _account->davDisplayName());
|
||||
_modalWidget = new AccountModalWidget(tr("Login required"), QUrl(QStringLiteral("qrc:/qt/qml/eu/QSfera/gui/qml/credentials/OAuthCredentials.qml")),
|
||||
oauthCredentials, ocApp()->settingsDialog());
|
||||
|
||||
connect(oauthCredentials, &QmlOAuthCredentials::logOutRequested, _modalWidget, [this] {
|
||||
_modalWidget->reject();
|
||||
_modalWidget.clear();
|
||||
_asyncAuth.reset();
|
||||
requestLogout();
|
||||
});
|
||||
connect(oauthCredentials, &QmlOAuthCredentials::requestRestart, this, [this] {
|
||||
_modalWidget->reject();
|
||||
_modalWidget.clear();
|
||||
_asyncAuth.reset();
|
||||
restartOauth();
|
||||
});
|
||||
connect(this, &HttpCredentialsGui::oAuthLoginAccepted, _modalWidget, &AccountModalWidget::accept);
|
||||
connect(this, &HttpCredentialsGui::oAuthErrorOccurred, oauthCredentials, [this]() {
|
||||
Q_ASSERT(!ready());
|
||||
ocApp()->showSettings();
|
||||
});
|
||||
|
||||
ocApp()->settingsDialog()->accountSettings(_account)->addModalWidget(_modalWidget);
|
||||
_asyncAuth->startAuthentication();
|
||||
}
|
||||
|
||||
void HttpCredentialsGui::asyncAuthResult(OAuth::Result r, const QString &token, const QString &refreshToken)
|
||||
{
|
||||
_asyncAuth.reset();
|
||||
switch (r) {
|
||||
case OAuth::ErrorInsecureUrl:
|
||||
// should not happen after the initial setup
|
||||
Q_ASSERT(false);
|
||||
[[fallthrough]];
|
||||
case OAuth::Error:
|
||||
Q_EMIT oAuthErrorOccurred();
|
||||
return;
|
||||
case OAuth::LoggedIn:
|
||||
Q_EMIT oAuthLoginAccepted();
|
||||
break;
|
||||
}
|
||||
|
||||
_accessToken = token;
|
||||
_refreshToken = refreshToken;
|
||||
_ready = true;
|
||||
persist();
|
||||
Q_EMIT fetched();
|
||||
}
|
||||
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@kde.org>
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "creds/httpcredentials.h"
|
||||
#include "creds/oauth.h"
|
||||
|
||||
#include <QPointer>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class AccountModalWidget;
|
||||
|
||||
/**
|
||||
* @brief The HttpCredentialsGui class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class HttpCredentialsGui : public HttpCredentials
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
HttpCredentialsGui() = default;
|
||||
|
||||
HttpCredentialsGui(const QString &accessToken, const QString &refreshToken);
|
||||
|
||||
void restartOauth() override;
|
||||
|
||||
|
||||
private Q_SLOTS:
|
||||
void asyncAuthResult(OAuth::Result, const QString &accessToken, const QString &refreshToken);
|
||||
|
||||
Q_SIGNALS:
|
||||
void oAuthLoginAccepted();
|
||||
void oAuthErrorOccurred();
|
||||
|
||||
private:
|
||||
QScopedPointer<AccountBasedOAuth, QScopedPointerObjectDeleteLater<AccountBasedOAuth>> _asyncAuth;
|
||||
QPointer<AccountModalWidget> _modalWidget;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "qmlcredentials.h"
|
||||
|
||||
#include "libsync/theme.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
|
||||
QmlCredentials::QmlCredentials(const QUrl &host, const QString &displayName, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _host(host)
|
||||
, _displayName(displayName)
|
||||
{
|
||||
}
|
||||
|
||||
bool QmlCredentials::isRefresh() const
|
||||
{
|
||||
return _isRefresh;
|
||||
}
|
||||
|
||||
void QmlCredentials::setIsRefresh(bool newIsRefresh)
|
||||
{
|
||||
if (_isRefresh != newIsRefresh) {
|
||||
_isRefresh = newIsRefresh;
|
||||
Q_EMIT isRefreshChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QmlOAuthCredentials::QmlOAuthCredentials(OAuth *oauth, const QUrl &host, const QString &displayName, QObject *parent)
|
||||
: QmlCredentials(host, displayName, parent)
|
||||
, _oauth(oauth)
|
||||
{
|
||||
connect(_oauth, &OAuth::authorisationLinkChanged, this, [this] {
|
||||
_ready = true;
|
||||
Q_EMIT readyChanged();
|
||||
});
|
||||
connect(_oauth, &QObject::destroyed, this, &QmlOAuthCredentials::readyChanged);
|
||||
}
|
||||
|
||||
void QmlOAuthCredentials::copyAuthenticationUrlToClipboard()
|
||||
{
|
||||
qApp->clipboard()->setText(_oauth->authorisationLink().toString(QUrl::FullyEncoded));
|
||||
}
|
||||
|
||||
void QmlOAuthCredentials::openAuthenticationUrlInBrowser()
|
||||
{
|
||||
_oauth->openBrowser();
|
||||
}
|
||||
|
||||
bool QmlOAuthCredentials::isReady() const
|
||||
{
|
||||
return isValid() && _ready;
|
||||
}
|
||||
|
||||
bool QmlOAuthCredentials::isValid() const
|
||||
{
|
||||
return _oauth;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/creds/oauth.h"
|
||||
|
||||
#include <QtQmlIntegration/QtQmlIntegration>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class QmlCredentials : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QUrl host MEMBER _host CONSTANT FINAL)
|
||||
Q_PROPERTY(QString displayName MEMBER _displayName CONSTANT FINAL)
|
||||
Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged FINAL)
|
||||
Q_PROPERTY(bool isRefresh READ isRefresh WRITE setIsRefresh NOTIFY isRefreshChanged FINAL)
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Abstract class")
|
||||
|
||||
public:
|
||||
QmlCredentials(const QUrl &host, const QString &displayName, QObject *parent = nullptr);
|
||||
|
||||
virtual bool isReady() const = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Whether we refresh the credentials or are in the wizard
|
||||
* @default true
|
||||
*/
|
||||
bool isRefresh() const;
|
||||
void setIsRefresh(bool newIsRefresh);
|
||||
|
||||
Q_SIGNALS:
|
||||
void readyChanged() const;
|
||||
void logOutRequested() const;
|
||||
|
||||
void isRefreshChanged();
|
||||
|
||||
private:
|
||||
QUrl _host;
|
||||
QString _displayName;
|
||||
bool _isRefresh = true;
|
||||
};
|
||||
|
||||
|
||||
class QmlOAuthCredentials : public QmlCredentials
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool isValid READ isValid NOTIFY readyChanged FINAL)
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("C++ only")
|
||||
|
||||
public:
|
||||
QmlOAuthCredentials(OAuth *oauth, const QUrl &host, const QString &displayName, QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE void copyAuthenticationUrlToClipboard();
|
||||
Q_INVOKABLE void openAuthenticationUrlInBrowser();
|
||||
|
||||
bool isReady() const override;
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void requestRestart();
|
||||
|
||||
|
||||
private:
|
||||
QPointer<OAuth> _oauth = nullptr;
|
||||
bool _ready = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) by Christian Kamm <mail@ckamm.de>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "elidedlabel.h"
|
||||
|
||||
#include <QResizeEvent>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
ElidedLabel::ElidedLabel(const QString &text, QWidget *parent)
|
||||
: QLabel(text, parent)
|
||||
, _text(text)
|
||||
, _elideMode(Qt::ElideNone)
|
||||
{
|
||||
}
|
||||
|
||||
void ElidedLabel::setText(const QString &text)
|
||||
{
|
||||
_text = text;
|
||||
QLabel::setText(text);
|
||||
update();
|
||||
}
|
||||
|
||||
void ElidedLabel::setElideMode(Qt::TextElideMode elideMode)
|
||||
{
|
||||
_elideMode = elideMode;
|
||||
update();
|
||||
}
|
||||
|
||||
void ElidedLabel::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QLabel::resizeEvent(event);
|
||||
|
||||
QFontMetrics fm = fontMetrics();
|
||||
QString elided = fm.elidedText(_text, _elideMode, event->size().width());
|
||||
QLabel::setText(elided);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) by Christian Kamm <mail@ckamm.de>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef ELIDEDLABEL_H
|
||||
#define ELIDEDLABEL_H
|
||||
|
||||
#include <QLabel>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/// Label that can elide its text
|
||||
class ElidedLabel : public QLabel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ElidedLabel(const QString &text, QWidget *parent = nullptr);
|
||||
|
||||
void setText(const QString &text);
|
||||
const QString &text() const { return _text; }
|
||||
|
||||
void setElideMode(Qt::TextElideMode elideMode);
|
||||
Qt::TextElideMode elideMode() const { return _elideMode; }
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private:
|
||||
QString _text;
|
||||
Qt::TextElideMode _elideMode;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "fetchserversettings.h"
|
||||
|
||||
#include "gui/accountstate.h"
|
||||
#include "gui/connectionvalidator.h"
|
||||
#include "gui/networkinformation.h"
|
||||
|
||||
#include "libsync/account.h"
|
||||
#include "libsync/networkjobs/jsonjob.h"
|
||||
|
||||
#include <OAIUser.h>
|
||||
|
||||
#include <QImageReader>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
|
||||
Q_LOGGING_CATEGORY(lcfetchserversettings, "sync.fetchserversettings", QtInfoMsg)
|
||||
|
||||
namespace {
|
||||
auto fetchSettingsTimeout()
|
||||
{
|
||||
return std::min<std::chrono::milliseconds>(20s, AbstractNetworkJob::httpTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move to libsync?
|
||||
FetchServerSettingsJob::FetchServerSettingsJob(const OCC::AccountPtr &account, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _account(account)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
void FetchServerSettingsJob::start()
|
||||
{
|
||||
// The main flow now needs the capabilities
|
||||
auto *job = new JsonApiJob(_account, QStringLiteral("ocs/v2.php/cloud/capabilities"), {}, {}, this);
|
||||
job->setTimeout(fetchSettingsTimeout());
|
||||
|
||||
connect(job, &JsonApiJob::finishedSignal, this, [job, this] {
|
||||
auto caps =
|
||||
job->data().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toObject().value(QStringLiteral("capabilities")).toObject();
|
||||
qCInfo(lcfetchserversettings) << u"Server capabilities" << caps;
|
||||
if (job->ocsSuccess()) {
|
||||
// Record that the server supports HTTP/2
|
||||
// Actual decision if we should use HTTP/2 is done in AccessManager::createRequest
|
||||
if (auto reply = job->reply()) {
|
||||
_account->setHttp2Supported(reply->attribute(QNetworkRequest::Http2WasUsedAttribute).toBool());
|
||||
}
|
||||
_account->setCapabilities({_account->url(), caps.toVariantMap()});
|
||||
runAsyncUpdates();
|
||||
}
|
||||
Q_EMIT finishedSignal();
|
||||
});
|
||||
job->start();
|
||||
}
|
||||
|
||||
void FetchServerSettingsJob::runAsyncUpdates()
|
||||
{
|
||||
// those jobs are:
|
||||
// - never auth jobs
|
||||
// - might get queued
|
||||
// - have the default timeout
|
||||
// - must not be parented by this object
|
||||
|
||||
// ideally we would parent them to the account, but as things are messed up by the shared pointer stuff we can't at the moment
|
||||
// so we just set them free
|
||||
|
||||
// this must not be passed to the lambda
|
||||
[account = _account] {
|
||||
auto *userJob = new JsonJob(account, account->url(), u"graph/v1.0/me"_s, "GET", nullptr);
|
||||
userJob->setTimeout(fetchSettingsTimeout());
|
||||
connect(userJob, &JsonApiJob::finishedSignal, account.data(), [userJob, account] {
|
||||
if (userJob->httpStatusCode() == 200) {
|
||||
OpenAPI::OAIUser me;
|
||||
me.fromJsonObject(userJob->data());
|
||||
account->setDavDisplayName(me.getDisplayName());
|
||||
}
|
||||
});
|
||||
userJob->start();
|
||||
|
||||
if (account->capabilities().appProviders().enabled) {
|
||||
auto *jsonJob = new JsonJob(account, account->capabilities().appProviders().appsUrl, {}, "GET", nullptr);
|
||||
connect(jsonJob, &JsonJob::finishedSignal, account.data(), [jsonJob, account] { account->setAppProvider(AppProvider{jsonJob->data()}); });
|
||||
jsonJob->start();
|
||||
}
|
||||
|
||||
auto *avatarJob = new SimpleNetworkJob(account, account->url(), u"graph/v1.0/me/photo/$value"_s, "GET", nullptr);
|
||||
connect(avatarJob, &SimpleNetworkJob::finishedSignal, account.data(), [avatarJob, account] {
|
||||
if (avatarJob->httpStatusCode() == 200) {
|
||||
QImageReader reader(avatarJob->reply());
|
||||
const auto image = reader.read();
|
||||
if (!image.isNull()) {
|
||||
account->setAvatar(QPixmap::fromImage(image));
|
||||
} else {
|
||||
qCWarning(lcfetchserversettings) << u"Failed to read avatar image:" << reader.errorString();
|
||||
}
|
||||
}
|
||||
});
|
||||
avatarJob->start();
|
||||
}();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/connectionvalidator.h"
|
||||
#include "libsync/accountfwd.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace OCC {
|
||||
class Capabilities;
|
||||
|
||||
class FetchServerSettingsJob : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FetchServerSettingsJob(const AccountPtr &account, QObject *parent);
|
||||
|
||||
void start();
|
||||
|
||||
Q_SIGNALS:
|
||||
void finishedSignal();
|
||||
|
||||
private:
|
||||
void runAsyncUpdates();
|
||||
|
||||
const AccountPtr _account;
|
||||
};
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,378 @@
|
||||
/*
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include "accountstate.h"
|
||||
#include "common/syncjournaldb.h"
|
||||
#include "gui/folderdefinition.h"
|
||||
#include "libsync/graphapi/space.h"
|
||||
#include "networkjobs.h"
|
||||
#include "progressdispatcher.h"
|
||||
#include "syncoptions.h"
|
||||
#include "syncresult.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QtQml/QQmlEngine>
|
||||
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
|
||||
class QThread;
|
||||
class QSettings;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class Vfs;
|
||||
class SyncEngine;
|
||||
class SyncRunFileLog;
|
||||
class FolderWatcher;
|
||||
class LocalDiscoveryTracker;
|
||||
|
||||
/**
|
||||
* @brief The Folder class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class QSFERA_GUI_EXPORT Folder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(GraphApi::Space *space READ space NOTIFY spaceChanged)
|
||||
Q_PROPERTY(QString path READ path CONSTANT)
|
||||
Q_PROPERTY(QUrl webDavUrl READ webDavUrl CONSTANT)
|
||||
Q_PROPERTY(bool isReady READ isReady NOTIFY isReadyChanged)
|
||||
Q_PROPERTY(bool isSyncPaused READ isSyncPaused NOTIFY syncPausedChanged)
|
||||
Q_PROPERTY(bool isSyncRunning READ isSyncRunning NOTIFY isSyncRunningChanged)
|
||||
Q_PROPERTY(bool isDeployed READ isDeployed CONSTANT)
|
||||
Q_PROPERTY(Vfs::Mode vfsMode READ vfsMode CONSTANT)
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Folders can only be created by the FolderManager")
|
||||
|
||||
public:
|
||||
enum class ChangeReason {
|
||||
Other,
|
||||
UnLock
|
||||
};
|
||||
Q_ENUM(ChangeReason)
|
||||
|
||||
static void prepareFolder(const QString &path, const QString &displayName, const QString &description, bool override);
|
||||
|
||||
~Folder() override;
|
||||
/**
|
||||
* The account the folder is configured on.
|
||||
*/
|
||||
AccountStatePtr accountState() const { return _accountState; }
|
||||
|
||||
QString displayName() const;
|
||||
|
||||
/**
|
||||
* short local path to display on the GUI (native separators)
|
||||
*/
|
||||
QString shortGuiLocalPath() const;
|
||||
|
||||
/**
|
||||
* canonical local folder path, always ends with /
|
||||
*/
|
||||
QString path() const;
|
||||
|
||||
/**
|
||||
* cleaned canonical folder path, like path() but never ends with a /
|
||||
*
|
||||
* Wrapper for QDir::cleanPath(path()) except for "Z:/",
|
||||
* where it returns "Z:" instead of "Z:/".
|
||||
*/
|
||||
QString cleanPath() const;
|
||||
|
||||
/**
|
||||
* The full remote WebDAV URL
|
||||
*/
|
||||
QUrl webDavUrl() const;
|
||||
|
||||
/**
|
||||
* switch sync on or off
|
||||
*/
|
||||
void setSyncPaused(bool);
|
||||
|
||||
bool isSyncPaused() const;
|
||||
|
||||
/**
|
||||
* Returns true when the folder may sync.
|
||||
*/
|
||||
bool canSync() const;
|
||||
|
||||
/**
|
||||
* Whether the folder is ready
|
||||
*/
|
||||
bool isReady() const;
|
||||
|
||||
bool hasSetupError() const
|
||||
{
|
||||
return _syncResult.status() == SyncResult::SetupError;
|
||||
}
|
||||
|
||||
/** True if the folder is currently synchronizing */
|
||||
bool isSyncRunning() const;
|
||||
|
||||
/**
|
||||
* return the last sync result with error message and status
|
||||
*/
|
||||
SyncResult syncResult() const;
|
||||
|
||||
/**
|
||||
* This is called when the sync folder definition is removed. Do cleanups here.
|
||||
*
|
||||
* It removes the database, among other things.
|
||||
*
|
||||
* The folder is not in a valid state afterwards!
|
||||
*/
|
||||
virtual void wipeForRemoval();
|
||||
|
||||
void setSyncState(SyncResult::Status state);
|
||||
|
||||
SyncResult::Status syncState() const;
|
||||
|
||||
void setDirtyNetworkLimits();
|
||||
|
||||
void reloadSyncOptions();
|
||||
|
||||
/**
|
||||
* Ignore syncing of hidden files or not. This is defined in the
|
||||
* folder definition
|
||||
*/
|
||||
bool ignoreHiddenFiles();
|
||||
void setIgnoreHiddenFiles(bool ignore);
|
||||
|
||||
// TODO: don't expose
|
||||
SyncJournalDb *journalDb()
|
||||
{
|
||||
return &_journal;
|
||||
}
|
||||
// TODO: don't expose
|
||||
SyncEngine &syncEngine()
|
||||
{
|
||||
return *_engine;
|
||||
}
|
||||
|
||||
Vfs &vfs()
|
||||
{
|
||||
OC_ENFORCE(_vfs);
|
||||
return *_vfs;
|
||||
}
|
||||
|
||||
auto lastSyncTime() const { return QDateTime::currentDateTime().addMSecs(-msecSinceLastSync().count()); }
|
||||
std::chrono::milliseconds msecSinceLastSync() const { return std::chrono::milliseconds(_timeSinceLastSyncDone.elapsed()); }
|
||||
std::chrono::milliseconds msecLastSyncDuration() const { return _lastSyncDuration; }
|
||||
|
||||
/**
|
||||
* Returns whether a file inside this folder should be excluded.
|
||||
*/
|
||||
bool isFileExcludedAbsolute(const QString &fullPath) const;
|
||||
|
||||
/**
|
||||
* Returns whether a file inside this folder should be excluded.
|
||||
*/
|
||||
bool isFileExcludedRelative(const QString &relativePath) const;
|
||||
|
||||
/** virtual files of some kind are enabled
|
||||
*
|
||||
* This is independent of whether new files will be virtual. It's possible to have this enabled
|
||||
* and never have an automatic virtual file. But when it's on, the shell context menu will allow
|
||||
* users to make existing files virtual.
|
||||
*/
|
||||
bool virtualFilesEnabled() const;
|
||||
void setVirtualFilesEnabled(bool enabled);
|
||||
|
||||
/** Whether this folder should show selective sync ui */
|
||||
bool supportsSelectiveSync() const;
|
||||
|
||||
/**
|
||||
* The folder is deployed by an admin
|
||||
* We will hide the remove option and the disable/enable vfs option.
|
||||
*/
|
||||
bool isDeployed() const;
|
||||
|
||||
Vfs::Mode vfsMode() const;
|
||||
|
||||
uint32_t priority();
|
||||
|
||||
void setPriority(uint32_t p);
|
||||
|
||||
static Result<void, QString> checkPathLength(const QString &path);
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The corresponding space object or null
|
||||
*/
|
||||
GraphApi::Space *space() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void syncStateChange();
|
||||
void syncFinished(const SyncResult &result);
|
||||
void syncPausedChanged(Folder *, bool paused);
|
||||
void canSyncChanged();
|
||||
void spaceChanged();
|
||||
void isReadyChanged();
|
||||
void isSyncRunningChanged();
|
||||
|
||||
|
||||
/**
|
||||
* Fires for each change inside this folder that wasn't caused
|
||||
* by sync activity.
|
||||
*/
|
||||
void watchedFileChangedExternally(const QString &path);
|
||||
|
||||
public Q_SLOTS:
|
||||
void openInWebBrowser();
|
||||
|
||||
/**
|
||||
* Starts a sync operation
|
||||
*
|
||||
* If the list of changed files is known, it is passed.
|
||||
*/
|
||||
void startSync();
|
||||
|
||||
void slotDiscardDownloadProgress();
|
||||
int slotWipeErrorBlacklist();
|
||||
|
||||
/**
|
||||
* Triggered by the folder watcher when a file/dir in this folder
|
||||
* changes. Needs to check whether this change should trigger a new
|
||||
* sync run to be scheduled.
|
||||
*/
|
||||
void slotWatchedPathsChanged(const QSet<QString> &paths, ChangeReason reason);
|
||||
|
||||
/** Ensures that the next sync performs a full local discovery. */
|
||||
void slotNextSyncFullLocalDiscovery();
|
||||
|
||||
/** Adds the path to the local discovery list
|
||||
*
|
||||
* A weaker version of slotNextSyncFullLocalDiscovery() that just
|
||||
* schedules all parent and child items of the path for local
|
||||
* discovery.
|
||||
*/
|
||||
void schedulePathForLocalDiscovery(const QString &relativePath);
|
||||
|
||||
/// Reloads the excludes, used when changing the user-defined excludes after saving them to disk.
|
||||
bool reloadExcludes();
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotSyncFinished(bool);
|
||||
|
||||
/** Adds a error message that's not tied to a specific item.
|
||||
*/
|
||||
void slotSyncError(const QString &message, ErrorCategory category = ErrorCategory::Normal);
|
||||
|
||||
void slotItemCompleted(const SyncFileItemPtr &);
|
||||
|
||||
/** Adjust sync result based on conflict data from IssuesWidget.
|
||||
*
|
||||
* This is pretty awkward, but IssuesWidget just keeps better track
|
||||
* of conflicts across partial local discovery.
|
||||
*/
|
||||
void slotFolderConflicts(Folder *folder, const QStringList &conflictPaths);
|
||||
|
||||
/** Warn users if they create a file or folder that is selective-sync excluded */
|
||||
void warnOnNewExcludedItem(const SyncJournalFileRecord &record, QStringView path);
|
||||
|
||||
/** Warn users about an unreliable folder watcher */
|
||||
void slotWatcherUnreliable(const QString &message);
|
||||
|
||||
private:
|
||||
/** Create a new Folder
|
||||
*/
|
||||
Folder(const FolderDefinition &definition, const AccountStatePtr &accountState, std::unique_ptr<Vfs> &&vfs, QObject *parent = nullptr);
|
||||
|
||||
|
||||
void showSyncResultPopup();
|
||||
|
||||
bool checkLocalPath();
|
||||
|
||||
SyncOptions loadSyncOptions();
|
||||
|
||||
void setIsReady(bool b);
|
||||
|
||||
/**
|
||||
* Sets up this folder's folderWatcher if possible.
|
||||
*
|
||||
* May be called several times.
|
||||
*/
|
||||
void registerFolderWatcher();
|
||||
|
||||
enum LogStatus {
|
||||
LogStatusRemove,
|
||||
LogStatusRename,
|
||||
LogStatusMove,
|
||||
LogStatusNew,
|
||||
LogStatusError,
|
||||
LogStatusConflict,
|
||||
LogStatusUpdated
|
||||
};
|
||||
|
||||
void createGuiLog(const QString &filename, LogStatus status, int count,
|
||||
const QString &renameTarget = QString());
|
||||
|
||||
void startVfs();
|
||||
|
||||
AccountStatePtr _accountState;
|
||||
FolderDefinition _definition;
|
||||
QString _canonicalLocalPath; // As returned with QFileInfo:canonicalFilePath. Always ends with "/"
|
||||
|
||||
SyncResult _syncResult;
|
||||
QScopedPointer<SyncEngine> _engine;
|
||||
QElapsedTimer _timeSinceLastSyncDone;
|
||||
QElapsedTimer _timeSinceLastSyncStart;
|
||||
QElapsedTimer _timeSinceLastFullLocalDiscovery;
|
||||
std::chrono::milliseconds _lastSyncDuration = {};
|
||||
|
||||
/// The number of syncs that failed in a row.
|
||||
/// Reset when a sync is successful.
|
||||
int _consecutiveFailingSyncs = 0;
|
||||
|
||||
/// The number of requested follow-up syncs.
|
||||
/// Reset when no follow-up is requested.
|
||||
int _consecutiveFollowUpSyncs = 0;
|
||||
|
||||
mutable SyncJournalDb _journal;
|
||||
|
||||
QScopedPointer<SyncRunFileLog> _fileLog;
|
||||
|
||||
/**
|
||||
* Setting up vfs is a async operation
|
||||
*/
|
||||
bool _vfsIsReady = false;
|
||||
|
||||
/**
|
||||
* Watches this folder's local directory for changes.
|
||||
*
|
||||
* Created by registerFolderWatcher(), triggers slotWatchedPathsChanged()
|
||||
*/
|
||||
QScopedPointer<FolderWatcher> _folderWatcher;
|
||||
|
||||
/**
|
||||
* Keeps track of locally dirty files so we can skip local discovery sometimes.
|
||||
*/
|
||||
QScopedPointer<LocalDiscoveryTracker> _localDiscoveryTracker;
|
||||
|
||||
/**
|
||||
* The vfs mode instance (created by plugin) to use. Never null.
|
||||
*/
|
||||
QSharedPointer<Vfs> _vfs;
|
||||
|
||||
friend class FolderMan;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "folderdefinition.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcFolder, "gui.folder.definition", QtInfoMsg)
|
||||
|
||||
namespace {
|
||||
|
||||
auto davUrlC()
|
||||
{
|
||||
return "davUrl";
|
||||
}
|
||||
|
||||
auto spaceIdC()
|
||||
{
|
||||
return "spaceId";
|
||||
}
|
||||
|
||||
auto displayNameC()
|
||||
{
|
||||
return "displayString";
|
||||
}
|
||||
|
||||
auto deployedC()
|
||||
{
|
||||
return "deployed";
|
||||
}
|
||||
|
||||
auto priorityC()
|
||||
{
|
||||
return "priority";
|
||||
}
|
||||
}
|
||||
|
||||
FolderDefinition::FolderDefinition(const QUuid &accountUuid, const QUrl &davUrl, const QString &spaceId, const QString &displayName)
|
||||
: _webDavUrl(davUrl)
|
||||
, _spaceId(spaceId)
|
||||
, _displayName(displayName)
|
||||
, _accountUUID(accountUuid)
|
||||
{
|
||||
Q_ASSERT(!_accountUUID.isNull());
|
||||
}
|
||||
|
||||
void FolderDefinition::setPriority(uint32_t newPriority)
|
||||
{
|
||||
_priority = newPriority;
|
||||
}
|
||||
|
||||
QUuid FolderDefinition::accountUUID() const
|
||||
{
|
||||
return _accountUUID;
|
||||
}
|
||||
|
||||
uint32_t FolderDefinition::priority() const
|
||||
{
|
||||
return _priority;
|
||||
}
|
||||
|
||||
void FolderDefinition::save(QSettings &settings, const FolderDefinition &folder)
|
||||
{
|
||||
settings.setValue("accountUUID", folder.accountUUID());
|
||||
settings.setValue("localPath", folder.localPath());
|
||||
settings.setValue("journalPath", folder.journalPath);
|
||||
settings.setValue(spaceIdC(), folder.spaceId());
|
||||
settings.setValue(davUrlC(), folder.webDavUrl());
|
||||
settings.setValue(displayNameC(), folder.displayName());
|
||||
settings.setValue("paused", folder.paused);
|
||||
settings.setValue("ignoreHiddenFiles", folder.ignoreHiddenFiles);
|
||||
settings.setValue(deployedC(), folder.isDeployed());
|
||||
settings.setValue(priorityC(), folder.priority());
|
||||
|
||||
settings.setValue("virtualFilesMode", Utility::enumToString(folder.virtualFilesMode));
|
||||
}
|
||||
|
||||
FolderDefinition FolderDefinition::load(QSettings &settings)
|
||||
{
|
||||
FolderDefinition folder{settings.value("accountUUID").toUuid(), settings.value(davUrlC()).toUrl(), settings.value(spaceIdC()).toString(),
|
||||
settings.value(displayNameC()).toString()};
|
||||
|
||||
folder.setLocalPath(settings.value("localPath").toString());
|
||||
folder.journalPath = settings.value("journalPath").toString();
|
||||
folder.paused = settings.value("paused").toBool();
|
||||
folder.ignoreHiddenFiles = settings.value("ignoreHiddenFiles", QVariant(true)).toBool();
|
||||
folder._deployed = settings.value(deployedC(), false).toBool();
|
||||
folder._priority = settings.value(priorityC(), 0).toUInt();
|
||||
|
||||
folder.virtualFilesMode = Vfs::Mode::Off;
|
||||
|
||||
QString vfsModeString = settings.value("virtualFilesMode").toString();
|
||||
|
||||
const auto vfs = Utility::isWindows() ? Vfs::Mode::WindowsCfApi : Vfs::Mode::OpenVFS;
|
||||
if (auto result = VfsPluginManager::instance().prepare(folder.localPath(), folder.accountUUID(), vfs); result) {
|
||||
vfsModeString = Utility::enumToString(vfs);
|
||||
} else {
|
||||
qCWarning(lcFolder) << u"Failed to upgrade" << folder.localPath() << u"to" << vfs << result.error();
|
||||
}
|
||||
if (!vfsModeString.isEmpty()) {
|
||||
if (auto mode = Vfs::modeFromString(vfsModeString)) {
|
||||
folder.virtualFilesMode = *mode;
|
||||
} else {
|
||||
qCWarning(lcFolder) << u"Unknown virtualFilesMode:" << vfsModeString << u"assuming 'off'";
|
||||
}
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
void FolderDefinition::setLocalPath(const QString &path)
|
||||
{
|
||||
_localPath = QDir::fromNativeSeparators(path);
|
||||
if (!_localPath.endsWith(QLatin1Char('/'))) {
|
||||
_localPath.append(QLatin1Char('/'));
|
||||
}
|
||||
}
|
||||
|
||||
QString FolderDefinition::absoluteJournalPath() const
|
||||
{
|
||||
return QDir(localPath()).filePath(journalPath);
|
||||
}
|
||||
|
||||
QString FolderDefinition::displayName() const
|
||||
{
|
||||
return _displayName;
|
||||
}
|
||||
|
||||
void FolderDefinition::setDisplayName(const QString &s)
|
||||
{
|
||||
_displayName = s;
|
||||
}
|
||||
|
||||
bool FolderDefinition::isDeployed() const
|
||||
{
|
||||
return _deployed;
|
||||
}
|
||||
|
||||
QUrl FolderDefinition::webDavUrl() const
|
||||
{
|
||||
Q_ASSERT(_webDavUrl.isValid());
|
||||
return _webDavUrl;
|
||||
}
|
||||
|
||||
QString FolderDefinition::localPath() const
|
||||
{
|
||||
return _localPath;
|
||||
}
|
||||
|
||||
QString FolderDefinition::spaceId() const
|
||||
{
|
||||
// we might call the function to check for the id
|
||||
// anyhow one of the conditions needs to be true
|
||||
Q_ASSERT(_webDavUrl.isValid() || !_spaceId.isEmpty());
|
||||
return _spaceId;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
#include "libsync/vfs/vfs.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUuid>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
|
||||
/**
|
||||
* @brief The FolderDefinition class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class QSFERA_GUI_EXPORT FolderDefinition
|
||||
{
|
||||
public:
|
||||
FolderDefinition(const QUuid &accountUuid, const QUrl &davUrl, const QString &spaceId, const QString &displayName);
|
||||
/// path to the journal, usually relative to localPath
|
||||
QString journalPath;
|
||||
|
||||
/// whether the folder is paused
|
||||
bool paused = false;
|
||||
/// whether the folder syncs hidden files
|
||||
bool ignoreHiddenFiles = true;
|
||||
/// Which virtual files setting the folder uses
|
||||
Vfs::Mode virtualFilesMode = Vfs::Mode::Off;
|
||||
|
||||
/// Saves the folder definition into the current settings.
|
||||
static void save(QSettings &settings, const FolderDefinition &folder);
|
||||
|
||||
/// Reads a folder definition from the current settings.
|
||||
static FolderDefinition load(QSettings &settings);
|
||||
|
||||
/// Ensure / as separator and trailing /.
|
||||
void setLocalPath(const QString &path);
|
||||
|
||||
/// journalPath relative to localPath.
|
||||
QString absoluteJournalPath() const;
|
||||
|
||||
QString localPath() const;
|
||||
|
||||
QUrl webDavUrl() const;
|
||||
|
||||
// could change in the case of spaces
|
||||
void setWebDavUrl(const QUrl &url) { _webDavUrl = url; }
|
||||
|
||||
// when using spaces we don't store the dav URL but the space id
|
||||
// this id is then used to look up the dav URL
|
||||
QString spaceId() const;
|
||||
|
||||
void setSpaceId(const QString &spaceId) { _spaceId = spaceId; }
|
||||
|
||||
QString displayName() const;
|
||||
void setDisplayName(const QString &s);
|
||||
|
||||
/**
|
||||
* The folder is deployed by an admin
|
||||
* We will hide the remove option and the disable/enable vfs option.
|
||||
*/
|
||||
bool isDeployed() const;
|
||||
|
||||
/**
|
||||
* Higher values mean more imortant
|
||||
* Used for sorting
|
||||
*/
|
||||
uint32_t priority() const;
|
||||
|
||||
void setPriority(uint32_t newPriority);
|
||||
|
||||
QUuid accountUUID() const;
|
||||
|
||||
private:
|
||||
QUrl _webDavUrl;
|
||||
|
||||
QString _spaceId;
|
||||
QString _displayName;
|
||||
/// path on local machine (always trailing /)
|
||||
QString _localPath;
|
||||
bool _deployed = false;
|
||||
|
||||
uint32_t _priority = 0;
|
||||
|
||||
QUuid _accountUUID;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "folderman.h"
|
||||
|
||||
#include "account.h"
|
||||
#include "accountmanager.h"
|
||||
#include "accountstate.h"
|
||||
#include "common/asserts.h"
|
||||
#include "configfile.h"
|
||||
#include "gui/folder.h"
|
||||
#include "gui/folderdefinition.h"
|
||||
#include "gui/networkinformation.h"
|
||||
#include "guiutility.h"
|
||||
#include "libsync/syncengine.h"
|
||||
#include "lockwatcher.h"
|
||||
#include "scheduling/syncscheduler.h"
|
||||
#include "socketapi/socketapi.h"
|
||||
#include "syncresult.h"
|
||||
#include "theme.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "common/utility_win.h"
|
||||
#endif
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QtCore>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
using namespace std::chrono;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
|
||||
QAnyStringView foldersC()
|
||||
{
|
||||
return QAnyStringView("Folders");
|
||||
}
|
||||
|
||||
qsizetype numberOfSyncJournals(const QString &path)
|
||||
{
|
||||
return QDir(path).entryList({ QStringLiteral(".sync_*.db"), QStringLiteral("._sync_*.db") }, QDir::Hidden | QDir::Files).size();
|
||||
}
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
Q_LOGGING_CATEGORY(lcFolderMan, "gui.folder.manager", QtInfoMsg)
|
||||
|
||||
void TrayOverallStatusResult::addResult(Folder *f)
|
||||
{
|
||||
_overallStatus._numNewConflictItems += f->syncResult()._numNewConflictItems;
|
||||
_overallStatus._numErrorItems += f->syncResult()._numErrorItems;
|
||||
_overallStatus._numBlacklistErrors += f->syncResult()._numBlacklistErrors;
|
||||
|
||||
auto time = f->lastSyncTime();
|
||||
if (time > lastSyncDone) {
|
||||
lastSyncDone = time;
|
||||
}
|
||||
|
||||
auto status = f->isSyncPaused() || NetworkInformation::instance()->isBehindCaptivePortal() || NetworkInformation::instance()->isMetered()
|
||||
? SyncResult::Paused
|
||||
: f->syncResult().status();
|
||||
if (status == SyncResult::Undefined) {
|
||||
status = SyncResult::Problem;
|
||||
}
|
||||
if (status > _overallStatus.status()) {
|
||||
_overallStatus.setStatus(status);
|
||||
}
|
||||
}
|
||||
|
||||
const SyncResult &TrayOverallStatusResult::overallStatus() const
|
||||
{
|
||||
return _overallStatus;
|
||||
}
|
||||
|
||||
FolderMan *FolderMan::_instance = nullptr;
|
||||
|
||||
FolderMan::FolderMan()
|
||||
: _lockWatcher(new LockWatcher)
|
||||
, _scheduler(new SyncScheduler(this))
|
||||
, _socketApi(new SocketApi)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
auto updateSyncRoots = [] {
|
||||
// set the metadata for a sync root, depending on the number of accounts
|
||||
for (const auto &accountStatePtr : AccountManager::instance()->accounts()) {
|
||||
if (accountStatePtr->account()->hasDefaultSyncRoot()) {
|
||||
Folder::prepareFolder(accountStatePtr->account()->defaultSyncRoot(),
|
||||
AccountManager::instance()->accounts().size() == 1
|
||||
? Theme::instance()->appNameGUI()
|
||||
: u"%1 - %2"_s.arg(Theme::instance()->appNameGUI(), accountStatePtr->account()->davDisplayName()),
|
||||
accountStatePtr->account()->davDisplayName(), true);
|
||||
}
|
||||
}
|
||||
};
|
||||
updateSyncRoots();
|
||||
connect(AccountManager::instance(), &AccountManager::accountAdded, this, updateSyncRoots);
|
||||
#endif
|
||||
connect(AccountManager::instance(), &AccountManager::accountRemoved,
|
||||
this, &FolderMan::slotRemoveFoldersForAccount);
|
||||
|
||||
connect(_lockWatcher.data(), &LockWatcher::fileUnlocked, this, [this](const QString &path, FileSystem::LockMode) {
|
||||
if (Folder *f = folderForPath(path)) {
|
||||
// Treat this equivalently to the file being reported by the file watcher
|
||||
f->slotWatchedPathsChanged({path}, Folder::ChangeReason::UnLock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FolderMan *FolderMan::instance()
|
||||
{
|
||||
Q_ASSERT(_instance);
|
||||
return _instance;
|
||||
}
|
||||
|
||||
FolderMan::~FolderMan()
|
||||
{
|
||||
unloadAndDeleteAllFolders();
|
||||
qDeleteAll(_folders);
|
||||
_instance = nullptr;
|
||||
}
|
||||
|
||||
const QVector<Folder *> &FolderMan::folders() const
|
||||
{
|
||||
return _folders;
|
||||
}
|
||||
|
||||
void FolderMan::unloadFolder(Folder *f)
|
||||
{
|
||||
Q_ASSERT(f);
|
||||
|
||||
_folders.removeAll(f);
|
||||
_socketApi->slotUnregisterPath(f);
|
||||
|
||||
|
||||
if (!f->hasSetupError()) {
|
||||
disconnect(f, nullptr, _socketApi.get(), nullptr);
|
||||
disconnect(f, nullptr, this, nullptr);
|
||||
disconnect(f, nullptr, &f->syncEngine().syncFileStatusTracker(), nullptr);
|
||||
disconnect(&f->syncEngine(), nullptr, f, nullptr);
|
||||
disconnect(
|
||||
&f->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::fileStatusChanged, _socketApi.get(), &SocketApi::broadcastStatusPushMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void FolderMan::unloadAndDeleteAllFolders()
|
||||
{
|
||||
if (!_folders.isEmpty()) {
|
||||
// clear the list of existing folders.
|
||||
saveFolders();
|
||||
const auto folders = std::move(_folders);
|
||||
for (auto *folder : folders) {
|
||||
// unload and disconnect all signals
|
||||
unloadFolder(folder);
|
||||
folder->deleteLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FolderMan::registerFolderWithSocketApi(Folder *folder)
|
||||
{
|
||||
if (!folder)
|
||||
return;
|
||||
if (!QDir(folder->path()).exists())
|
||||
return;
|
||||
|
||||
// register the folder with the socket API
|
||||
if (folder->canSync())
|
||||
_socketApi->slotRegisterPath(folder);
|
||||
}
|
||||
|
||||
std::optional<qsizetype> FolderMan::loadFolders()
|
||||
{
|
||||
qCInfo(lcFolderMan) << u"Setup folders from settings file";
|
||||
|
||||
auto settings = ConfigFile::makeQSettings();
|
||||
const auto size = settings.beginReadArray(foldersC());
|
||||
|
||||
for (auto i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
FolderDefinition folderDefinition = FolderDefinition::load(settings);
|
||||
|
||||
if (SyncJournalDb::dbIsTooNewForClient(folderDefinition.absoluteJournalPath())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto vfs = VfsPluginManager::instance().createVfsFromPlugin(folderDefinition.virtualFilesMode);
|
||||
if (!vfs) {
|
||||
// TODO: Must do better error handling
|
||||
qFatal("Could not load plugin");
|
||||
}
|
||||
auto account = AccountManager::instance()->account(folderDefinition.accountUUID());
|
||||
if (account.isNull()) {
|
||||
qFatal("Could not load account");
|
||||
}
|
||||
addFolderInternal(std::move(folderDefinition), account, std::move(vfs));
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
Q_EMIT folderListChanged();
|
||||
|
||||
return _folders.size();
|
||||
}
|
||||
|
||||
void FolderMan::saveFolders()
|
||||
{
|
||||
auto settings = ConfigFile::makeQSettings();
|
||||
settings.remove(foldersC());
|
||||
settings.beginWriteArray(foldersC(), _folders.size());
|
||||
int i = 0;
|
||||
for (const auto folder : std::as_const(_folders)) {
|
||||
settings.setArrayIndex(i++);
|
||||
auto definitionToSave = folder->_definition;
|
||||
// with spaces we rely on the space id
|
||||
// we save the dav URL nevertheless to have it available during startup
|
||||
definitionToSave.setWebDavUrl(folder->webDavUrl());
|
||||
definitionToSave.setDisplayName(folder->displayName());
|
||||
FolderDefinition::save(settings, definitionToSave);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
bool FolderMan::ensureJournalGone(const QString &journalDbFile)
|
||||
{
|
||||
// remove the old journal file
|
||||
while (QFile::exists(journalDbFile) && !QFile::remove(journalDbFile)) {
|
||||
qCWarning(lcFolderMan) << u"Could not remove old db file at" << journalDbFile;
|
||||
int ret = QMessageBox::warning(nullptr, tr("Could not reset folder state"),
|
||||
tr("An old sync journal %1 was found, "
|
||||
"but could not be removed. Please make sure "
|
||||
"that no application is currently using it.")
|
||||
.arg(QDir::fromNativeSeparators(QDir::cleanPath(journalDbFile))),
|
||||
QMessageBox::Retry | QMessageBox::Abort);
|
||||
if (ret == QMessageBox::Abort) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SocketApi *FolderMan::socketApi()
|
||||
{
|
||||
return _socketApi.get();
|
||||
}
|
||||
|
||||
void FolderMan::slotFolderCanSyncChanged()
|
||||
{
|
||||
Folder *f = qobject_cast<Folder *>(sender());
|
||||
OC_ASSERT(f);
|
||||
if (f->canSync()) {
|
||||
_socketApi->slotRegisterPath(f);
|
||||
} else {
|
||||
_socketApi->slotUnregisterPath(f);
|
||||
}
|
||||
}
|
||||
|
||||
void FolderMan::scheduleAllFolders()
|
||||
{
|
||||
for (auto *f : std::as_const(_folders)) {
|
||||
if (f && f->canSync()) {
|
||||
scheduler()->enqueueFolder(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FolderMan::slotSyncOnceFileUnlocks(const QString &path, FileSystem::LockMode mode)
|
||||
{
|
||||
_lockWatcher->addFile(path, mode);
|
||||
}
|
||||
|
||||
void FolderMan::slotIsConnectedChanged()
|
||||
{
|
||||
AccountStatePtr accountState(qobject_cast<AccountState *>(sender()));
|
||||
if (!accountState) {
|
||||
return;
|
||||
}
|
||||
QString accountName = accountState->account()->displayNameWithHost();
|
||||
|
||||
if (accountState->isConnected()) {
|
||||
qCInfo(lcFolderMan) << u"Account" << accountName << u"connected, scheduling its folders";
|
||||
|
||||
for (auto *f : std::as_const(_folders)) {
|
||||
if (f->accountState() == accountState && f->canSync()) {
|
||||
scheduler()->enqueueFolder(f);
|
||||
}
|
||||
}
|
||||
} else if (accountState->state() == AccountState::State::Disconnected || accountState->state() == AccountState::State::SignedOut) {
|
||||
qCInfo(lcFolderMan) << u"Account" << accountName << u"disconnected or paused, terminating or descheduling sync folders";
|
||||
|
||||
if (scheduler()->currentSync() && scheduler()->currentSync()->accountState() == accountState) {
|
||||
scheduler()->terminateCurrentSync(tr("Account disconnected or paused"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only enable or disable foldermans will schedule and do syncs.
|
||||
// this is not the same as Pause and Resume of folders.
|
||||
void FolderMan::setSyncEnabled(bool enabled)
|
||||
{
|
||||
if (enabled) {
|
||||
// We have things in our queue that were waiting for the connection to come back on.
|
||||
scheduler()->start();
|
||||
} else {
|
||||
scheduler()->stop();
|
||||
}
|
||||
qCInfo(lcFolderMan) << Q_FUNC_INFO << enabled;
|
||||
// force a redraw in case the network connect status changed
|
||||
Q_EMIT folderSyncStateChange(nullptr);
|
||||
}
|
||||
|
||||
void FolderMan::slotRemoveFoldersForAccount(const AccountStatePtr &accountState)
|
||||
{
|
||||
QList<Folder *> foldersToRemove;
|
||||
// reserve a magic number
|
||||
foldersToRemove.reserve(16);
|
||||
for (auto *folder : std::as_const(_folders)) {
|
||||
if (folder->accountState() == accountState) {
|
||||
foldersToRemove.append(folder);
|
||||
}
|
||||
}
|
||||
for (const auto &f : foldersToRemove) {
|
||||
removeFolder(f);
|
||||
}
|
||||
}
|
||||
|
||||
void FolderMan::slotServerVersionChanged(Account *account)
|
||||
{
|
||||
// Pause folders if the server version is unsupported
|
||||
if (account->serverSupportLevel() == Account::ServerSupportLevel::Unsupported) {
|
||||
qCWarning(lcFolderMan) << u"The server version is unsupported:" << account->capabilities().status().versionString()
|
||||
<< u"pausing all folders on the account";
|
||||
|
||||
for (auto &f : std::as_const(_folders)) {
|
||||
if (f->accountState()->account().data() == account) {
|
||||
f->setSyncPaused(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FolderMan::isAnySyncRunning() const
|
||||
{
|
||||
if (_scheduler->hasCurrentRunningSyncRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (auto f : _folders) {
|
||||
if (f->isSyncRunning())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Folder *FolderMan::addFolder(const AccountStatePtr &accountState, const FolderDefinition &folderDefinition)
|
||||
{
|
||||
// Choose a db filename
|
||||
auto definition = folderDefinition;
|
||||
definition.journalPath = SyncJournalDb::makeDbName(folderDefinition.localPath());
|
||||
|
||||
if (!ensureJournalGone(definition.absoluteJournalPath())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto vfs = VfsPluginManager::instance().createVfsFromPlugin(folderDefinition.virtualFilesMode);
|
||||
if (!vfs) {
|
||||
qCWarning(lcFolderMan) << u"Could not load plugin for mode" << folderDefinition.virtualFilesMode;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto folder = addFolderInternal(definition, accountState, std::move(vfs));
|
||||
|
||||
if (folder) {
|
||||
Q_EMIT folderSyncStateChange(folder);
|
||||
Q_EMIT folderListChanged();
|
||||
}
|
||||
saveFolders();
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
Folder *FolderMan::addFolderInternal(
|
||||
FolderDefinition folderDefinition,
|
||||
const AccountStatePtr &accountState,
|
||||
std::unique_ptr<Vfs> vfs)
|
||||
{
|
||||
auto folder = new Folder(folderDefinition, accountState, std::move(vfs), this);
|
||||
|
||||
qCInfo(lcFolderMan) << u"Adding folder to Folder Map " << folder << folder->path();
|
||||
_folders.push_back(folder);
|
||||
|
||||
// See matching disconnects in unloadFolder().
|
||||
if (!folder->hasSetupError()) {
|
||||
connect(folder, &Folder::syncStateChange, _socketApi.get(), [folder, this] { _socketApi->slotUpdateFolderView(folder); });
|
||||
connect(folder, &Folder::syncStateChange, this, [folder, this] { Q_EMIT folderSyncStateChange(folder); });
|
||||
connect(folder, &Folder::canSyncChanged, this, &FolderMan::slotFolderCanSyncChanged);
|
||||
connect(
|
||||
&folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::fileStatusChanged, _socketApi.get(), &SocketApi::broadcastStatusPushMessage);
|
||||
connect(folder, &Folder::watchedFileChangedExternally, &folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::slotPathTouched);
|
||||
registerFolderWithSocketApi(folder);
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
Folder *FolderMan::folderForPath(const QString &path, QString *relativePath)
|
||||
{
|
||||
QString absolutePath = QDir::cleanPath(path) + QLatin1Char('/');
|
||||
|
||||
for (auto *folder : std::as_const(_folders)) {
|
||||
const QString folderPath = folder->cleanPath() + QLatin1Char('/');
|
||||
|
||||
if (FileSystem::isChildPathOf2(absolutePath, folderPath).testAnyFlag(FileSystem::ChildResult::IsChild)) {
|
||||
if (relativePath) {
|
||||
*relativePath = absolutePath.mid(folderPath.length());
|
||||
relativePath->chop(1); // we added a '/' above
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
|
||||
if (relativePath)
|
||||
relativePath->clear();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void FolderMan::removeFolder(Folder *f)
|
||||
{
|
||||
if (!OC_ENSURE(f)) {
|
||||
return;
|
||||
}
|
||||
|
||||
qCInfo(lcFolderMan) << u"Removing " << f->path();
|
||||
|
||||
if (scheduler()->currentSync() == f) {
|
||||
// abort the sync now
|
||||
scheduler()->terminateCurrentSync(tr("Folder is about to be removed"));
|
||||
}
|
||||
|
||||
f->setSyncPaused(true);
|
||||
f->wipeForRemoval();
|
||||
|
||||
unloadFolder(f);
|
||||
|
||||
Q_EMIT folderRemoved(f);
|
||||
Q_EMIT folderListChanged();
|
||||
|
||||
f->deleteLater();
|
||||
saveFolders();
|
||||
}
|
||||
|
||||
QString FolderMan::getBackupName(QString fullPathName) const
|
||||
{
|
||||
if (fullPathName.endsWith(QLatin1String("/")))
|
||||
fullPathName.chop(1);
|
||||
|
||||
if (fullPathName.isEmpty())
|
||||
return QString();
|
||||
|
||||
QString newName = fullPathName + tr(" (backup)");
|
||||
QFileInfo fi(newName);
|
||||
int cnt = 2;
|
||||
do {
|
||||
if (fi.exists()) {
|
||||
newName = fullPathName + tr(" (backup %1)").arg(cnt++);
|
||||
fi.setFile(newName);
|
||||
}
|
||||
} while (fi.exists());
|
||||
|
||||
return newName;
|
||||
}
|
||||
|
||||
|
||||
void FolderMan::setDirtyNetworkLimits()
|
||||
{
|
||||
for (auto *f : std::as_const(_folders)) {
|
||||
// set only in busy folders. Otherwise they read the config anyway.
|
||||
if (f && f->isSyncRunning()) {
|
||||
f->setDirtyNetworkLimits();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TrayOverallStatusResult FolderMan::trayOverallStatus(const QVector<Folder *> &folders)
|
||||
{
|
||||
TrayOverallStatusResult result;
|
||||
|
||||
// if one of them has an error -> show error
|
||||
// if one is paused, but others ok, show ok
|
||||
//
|
||||
for (auto *folder : folders) {
|
||||
result.addResult(folder);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString FolderMan::trayTooltipStatusString(
|
||||
const SyncResult &result, bool paused)
|
||||
{
|
||||
QString folderMessage;
|
||||
switch (result.status()) {
|
||||
case SyncResult::Success:
|
||||
[[fallthrough]];
|
||||
case SyncResult::Problem:
|
||||
if (result.hasUnresolvedConflicts()) {
|
||||
folderMessage = tr("Sync was successful, unresolved conflicts.");
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
default:
|
||||
return Utility::enumToDisplayName(result.status());
|
||||
}
|
||||
if (paused) {
|
||||
// sync is disabled.
|
||||
folderMessage = tr("%1 (Sync is paused)").arg(folderMessage);
|
||||
}
|
||||
return folderMessage;
|
||||
}
|
||||
|
||||
static QString checkPathForSyncRootMarkingRecursive(const QString &path, FolderMan::NewFolderType folderType, const QUuid &accountUuid)
|
||||
{
|
||||
std::pair<QString, QUuid> existingTags = Utility::getDirectorySyncRootMarkings(path);
|
||||
if (!existingTags.first.isEmpty()) {
|
||||
if (existingTags.first != Theme::instance()->orgDomainName()) {
|
||||
// another application uses this as spaces root folder
|
||||
return FolderMan::tr("The folder »%1« is already in use by application %2!").arg(path, existingTags.first);
|
||||
}
|
||||
|
||||
// Looks good, it's our app, let's check the account tag:
|
||||
switch (folderType) {
|
||||
case FolderMan::NewFolderType::SpacesFolder:
|
||||
if (existingTags.second == accountUuid) {
|
||||
// Nice, that's what we like, the sync root for our account in our app. No error.
|
||||
return {};
|
||||
}
|
||||
[[fallthrough]];
|
||||
case FolderMan::NewFolderType::SpacesSyncRoot:
|
||||
// It's our application but we don't want to create a spaces folder, so it must be another space root
|
||||
return FolderMan::tr("The folder »%1« is already in use by another account.").arg(path);
|
||||
}
|
||||
}
|
||||
|
||||
QString parent = QFileInfo(path).path();
|
||||
if (parent == path) { // root dir, stop recursing
|
||||
return {};
|
||||
}
|
||||
|
||||
return checkPathForSyncRootMarkingRecursive(parent, folderType, accountUuid);
|
||||
}
|
||||
|
||||
QString FolderMan::checkPathValidityRecursive(const QString &path, FolderMan::NewFolderType folderType, const QUuid &accountUuid)
|
||||
{
|
||||
if (path.isEmpty()) {
|
||||
return FolderMan::tr("No valid folder selected!");
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QNtfsPermissionCheckGuard ntfs_perm;
|
||||
#endif
|
||||
|
||||
auto pathLenghtCheck = Folder::checkPathLength(path);
|
||||
if (!pathLenghtCheck) {
|
||||
return pathLenghtCheck.error();
|
||||
}
|
||||
|
||||
const QFileInfo selectedPathInfo(path);
|
||||
if (!selectedPathInfo.exists()) {
|
||||
const QString parentPath = selectedPathInfo.path();
|
||||
if (parentPath != path) {
|
||||
return checkPathValidityRecursive(parentPath, folderType, accountUuid);
|
||||
}
|
||||
return FolderMan::tr("The selected path does not exist!");
|
||||
}
|
||||
|
||||
if (numberOfSyncJournals(selectedPathInfo.filePath()) != 0) {
|
||||
return FolderMan::tr("The folder »%1« is used in a folder sync connection!").arg(QDir::toNativeSeparators(selectedPathInfo.filePath()));
|
||||
}
|
||||
|
||||
// At this point we know there is no syncdb in the parent hyrarchy, check for spaces sync root.
|
||||
|
||||
if (!selectedPathInfo.isDir()) {
|
||||
return FolderMan::tr("The selected path is not a folder!");
|
||||
}
|
||||
|
||||
if (!selectedPathInfo.isWritable()) {
|
||||
return FolderMan::tr("You have no permission to write to the selected folder!");
|
||||
}
|
||||
|
||||
return checkPathForSyncRootMarkingRecursive(path, folderType, accountUuid);
|
||||
}
|
||||
|
||||
/*
|
||||
* - spaces sync root not in syncdb folder
|
||||
* - spaces sync root not in another spaces sync root
|
||||
*
|
||||
* - space not in syncdb folder
|
||||
* - space *can* be in sync root
|
||||
* - space not in spaces sync root of other account (check with account uuid)
|
||||
*/
|
||||
QString FolderMan::checkPathValidityForNewFolder(const QString &path, NewFolderType folderType, const QUuid &accountUuid) const
|
||||
{
|
||||
// check if the local directory isn't used yet in another sync
|
||||
if (path.isEmpty()) {
|
||||
return u"Passingg an empty path is not supported"_s;
|
||||
}
|
||||
const QString userDir = FileSystem::canonicalPath(path) + QLatin1Char('/');
|
||||
for (auto f : _folders) {
|
||||
const QString folderDir = FileSystem::canonicalPath(f->path()) + QLatin1Char('/');
|
||||
|
||||
const auto isChild = FileSystem::isChildPathOf2(folderDir, userDir);
|
||||
if (isChild.testFlag(FileSystem::ChildResult::IsEqual)) {
|
||||
return tr("There is already a sync from the server to this local folder. "
|
||||
"Please pick another local folder!");
|
||||
} else if (isChild.testFlag(FileSystem::ChildResult::IsChild)) {
|
||||
return tr("The local folder »%1« already contains a folder used in a folder sync connection. "
|
||||
"Please pick another local folder!")
|
||||
.arg(QDir::toNativeSeparators(path));
|
||||
}
|
||||
|
||||
if (FileSystem::isChildPathOf(userDir, folderDir)) {
|
||||
return tr("The local folder »%1« is already contained in a folder used in a folder sync connection. "
|
||||
"Please pick another local folder!")
|
||||
.arg(QDir::toNativeSeparators(path));
|
||||
}
|
||||
}
|
||||
|
||||
const auto result = checkPathValidityRecursive(path, folderType, accountUuid);
|
||||
if (!result.isEmpty()) {
|
||||
return tr("Please pick another local folder for »%1«.").arg(result);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QString FolderMan::findGoodPathForNewSyncFolder(
|
||||
const QString &basePath, const QString &newFolder, FolderMan::NewFolderType folderType, const QUuid &accountUuid)
|
||||
{
|
||||
OC_ASSERT(!accountUuid.isNull() || folderType == FolderMan::NewFolderType::SpacesSyncRoot);
|
||||
|
||||
// reserve extra characters to allow appending of a number
|
||||
const QString normalisedPath = FileSystem::createPortableFileName(basePath, newFolder, std::string_view(" (100)").size());
|
||||
|
||||
// If the parent folder is a sync folder or contained in one, we can't
|
||||
// possibly find a valid sync folder inside it.
|
||||
// Example: Someone syncs their home directory. Then ~/foobar is not
|
||||
// going to be an acceptable sync folder path for any value of foobar.
|
||||
// If relativePath is empty, the path is equal to newFolder, and we will find a name in the following loop
|
||||
QString relativePath;
|
||||
if (FolderMan::instance()->folderForPath(FileSystem::canonicalPath(normalisedPath), &relativePath) && !relativePath.isEmpty()) {
|
||||
// Any path with that parent is going to be unacceptable,
|
||||
// so just keep it as-is.
|
||||
return FileSystem::canonicalPath(normalisedPath);
|
||||
}
|
||||
// Count attempts and give up eventually
|
||||
{
|
||||
QString folder = normalisedPath;
|
||||
for (int attempt = 2; attempt <= 100; ++attempt) {
|
||||
if (!QFileInfo::exists(folder) && FolderMan::instance()->checkPathValidityForNewFolder(folder, folderType, accountUuid).isEmpty()) {
|
||||
return FileSystem::canonicalPath(folder);
|
||||
}
|
||||
folder = normalisedPath + QStringLiteral(" (%1)").arg(attempt);
|
||||
}
|
||||
}
|
||||
// we failed to find a non existing path
|
||||
Q_ASSERT(false);
|
||||
return FileSystem::canonicalPath(normalisedPath);
|
||||
}
|
||||
|
||||
bool FolderMan::ignoreHiddenFiles() const
|
||||
{
|
||||
if (_folders.empty()) {
|
||||
return true;
|
||||
}
|
||||
return _folders.first()->ignoreHiddenFiles();
|
||||
}
|
||||
|
||||
void FolderMan::setIgnoreHiddenFiles(bool ignore)
|
||||
{
|
||||
// Note that the setting will revert to 'true' if all folders
|
||||
// are deleted...
|
||||
for (auto *folder : std::as_const(_folders)) {
|
||||
folder->setIgnoreHiddenFiles(ignore);
|
||||
}
|
||||
saveFolders();
|
||||
}
|
||||
|
||||
Result<void, QString> FolderMan::unsupportedConfiguration(const QString &path) const
|
||||
{
|
||||
auto it = _unsupportedConfigurationError.find(path);
|
||||
if (it == _unsupportedConfigurationError.end()) {
|
||||
it = _unsupportedConfigurationError.insert(path, [&]() -> Result<void, QString> {
|
||||
if (numberOfSyncJournals(path) > 1) {
|
||||
const QString error = tr("Multiple accounts are sharing the folder »%1«.\n"
|
||||
"This configuration is know to lead to dataloss and is no longer supported.\n"
|
||||
"Please consider removing this folder from the account and adding it again.")
|
||||
.arg(path);
|
||||
if (Theme::instance()->warnOnMultipleDb()) {
|
||||
qCWarning(lcFolderMan) << error;
|
||||
return error;
|
||||
} else {
|
||||
qCWarning(lcFolderMan) << error << u"this error is not displayed to the user as this is a branded"
|
||||
<< u"client and the error itself might be a false positive caused by a previous broken migration";
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}());
|
||||
}
|
||||
return *it;
|
||||
}
|
||||
|
||||
bool FolderMan::isSpaceSynced(GraphApi::Space *space) const
|
||||
{
|
||||
auto it = std::find_if(_folders.cbegin(), _folders.cend(), [space](auto f) { return f->space() == space; });
|
||||
return it != _folders.cend();
|
||||
}
|
||||
|
||||
void FolderMan::slotReloadSyncOptions()
|
||||
{
|
||||
for (auto *f : std::as_const(_folders)) {
|
||||
if (f) {
|
||||
f->reloadSyncOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Folder *FolderMan::addFolderFromWizard(const AccountStatePtr &accountStatePtr, FolderDefinition &&folderDefinition, bool useVfs)
|
||||
{
|
||||
if (!FolderMan::prepareFolder(folderDefinition.localPath())) {
|
||||
return {};
|
||||
}
|
||||
|
||||
folderDefinition.ignoreHiddenFiles = ignoreHiddenFiles();
|
||||
|
||||
if (useVfs) {
|
||||
folderDefinition.virtualFilesMode = VfsPluginManager::instance().bestAvailableVfsMode();
|
||||
}
|
||||
|
||||
auto newFolder = addFolder(accountStatePtr, folderDefinition);
|
||||
|
||||
if (newFolder) {
|
||||
qCDebug(lcFolderMan) << u"Local sync folder" << folderDefinition.localPath() << u"successfully created!";
|
||||
} else {
|
||||
qCWarning(lcFolderMan) << u"Failed to create local sync folder!";
|
||||
}
|
||||
return newFolder;
|
||||
}
|
||||
|
||||
Folder *FolderMan::addFolderFromFolderWizardResult(const AccountStatePtr &accountStatePtr, const SyncConnectionDescription &description)
|
||||
{
|
||||
FolderDefinition definition{accountStatePtr->account()->uuid(), description.davUrl, description.spaceId, description.displayName};
|
||||
definition.setLocalPath(description.localPath);
|
||||
auto f = addFolderFromWizard(accountStatePtr, std::move(definition), description.useVirtualFiles);
|
||||
if (f) {
|
||||
f->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, description.selectiveSyncBlackList);
|
||||
f->setPriority(description.priority);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
QString FolderMan::suggestSyncFolder(NewFolderType folderType, const QUuid &accountUuid)
|
||||
{
|
||||
return FolderMan::instance()->findGoodPathForNewSyncFolder(QDir::homePath(), Theme::instance()->appName(), folderType, accountUuid);
|
||||
}
|
||||
|
||||
bool FolderMan::prepareFolder(const QString &folder)
|
||||
{
|
||||
if (!QFileInfo::exists(folder)) {
|
||||
if (!OC_ENSURE(QDir().mkpath(folder))) {
|
||||
return false;
|
||||
}
|
||||
FileSystem::setFolderMinimumPermissions(folder);
|
||||
Folder::prepareFolder(folder, {}, {}, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<FolderMan> FolderMan::createInstance()
|
||||
{
|
||||
OC_ASSERT(!_instance);
|
||||
_instance = new FolderMan();
|
||||
return std::unique_ptr<FolderMan>(_instance);
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include "folder.h"
|
||||
#include "scheduling/syncscheduler.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QQueue>
|
||||
|
||||
class TestFolderMigration;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class FolderMan;
|
||||
namespace TestUtils {
|
||||
// prototype for test friend
|
||||
FolderMan *folderMan();
|
||||
}
|
||||
|
||||
class Application;
|
||||
class SyncResult;
|
||||
class SocketApi;
|
||||
class LockWatcher;
|
||||
|
||||
/**
|
||||
* @brief Return object for Folder::trayOverallStatus.
|
||||
* @ingroup gui
|
||||
*/
|
||||
class TrayOverallStatusResult
|
||||
{
|
||||
public:
|
||||
QDateTime lastSyncDone;
|
||||
|
||||
void addResult(Folder *f);
|
||||
const SyncResult &overallStatus() const;
|
||||
|
||||
private:
|
||||
SyncResult _overallStatus;
|
||||
};
|
||||
|
||||
class QSFERA_GUI_EXPORT FolderMan : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* For a new folder, the type guides what kind of checks are done to ensure the new folder is not embedded in an existing one.
|
||||
* Or in case of a space folder, that if the new folder is in a Space sync root, it is the sync root of the same account.
|
||||
*/
|
||||
enum class NewFolderType {
|
||||
SpacesSyncRoot,
|
||||
SpacesFolder,
|
||||
};
|
||||
|
||||
struct SyncConnectionDescription
|
||||
{
|
||||
/***
|
||||
- * The WebDAV URL for the sync connection.
|
||||
*/
|
||||
QUrl davUrl;
|
||||
|
||||
/***
|
||||
* The id of the space
|
||||
*/
|
||||
QString spaceId;
|
||||
|
||||
/***
|
||||
* The local folder used for the sync.
|
||||
*/
|
||||
QString localPath;
|
||||
|
||||
/***
|
||||
* The Space name to display in the list of folders or an empty string.
|
||||
*/
|
||||
QString displayName;
|
||||
|
||||
/***
|
||||
* Wether to use virtual files.
|
||||
*/
|
||||
bool useVirtualFiles;
|
||||
|
||||
uint32_t priority;
|
||||
|
||||
QSet<QString> selectiveSyncBlackList;
|
||||
};
|
||||
|
||||
static QString suggestSyncFolder(NewFolderType folderType, const QUuid &accountUuid);
|
||||
|
||||
static QString checkPathValidityRecursive(const QString &path, FolderMan::NewFolderType folderType, const QUuid &accountUuid);
|
||||
|
||||
static std::unique_ptr<FolderMan> createInstance();
|
||||
~FolderMan() override;
|
||||
|
||||
/**
|
||||
* Helper to access the FolderMan instance
|
||||
* Warning: may be null in unit tests
|
||||
*/
|
||||
// TODO: use acces throug ocApp and remove that instance pointer
|
||||
static FolderMan *instance();
|
||||
|
||||
/// \returns empty if a downgrade of a folder was detected, otherwise it will return the number
|
||||
/// of folders that were set up (note: this can be zero when no folders were configured).
|
||||
std::optional<qsizetype> loadFolders();
|
||||
|
||||
const QVector<Folder *> &folders() const;
|
||||
|
||||
/** Adds a folder for an account, ensures the journal is gone and saves it in the settings.
|
||||
*/
|
||||
Folder *addFolder(const AccountStatePtr &accountState, const FolderDefinition &folderDefinition);
|
||||
|
||||
/**
|
||||
* Adds a folder for an account. Used to be part of the wizard code base. Constructs the folder definition from the parameters.
|
||||
* In case Wizard::SyncMode::SelectiveSync is used, nullptr is returned.
|
||||
*/
|
||||
Folder *addFolderFromWizard(const AccountStatePtr &accountStatePtr, FolderDefinition &&definition, bool useVfs);
|
||||
Folder *addFolderFromFolderWizardResult(const AccountStatePtr &accountStatePtr, const SyncConnectionDescription &config);
|
||||
|
||||
/** Removes a folder */
|
||||
void removeFolder(Folder *);
|
||||
|
||||
/**
|
||||
* Returns the folder which the file or directory stored in path is in
|
||||
*
|
||||
* Optionally, the path relative to the found folder is returned in
|
||||
* relativePath.
|
||||
*/
|
||||
Folder *folderForPath(const QString &path, QString *relativePath = nullptr);
|
||||
|
||||
/**
|
||||
* Ensures that a given directory does not contain a sync journal file.
|
||||
*
|
||||
* @returns false if the journal could not be removed, true otherwise.
|
||||
*/
|
||||
static bool ensureJournalGone(const QString &journalDbFile);
|
||||
|
||||
/// Produce text for use in the tray tooltip
|
||||
static QString trayTooltipStatusString(const SyncResult &result, bool paused);
|
||||
|
||||
/**
|
||||
* Compute status summarizing multiple folders
|
||||
* @return tuple containing folders, status, unresolvedConflicts and lastSyncDone
|
||||
*/
|
||||
static TrayOverallStatusResult trayOverallStatus(const QVector<Folder *> &folders);
|
||||
|
||||
SocketApi *socketApi();
|
||||
|
||||
/**
|
||||
* Check if @a path is a valid path for a new folder considering the already sync'ed items.
|
||||
* Make sure that this folder, or any subfolder is not sync'ed already.
|
||||
*
|
||||
* @returns an empty string if it is allowed, or an error if it is not allowed
|
||||
*/
|
||||
QString checkPathValidityForNewFolder(const QString &path, NewFolderType folderType, const QUuid &accountUuid) const;
|
||||
|
||||
/**
|
||||
* Attempts to find a non-existing, acceptable path for creating a new sync folder.
|
||||
*
|
||||
* Uses \a basePath as the baseline. It'll return this path if it's acceptable.
|
||||
*
|
||||
* Note that this can fail. If someone syncs ~ and \a basePath is ~/QSfera, no
|
||||
* subfolder of ~ would be a good candidate. When that happens \a basePath
|
||||
* is returned.
|
||||
*/
|
||||
static QString findGoodPathForNewSyncFolder(const QString &basePath, const QString &newFolder, NewFolderType folderType, const QUuid &accountUuid);
|
||||
|
||||
/**
|
||||
* While ignoring hidden files can theoretically be switched per folder,
|
||||
* it's currently a global setting that users can only change for all folders
|
||||
* at once.
|
||||
* These helper functions can be removed once it's properly per-folder.
|
||||
*/
|
||||
bool ignoreHiddenFiles() const;
|
||||
void setIgnoreHiddenFiles(bool ignore);
|
||||
|
||||
/**
|
||||
* Returns true if any folder is currently syncing.
|
||||
*
|
||||
* This might be a FolderMan-scheduled sync, or a externally
|
||||
* managed sync like a placeholder hydration.
|
||||
*/
|
||||
bool isAnySyncRunning() const;
|
||||
|
||||
/** Removes all folders */
|
||||
void unloadAndDeleteAllFolders();
|
||||
|
||||
/**
|
||||
* If enabled is set to false, no new folders will start to sync.
|
||||
* The current one will finish.
|
||||
*/
|
||||
void setSyncEnabled(bool);
|
||||
|
||||
SyncScheduler *scheduler() { return _scheduler; }
|
||||
|
||||
|
||||
/** Queues all folders for syncing. */
|
||||
void scheduleAllFolders();
|
||||
void setDirtyNetworkLimits();
|
||||
|
||||
/** If the folder configuration is no longer supported this will return an error string */
|
||||
Result<void, QString> unsupportedConfiguration(const QString &path) const;
|
||||
|
||||
[[nodiscard]] bool isSpaceSynced(GraphApi::Space *space) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* signal to indicate a folder has changed its sync state.
|
||||
*
|
||||
* Attention: The folder may be zero. Do a general update of the state then.
|
||||
*/
|
||||
void folderSyncStateChange(Folder *);
|
||||
|
||||
/**
|
||||
* Emitted whenever the list of configured folders changes.
|
||||
*/
|
||||
void folderListChanged();
|
||||
void folderRemoved(Folder *folder);
|
||||
|
||||
public Q_SLOTS:
|
||||
|
||||
/**
|
||||
* Schedules folders of newly connected accounts, terminates and
|
||||
* de-schedules folders of disconnected accounts.
|
||||
*/
|
||||
void slotIsConnectedChanged();
|
||||
|
||||
/**
|
||||
* Triggers a sync run once the lock on the given file is removed.
|
||||
*
|
||||
* Automatically detemines the folder that's responsible for the file.
|
||||
* See slotWatchedFileUnlocked().
|
||||
*/
|
||||
void slotSyncOnceFileUnlocks(const QString &path, FileSystem::LockMode mode);
|
||||
|
||||
/// This slot will tell all sync engines to reload the sync options.
|
||||
void slotReloadSyncOptions();
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotFolderCanSyncChanged();
|
||||
|
||||
void slotRemoveFoldersForAccount(const AccountStatePtr &accountState);
|
||||
|
||||
void slotServerVersionChanged(Account *account);
|
||||
|
||||
private:
|
||||
explicit FolderMan();
|
||||
|
||||
[[nodiscard]] static bool prepareFolder(const QString &folder);
|
||||
|
||||
/** Adds a new folder, does not add it to the account settings and
|
||||
* does not set an account on the new folder.
|
||||
*/
|
||||
Folder *addFolderInternal(FolderDefinition folderDefinition,
|
||||
const AccountStatePtr &accountState, std::unique_ptr<Vfs> vfs);
|
||||
|
||||
/* unloads a folder object, does not delete it */
|
||||
void unloadFolder(Folder *);
|
||||
|
||||
void saveFolders();
|
||||
|
||||
// finds all folder configuration files
|
||||
// and create the folders
|
||||
QString getBackupName(QString fullPathName) const;
|
||||
|
||||
// makes the folder known to the socket api
|
||||
void registerFolderWithSocketApi(Folder *folder);
|
||||
|
||||
QVector<Folder *> _folders;
|
||||
QString _folderConfigPath;
|
||||
|
||||
/// Folder aliases from the settings that weren't read
|
||||
QSet<QString> _additionalBlockedFolderAliases;
|
||||
|
||||
/// Watches files that couldn't be synced due to locks
|
||||
QScopedPointer<LockWatcher> _lockWatcher;
|
||||
|
||||
/// Scheduled folders that should be synced as soon as possible
|
||||
SyncScheduler *_scheduler;
|
||||
|
||||
std::unique_ptr<SocketApi> _socketApi;
|
||||
|
||||
mutable QMap<QString, Result<void, QString>> _unsupportedConfigurationError;
|
||||
|
||||
static FolderMan *_instance;
|
||||
friend class OCC::Application;
|
||||
friend class ::TestFolderMigration;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@kde.org>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "folderstatusmodel.h"
|
||||
#include "account.h"
|
||||
#include "accountstate.h"
|
||||
#include "folderman.h"
|
||||
#include "gui/networkinformation.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include "libsync/graphapi/space.h"
|
||||
#include "libsync/graphapi/spacesmanager.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFileIconProvider>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include <set>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Q_DECLARE_METATYPE(QPersistentModelIndex)
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcFolderStatus, "gui.folder.model", QtInfoMsg)
|
||||
|
||||
namespace {
|
||||
// minimum delay between progress updates
|
||||
constexpr auto progressUpdateTimeOutC = 1s;
|
||||
|
||||
QChar statusIconName(Folder *f)
|
||||
{
|
||||
auto status = f->syncResult();
|
||||
if (!f->accountState()->isConnected()) {
|
||||
status.setStatus(SyncResult::Status::Offline);
|
||||
} else if (f->isSyncPaused() || NetworkInformation::instance()->isBehindCaptivePortal() || NetworkInformation::instance()->isMetered()) {
|
||||
status.setStatus(SyncResult::Status::Paused);
|
||||
}
|
||||
return status.glype();
|
||||
}
|
||||
|
||||
class SubFolderInfo
|
||||
{
|
||||
public:
|
||||
class Progress
|
||||
{
|
||||
public:
|
||||
Progress() { }
|
||||
QString _progressString;
|
||||
QString _overallSyncString;
|
||||
float _overallPercent = 0;
|
||||
};
|
||||
SubFolderInfo(Folder *folder)
|
||||
: _folder(folder)
|
||||
{
|
||||
}
|
||||
Folder *const _folder;
|
||||
|
||||
Progress _progress;
|
||||
|
||||
std::chrono::steady_clock::time_point _lastProgressUpdated = std::chrono::steady_clock::now();
|
||||
ProgressInfo::Status _lastProgressUpdateStatus = ProgressInfo::None;
|
||||
};
|
||||
|
||||
void computeProgress(const ProgressInfo &progress, SubFolderInfo::Progress *pi)
|
||||
{
|
||||
// find the single item to display: This is going to be the bigger item, or the last completed
|
||||
// item if no items are in progress.
|
||||
SyncFileItem curItem = progress._lastCompletedItem;
|
||||
qint64 curItemProgress = -1; // -1 means finished
|
||||
qint64 biggerItemSize = 0;
|
||||
quint64 estimatedUpBw = 0;
|
||||
quint64 estimatedDownBw = 0;
|
||||
QStringList allFilenames;
|
||||
for (const auto &citm : progress._currentItems) {
|
||||
if (curItemProgress == -1 || (ProgressInfo::isSizeDependent(citm._item) && biggerItemSize < citm._item._size)) {
|
||||
curItemProgress = citm._progress.completed();
|
||||
curItem = citm._item;
|
||||
biggerItemSize = citm._item._size;
|
||||
}
|
||||
if (citm._item._direction != SyncFileItem::Up) {
|
||||
estimatedDownBw += progress.fileProgress(citm._item).estimatedBandwidth;
|
||||
} else {
|
||||
estimatedUpBw += progress.fileProgress(citm._item).estimatedBandwidth;
|
||||
}
|
||||
allFilenames.append(QApplication::translate("FolderStatus", "'%1'").arg(citm._item.localName()));
|
||||
}
|
||||
if (curItemProgress == -1) {
|
||||
curItemProgress = curItem._size;
|
||||
}
|
||||
|
||||
const QString itemFileName = curItem.localName();
|
||||
const QString kindString = Progress::asActionString(curItem);
|
||||
|
||||
QString fileProgressString;
|
||||
if (ProgressInfo::isSizeDependent(curItem)) {
|
||||
// quint64 estimatedBw = progress.fileProgress(curItem).estimatedBandwidth;
|
||||
if (estimatedUpBw || estimatedDownBw) {
|
||||
/*
|
||||
//: Example text: "uploading foobar.png (1MB of 2MB) time left 2 minutes at a rate of 24Kb/s"
|
||||
fileProgressString = tr("%1 %2 (%3 of %4) %5 left at a rate of %6/s")
|
||||
.arg(kindString, itemFileName, s1, s2,
|
||||
Utility::durationToDescriptiveString(progress.fileProgress(curItem).estimatedEta),
|
||||
Utility::octetsToString(estimatedBw) );
|
||||
*/
|
||||
//: Example text: "Syncing 'foo.txt', 'bar.txt'"
|
||||
fileProgressString = QApplication::translate("FolderStatus", "Syncing %1").arg(allFilenames.join(QStringLiteral(", ")));
|
||||
if (estimatedDownBw > 0) {
|
||||
fileProgressString.append(QApplication::translate("FolderStatus", ", ⬇️ %1/s").arg(Utility::octetsToString(estimatedDownBw)));
|
||||
}
|
||||
if (estimatedUpBw > 0) {
|
||||
fileProgressString.append(QApplication::translate("FolderStatus", ", ⬆️ %1/s").arg(Utility::octetsToString(estimatedUpBw)));
|
||||
}
|
||||
} else {
|
||||
//: Example text: "uploading foobar.png (2MB of 2MB)"
|
||||
fileProgressString = QApplication::translate("FolderStatus", "%1 %2 (%3 of %4)")
|
||||
.arg(kindString, itemFileName, Utility::octetsToString(curItemProgress), Utility::octetsToString(curItem._size));
|
||||
}
|
||||
} else if (!kindString.isEmpty()) {
|
||||
//: Example text: "uploading foobar.png"
|
||||
fileProgressString = QApplication::translate("FolderStatus", "%1 %2").arg(kindString, itemFileName);
|
||||
}
|
||||
pi->_progressString = fileProgressString;
|
||||
|
||||
// overall progress
|
||||
qint64 completedSize = progress.completedSize();
|
||||
qint64 completedFile = progress.completedFiles();
|
||||
qint64 currentFile = progress.currentFile();
|
||||
qint64 totalSize = qMax(completedSize, progress.totalSize());
|
||||
qint64 totalFileCount = qMax(currentFile, progress.totalFiles());
|
||||
QString overallSyncString;
|
||||
if (totalSize > 0) {
|
||||
const QString s1 = Utility::octetsToString(completedSize);
|
||||
const QString s2 = Utility::octetsToString(totalSize);
|
||||
|
||||
if (progress.trustEta()) {
|
||||
//: Example text: "5 minutes left, 12 MB of 345 MB, file 6 of 7"
|
||||
overallSyncString = QApplication::translate("FolderStatus", "%5 left, %1 of %2, file %3 of %4")
|
||||
.arg(s1, s2)
|
||||
.arg(currentFile)
|
||||
.arg(totalFileCount)
|
||||
.arg(Utility::durationToDescriptiveString1(std::chrono::milliseconds(progress.totalProgress().estimatedEta)));
|
||||
|
||||
} else {
|
||||
//: Example text: "12 MB of 345 MB, file 6 of 7"
|
||||
overallSyncString = QApplication::translate("FolderStatus", "%1 of %2, file %3 of %4").arg(s1, s2).arg(currentFile).arg(totalFileCount);
|
||||
}
|
||||
} else if (totalFileCount > 0) {
|
||||
// Don't attempt to estimate the time left if there is no kb to transfer.
|
||||
overallSyncString = QApplication::translate("FolderStatus", "file %1 of %2").arg(currentFile).arg(totalFileCount);
|
||||
}
|
||||
|
||||
pi->_overallSyncString = overallSyncString;
|
||||
|
||||
int overallPercent = 0;
|
||||
if (totalFileCount > 0) {
|
||||
// Add one 'byte' for each file so the percentage is moving when deleting or renaming files
|
||||
overallPercent = qRound(double(completedSize + completedFile) / double(totalSize + totalFileCount) * 100.0);
|
||||
}
|
||||
pi->_overallPercent = qBound(0, overallPercent, 100);
|
||||
}
|
||||
}
|
||||
|
||||
class FolderStatusModelPrivate
|
||||
{
|
||||
public:
|
||||
AccountStatePtr _accountState;
|
||||
std::vector<std::unique_ptr<SubFolderInfo>> _folders;
|
||||
};
|
||||
|
||||
FolderStatusModel::FolderStatusModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, d_ptr(new FolderStatusModelPrivate)
|
||||
|
||||
{
|
||||
}
|
||||
|
||||
FolderStatusModel::~FolderStatusModel()
|
||||
{
|
||||
Q_D(FolderStatusModel);
|
||||
delete d;
|
||||
}
|
||||
|
||||
void FolderStatusModel::setAccountState(const AccountStatePtr &accountState)
|
||||
{
|
||||
Q_D(FolderStatusModel);
|
||||
beginResetModel();
|
||||
d->_folders.clear();
|
||||
if (d->_accountState != accountState) {
|
||||
Q_ASSERT(!d->_accountState);
|
||||
d->_accountState = accountState;
|
||||
|
||||
connect(FolderMan::instance(), &FolderMan::folderSyncStateChange, this, &FolderStatusModel::slotFolderSyncStateChange);
|
||||
|
||||
connect(accountState->account()->spacesManager(), &GraphApi::SpacesManager::updated, this,
|
||||
[this] { Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); });
|
||||
connect(accountState->account()->spacesManager(), &GraphApi::SpacesManager::spaceChanged, this, [this](auto *space) {
|
||||
Q_D(FolderStatusModel);
|
||||
for (int i = 0; i < rowCount(); ++i) {
|
||||
if (d->_folders[i]->_folder->space() == space) {
|
||||
Q_EMIT dataChanged(index(i, 0), index(i, 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const auto &f : FolderMan::instance()->folders()) {
|
||||
if (!accountState)
|
||||
break;
|
||||
if (f->accountState() != accountState)
|
||||
continue;
|
||||
|
||||
d->_folders.push_back(std::make_unique<SubFolderInfo>(f));
|
||||
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo, this, [f, this](Folder *folder, const ProgressInfo &progress) {
|
||||
if (folder == f) {
|
||||
slotSetProgress(progress, f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QVariant FolderStatusModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
Q_D(const FolderStatusModel);
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
if (role == Qt::EditRole)
|
||||
return QVariant();
|
||||
|
||||
const auto &folderInfo = d->_folders.at(index.row());
|
||||
auto f = folderInfo->_folder;
|
||||
if (!f)
|
||||
return QVariant();
|
||||
|
||||
auto getErrors = [f] {
|
||||
auto errors = f->syncResult().errorStrings();
|
||||
const auto legacyError = FolderMan::instance()->unsupportedConfiguration(f->path());
|
||||
if (!legacyError) {
|
||||
errors.append(legacyError.error());
|
||||
}
|
||||
if (f->syncResult().hasUnresolvedConflicts()) {
|
||||
errors.append(tr("There are unresolved conflicts."));
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
auto getDescription = [f] {
|
||||
if (auto *space = f->space()) {
|
||||
if (!space->drive().getDescription().isEmpty()) {
|
||||
return space->drive().getDescription();
|
||||
}
|
||||
}
|
||||
return tr("Local folder: %1").arg(f->shortGuiLocalPath());
|
||||
};
|
||||
|
||||
switch (static_cast<Roles>(role)) {
|
||||
case Roles::Subtitle:
|
||||
return getDescription();
|
||||
case Roles::FolderErrorMsg:
|
||||
return getErrors();
|
||||
case Roles::DisplayName:
|
||||
return f->displayName();
|
||||
case Roles::FolderStatusIcon:
|
||||
return statusIconName(f);
|
||||
case Roles::SyncProgressItemString:
|
||||
return folderInfo->_progress._progressString;
|
||||
case Roles::SyncProgressOverallPercent:
|
||||
return folderInfo->_progress._overallPercent / 100.0;
|
||||
case Roles::SyncProgressOverallString:
|
||||
return folderInfo->_progress._overallSyncString;
|
||||
case Roles::Priority:
|
||||
// everything will be sorted in descending order, multiply the priority by 100 and prefer A over Z by appling a negative factor
|
||||
return QVariant::fromValue(f->priority() * 100 - (f->displayName().isEmpty() ? 0 : static_cast<int64_t>(f->displayName().at(0).toLower().unicode())));
|
||||
case Roles::Quota: {
|
||||
qint64 used{};
|
||||
qint64 total{};
|
||||
if (auto *space = f->space()) {
|
||||
const auto quota = space->drive().getQuota();
|
||||
if (quota.isValid()) {
|
||||
used = quota.getUsed();
|
||||
total = quota.getTotal();
|
||||
}
|
||||
}
|
||||
if (total <= 0) {
|
||||
return {};
|
||||
}
|
||||
return tr("%1 of %2 used").arg(Utility::octetsToString(used), Utility::octetsToString(total));
|
||||
}
|
||||
case Roles::Folder:
|
||||
return QVariant::fromValue(f);
|
||||
case Roles::AccessibleDescriptionRole: {
|
||||
QStringList desc = {f->displayName(), Utility::enumToDisplayName(f->syncResult().status())};
|
||||
desc << getErrors();
|
||||
if (f->syncResult().status() == SyncResult::SyncRunning) {
|
||||
desc << folderInfo->_progress._overallSyncString << QStringLiteral("%1%").arg(QString::number(folderInfo->_progress._overallPercent));
|
||||
}
|
||||
desc << getDescription();
|
||||
return desc.join(QLatin1Char(','));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Folder *FolderStatusModel::folder(const QModelIndex &index) const
|
||||
{
|
||||
Q_D(const FolderStatusModel);
|
||||
Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));
|
||||
return d->_folders.at(index.row())->_folder;
|
||||
}
|
||||
|
||||
int FolderStatusModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_D(const FolderStatusModel);
|
||||
Q_ASSERT(!parent.isValid());
|
||||
return static_cast<int>(d->_folders.size());
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> FolderStatusModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{static_cast<int>(Roles::DisplayName), "displayName"},
|
||||
{static_cast<int>(Roles::Subtitle), "subtitle"},
|
||||
{static_cast<int>(Roles::FolderStatusIcon), "statusIcon"},
|
||||
{static_cast<int>(Roles::SyncProgressOverallPercent), "progress"},
|
||||
{static_cast<int>(Roles::SyncProgressOverallString), "overallText"},
|
||||
{static_cast<int>(Roles::SyncProgressItemString), "itemText"},
|
||||
{static_cast<int>(Roles::FolderErrorMsg), "errorMsg"},
|
||||
{static_cast<int>(Roles::Quota), "quota"},
|
||||
{static_cast<int>(Roles::Folder), "folder"},
|
||||
{static_cast<int>(Roles::AccessibleDescriptionRole), "accessibleDescription"},
|
||||
};
|
||||
}
|
||||
|
||||
void FolderStatusModel::slotUpdateFolderState(Folder *folder)
|
||||
{
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
const auto folderIndex = indexOf(folder);
|
||||
if (folderIndex != -1) {
|
||||
Q_EMIT dataChanged(index(folderIndex, 0), index(folderIndex, 0));
|
||||
}
|
||||
}
|
||||
|
||||
void FolderStatusModel::slotSetProgress(const ProgressInfo &progress, Folder *f)
|
||||
{
|
||||
Q_D(FolderStatusModel);
|
||||
if (!qobject_cast<QWidget *>(QObject::parent())->isVisible()) {
|
||||
return; // for https://github.com/owncloud/client/issues/2648#issuecomment-71377909
|
||||
}
|
||||
|
||||
const int folderIndex = indexOf(f);
|
||||
if (folderIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const auto &folder = d->_folders.at(folderIndex);
|
||||
|
||||
auto *pi = &folder->_progress;
|
||||
// depending on the use of virtual files or small files this slot might be called very often.
|
||||
// throttle the model updates to prevent an needlessly high cpu usage used on ui updates.
|
||||
if (folder->_lastProgressUpdateStatus != progress.status() || (std::chrono::steady_clock::now() - folder->_lastProgressUpdated > progressUpdateTimeOutC)) {
|
||||
folder->_lastProgressUpdateStatus = progress.status();
|
||||
|
||||
switch (progress.status()) {
|
||||
case ProgressInfo::None:
|
||||
Q_UNREACHABLE();
|
||||
break;
|
||||
case ProgressInfo::Discovery:
|
||||
if (!progress._currentDiscoveredRemoteFolder.isEmpty()) {
|
||||
pi->_overallSyncString = tr("Checking for changes in remote »%1«").arg(progress._currentDiscoveredRemoteFolder);
|
||||
} else if (!progress._currentDiscoveredLocalFolder.isEmpty()) {
|
||||
pi->_overallSyncString = tr("Checking for changes in local »%1«").arg(progress._currentDiscoveredLocalFolder);
|
||||
}
|
||||
break;
|
||||
case ProgressInfo::Reconcile:
|
||||
pi->_overallSyncString = tr("Reconciling changes");
|
||||
break;
|
||||
case ProgressInfo::Propagation:
|
||||
Q_FALLTHROUGH();
|
||||
case ProgressInfo::Done:
|
||||
computeProgress(progress, pi);
|
||||
}
|
||||
Q_EMIT dataChanged(index(folderIndex, 0), index(folderIndex, 0));
|
||||
folder->_lastProgressUpdated = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
int FolderStatusModel::indexOf(Folder *f) const
|
||||
{
|
||||
Q_D(const FolderStatusModel);
|
||||
const auto found = std::find_if(d->_folders.cbegin(), d->_folders.cend(), [f](const auto &it) { return it->_folder == f; });
|
||||
if (found == d->_folders.cend()) {
|
||||
return -1;
|
||||
}
|
||||
return std::distance(d->_folders.cbegin(), found);
|
||||
}
|
||||
|
||||
void FolderStatusModel::slotFolderSyncStateChange(Folder *f)
|
||||
{
|
||||
Q_D(FolderStatusModel);
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int folderIndex = indexOf(f);
|
||||
if (folderIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &pi = d->_folders.at(folderIndex)->_progress;
|
||||
|
||||
SyncResult::Status state = f->syncResult().status();
|
||||
if (!f->canSync()) {
|
||||
// Reset progress info.
|
||||
pi = {};
|
||||
} else {
|
||||
switch (state) {
|
||||
case SyncResult::Queued:
|
||||
pi = {};
|
||||
pi._overallSyncString = Utility::enumToDisplayName(state);
|
||||
break;
|
||||
case SyncResult::Problem:
|
||||
[[fallthrough]];
|
||||
case SyncResult::Success:
|
||||
[[fallthrough]];
|
||||
case SyncResult::Error:
|
||||
// Reset the progress info after a sync.
|
||||
pi = {};
|
||||
break;
|
||||
case SyncResult::Undefined:
|
||||
[[fallthrough]];
|
||||
case SyncResult::SyncRunning:
|
||||
[[fallthrough]];
|
||||
case SyncResult::Paused:
|
||||
[[fallthrough]];
|
||||
case SyncResult::Offline:
|
||||
[[fallthrough]];
|
||||
case SyncResult::SetupError:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// update the icon etc. now
|
||||
slotUpdateFolderState(f);
|
||||
}
|
||||
|
||||
void FolderStatusModel::resetFolders()
|
||||
{
|
||||
Q_D(FolderStatusModel);
|
||||
setAccountState(d->_accountState);
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@kde.org>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef FOLDERSTATUSMODEL_H
|
||||
#define FOLDERSTATUSMODEL_H
|
||||
|
||||
#include "accountfwd.h"
|
||||
#include "progressdispatcher.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QElapsedTimer>
|
||||
#include <QLoggingCategory>
|
||||
#include <QQuickImageProvider>
|
||||
#include <QtQml/QQmlEngine>
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcFolderStatus)
|
||||
|
||||
class Folder;
|
||||
class PropfindJob;
|
||||
class FolderStatusModelPrivate;
|
||||
|
||||
/**
|
||||
* @brief The FolderStatusModel class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class FolderStatusModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
public:
|
||||
enum class Roles {
|
||||
AccessibleDescriptionRole = Qt::AccessibleDescriptionRole,
|
||||
DisplayName = Qt::UserRole + 1, // must be 0 as it is also used from the default delegate
|
||||
Subtitle,
|
||||
FolderErrorMsg,
|
||||
SyncProgressOverallPercent,
|
||||
SyncProgressOverallString,
|
||||
SyncProgressItemString,
|
||||
Priority,
|
||||
Quota,
|
||||
FolderStatusIcon,
|
||||
Folder
|
||||
};
|
||||
Q_ENUMS(Roles)
|
||||
|
||||
FolderStatusModel(QObject *parent = nullptr);
|
||||
~FolderStatusModel() override;
|
||||
void setAccountState(const AccountStatePtr &accountState);
|
||||
|
||||
Folder *folder(const QModelIndex &index) const;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
public Q_SLOTS:
|
||||
void slotUpdateFolderState(Folder *);
|
||||
void resetFolders();
|
||||
void slotSetProgress(const ProgressInfo &progress, Folder *f);
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotFolderSyncStateChange(Folder *f);
|
||||
|
||||
private:
|
||||
int indexOf(Folder *f) const;
|
||||
|
||||
Q_DECLARE_PRIVATE(FolderStatusModel)
|
||||
FolderStatusModelPrivate *d_ptr;
|
||||
};
|
||||
|
||||
|
||||
} // namespace OCC
|
||||
#endif // FOLDERSTATUSMODEL_H
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// event masks
|
||||
#include "folderwatcher.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <QFlags>
|
||||
#include <QTimer>
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include "folderwatcher_win.h"
|
||||
#elif defined(Q_OS_MAC)
|
||||
#include "folderwatcher_mac.h"
|
||||
#elif defined(Q_OS_UNIX)
|
||||
#include "folderwatcher_linux.h"
|
||||
#endif
|
||||
|
||||
#include "folder.h"
|
||||
#include "filesystem.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
constexpr auto notificationTimeoutC = 10s;
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcFolderWatcher, "gui.folderwatcher", QtInfoMsg)
|
||||
|
||||
FolderWatcher::FolderWatcher(Folder *folder)
|
||||
: QObject(folder)
|
||||
, _folder(folder)
|
||||
{
|
||||
_timer.setInterval(notificationTimeoutC);
|
||||
_timer.setSingleShot(true);
|
||||
connect(&_timer, &QTimer::timeout, this, [this] {
|
||||
auto paths = popChangeSet();
|
||||
Q_ASSERT(!paths.empty());
|
||||
if (!paths.isEmpty()) {
|
||||
qCInfo(lcFolderWatcher) << u"Detected changes in paths:" << paths;
|
||||
Q_EMIT pathChanged(paths);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FolderWatcher::~FolderWatcher()
|
||||
{
|
||||
}
|
||||
|
||||
void FolderWatcher::init(const QString &root)
|
||||
{
|
||||
_d.reset(new FolderWatcherPrivate(this, root));
|
||||
}
|
||||
|
||||
bool FolderWatcher::pathIsIgnored(const QString &path) const
|
||||
{
|
||||
Q_ASSERT(!path.isEmpty());
|
||||
Q_ASSERT(_folder);
|
||||
if (!QFileInfo::exists(path)) {
|
||||
std::error_code ec;
|
||||
const auto relPath =
|
||||
FileSystem::fromFilesystemPath(std::filesystem::proximate(FileSystem::toFilesystemPath(path), FileSystem::toFilesystemPath(_folder->path()), ec));
|
||||
Q_ASSERT(!ec);
|
||||
if (ec) {
|
||||
qCDebug(lcFolderWatcher) << u"* Failed to lookup record for path:" << path << u"error:" << ec.message();
|
||||
return false;
|
||||
}
|
||||
if (auto record = _folder->journalDb()->getFileRecord(relPath); record.isValid()) {
|
||||
// we know about the file, it got removed, we should not ignore that
|
||||
qCDebug(lcFolderWatcher) << u"* Not ignoring removed file" << path;
|
||||
return false;
|
||||
}
|
||||
// probably a temporary file that no longer exists
|
||||
qCDebug(lcFolderWatcher) << u"* Ignoring file" << path << u"It no longer exists and we don't have it in the database.";
|
||||
return true;
|
||||
}
|
||||
if (_folder->isFileExcludedAbsolute(path) && !Utility::isConflictFile(path)) {
|
||||
qCDebug(lcFolderWatcher) << u"* Ignoring file" << path;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FolderWatcher::isReliable() const
|
||||
{
|
||||
return _isReliable;
|
||||
}
|
||||
|
||||
void FolderWatcher::startNotificatonTest(const QString &path)
|
||||
{
|
||||
#ifdef Q_OS_MAC
|
||||
// Testing the folder watcher on OSX is harder because the watcher
|
||||
// automatically discards changes that were performed by our process.
|
||||
// It would still be useful to test but the OSX implementation
|
||||
// is deferred until later.
|
||||
return;
|
||||
#endif
|
||||
|
||||
Q_ASSERT(_testNotificationPath.isEmpty());
|
||||
Q_ASSERT(!path.isEmpty());
|
||||
_testNotificationPath = path;
|
||||
|
||||
// Don't do the local file modification immediately:
|
||||
// wait for FolderWatchPrivate::_ready
|
||||
startNotificationTestWhenReady();
|
||||
}
|
||||
|
||||
void FolderWatcher::startNotificationTestWhenReady()
|
||||
{
|
||||
if (!_testNotificationPath.isEmpty()) {
|
||||
// we already received the notification
|
||||
return;
|
||||
}
|
||||
if (!_d->isReady()) {
|
||||
QTimer::singleShot(1s, this, &FolderWatcher::startNotificationTestWhenReady);
|
||||
return;
|
||||
}
|
||||
|
||||
if (OC_ENSURE(QFile::exists(_testNotificationPath))) {
|
||||
const auto mtime = FileSystem::getModTime(_testNotificationPath);
|
||||
FileSystem::setModTime(_testNotificationPath, mtime + 1);
|
||||
} else {
|
||||
QFile f(_testNotificationPath);
|
||||
f.open(QIODevice::WriteOnly | QIODevice::Append);
|
||||
}
|
||||
|
||||
QTimer::singleShot(notificationTimeoutC + 5s, this, [this]() {
|
||||
if (!_testNotificationPath.isEmpty())
|
||||
Q_EMIT becameUnreliable(tr("The watcher did not receive a test notification."));
|
||||
_testNotificationPath.clear();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
int FolderWatcher::testLinuxWatchCount() const
|
||||
{
|
||||
#ifdef Q_OS_LINUX
|
||||
return _d->testWatchCount();
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
void FolderWatcher::addChanges(QSet<QString> &&paths)
|
||||
{
|
||||
Q_ASSERT(thread() == QThread::currentThread());
|
||||
// the timer must be inactive if we haven't received changes yet
|
||||
Q_ASSERT((!_timer.isActive() && _changeSet.isEmpty()) || !_changeSet.isEmpty());
|
||||
Q_ASSERT(!paths.isEmpty());
|
||||
// ------- handle ignores:
|
||||
auto it = paths.cbegin();
|
||||
while (it != paths.cend()) {
|
||||
// we cause a file change from time to time to check whether the folder watcher works as expected
|
||||
if (!_testNotificationPath.isEmpty() && Utility::fileNamesEqual(*it, _testNotificationPath)) {
|
||||
_testNotificationPath.clear();
|
||||
}
|
||||
if (pathIsIgnored(*it)) {
|
||||
it = paths.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (!paths.isEmpty()) {
|
||||
_changeSet.unite(paths);
|
||||
if (!_timer.isActive()) {
|
||||
_timer.start();
|
||||
// promote that we will report changes once _timer times out
|
||||
Q_EMIT changesDetected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QSet<QString> FolderWatcher::popChangeSet()
|
||||
{
|
||||
// stop the timer as we pop all queued changes
|
||||
_timer.stop();
|
||||
return std::move(_changeSet);
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QScopedPointer>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcFolderWatcher)
|
||||
|
||||
class FolderWatcherPrivate;
|
||||
class Folder;
|
||||
|
||||
/**
|
||||
* @brief Monitors a directory recursively for changes
|
||||
*
|
||||
* Folder Watcher monitors a directory and its sub directories
|
||||
* for changes in the local file system. Changes are signalled
|
||||
* through the pathChanged() signal.
|
||||
*
|
||||
* @ingroup gui
|
||||
*/
|
||||
|
||||
class QSFERA_GUI_EXPORT FolderWatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Construct, connect signals, call init()
|
||||
explicit FolderWatcher(Folder *folder = nullptr);
|
||||
~FolderWatcher() override;
|
||||
|
||||
/**
|
||||
* @param root Path of the root of the folder
|
||||
*/
|
||||
void init(const QString &root);
|
||||
|
||||
/* Check if the path is ignored. */
|
||||
virtual bool pathIsIgnored(const QString &path) const;
|
||||
|
||||
/**
|
||||
* Returns false if the folder watcher can't be trusted to capture all
|
||||
* notifications.
|
||||
*
|
||||
* For example, this can happen on linux if the inotify user limit from
|
||||
* /proc/sys/fs/inotify/max_user_watches is exceeded.
|
||||
*/
|
||||
bool isReliable() const;
|
||||
|
||||
/**
|
||||
* Triggers a change in the path and verifies a notification arrives.
|
||||
*
|
||||
* If no notification is seen, the folderwatcher marks itself as unreliable.
|
||||
* The path must be ignored by the watcher.
|
||||
*/
|
||||
void startNotificatonTest(const QString &path);
|
||||
|
||||
/// For testing linux behavior only
|
||||
int testLinuxWatchCount() const;
|
||||
|
||||
// pop the accumulated changes
|
||||
QSet<QString> popChangeSet();
|
||||
|
||||
|
||||
Q_SIGNALS:
|
||||
/** Emitted when one of the watched directories or one
|
||||
* of the contained files is changed. */
|
||||
void pathChanged(const QSet<QString> &path);
|
||||
|
||||
/**
|
||||
* We detected a file change, this signal can be used to trigger the prepareSync state
|
||||
*/
|
||||
void changesDetected();
|
||||
|
||||
/**
|
||||
* Emitted if some notifications were lost.
|
||||
*
|
||||
* Would happen, for example, if the number of pending notifications
|
||||
* exceeded the allocated buffer size on Windows. Note that the folder
|
||||
* watcher could still be able to capture all future notifications -
|
||||
* i.e. isReliable() is orthogonal to losing changes occasionally.
|
||||
*/
|
||||
void lostChanges();
|
||||
|
||||
/**
|
||||
* Signals when the watcher became unreliable. The string is a translated
|
||||
* message that can be shown to users.
|
||||
*/
|
||||
void becameUnreliable(const QString &message);
|
||||
private Q_SLOTS:
|
||||
void startNotificationTestWhenReady();
|
||||
|
||||
protected:
|
||||
// called from the implementations to indicate a change in path
|
||||
void addChanges(QSet<QString> &&paths);
|
||||
|
||||
private:
|
||||
QScopedPointer<FolderWatcherPrivate> _d;
|
||||
QTimer _timer;
|
||||
QSet<QString> _changeSet;
|
||||
Folder *_folder;
|
||||
bool _isReliable = true;
|
||||
|
||||
/** Path of the expected test notification */
|
||||
QString _testNotificationPath;
|
||||
|
||||
friend class FolderWatcherPrivate;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <cerrno>
|
||||
#include <sys/inotify.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "folder.h"
|
||||
#include "folderwatcher_linux.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QVarLengthArray>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path)
|
||||
: QObject()
|
||||
, _parent(p)
|
||||
, _folder(path)
|
||||
{
|
||||
_fd = inotify_init();
|
||||
if (_fd != -1) {
|
||||
_socket.reset(new QSocketNotifier(_fd, QSocketNotifier::Read));
|
||||
connect(_socket.data(), &QSocketNotifier::activated, this, &FolderWatcherPrivate::slotReceivedNotification);
|
||||
} else {
|
||||
qCWarning(lcFolderWatcher) << u"notify_init() failed: " << strerror(errno);
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod(this, [path, this] { slotAddFolderRecursive(path); });
|
||||
}
|
||||
|
||||
// attention: result list passed by reference!
|
||||
bool FolderWatcherPrivate::findFoldersBelow(const QDir &dir, QStringList &fullList)
|
||||
{
|
||||
if (!dir.exists()) {
|
||||
qCDebug(lcFolderWatcher) << u" - non existing path coming in: " << dir.absolutePath();
|
||||
return false;
|
||||
} else if (!dir.isReadable()) {
|
||||
qCDebug(lcFolderWatcher) << u" - path without read permissions coming in: " << dir.absolutePath();
|
||||
return false;
|
||||
}
|
||||
|
||||
const QDir::Filters filter = QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks | QDir::Hidden;
|
||||
|
||||
bool ok = true;
|
||||
for (const QString &path : dir.entryList({ QStringLiteral("*") }, filter)) {
|
||||
const QString fullPath(dir.path() + QLatin1String("/") + path);
|
||||
fullList.append(fullPath);
|
||||
ok &= findFoldersBelow(QDir(fullPath), fullList);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void FolderWatcherPrivate::inotifyRegisterPath(const QString &path)
|
||||
{
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int wd = inotify_add_watch(_fd, path.toUtf8().constData(),
|
||||
IN_CLOSE_WRITE | IN_ATTRIB | IN_MOVE | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF | IN_UNMOUNT | IN_ONLYDIR);
|
||||
if (wd != -1) {
|
||||
_watchToPath.insert(wd, path);
|
||||
_pathToWatch.insert(path, wd);
|
||||
} else {
|
||||
// If we're running out of memory or inotify watches, become unreliable.
|
||||
if (_parent->_isReliable && (errno == ENOMEM || errno == ENOSPC)) {
|
||||
_parent->_isReliable = false;
|
||||
Q_EMIT _parent->becameUnreliable(tr("This problem usually happens when the inotify watches are exhausted. "
|
||||
"Check the FAQ for details."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FolderWatcherPrivate::slotAddFolderRecursive(const QString &path)
|
||||
{
|
||||
if (_pathToWatch.contains(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int subdirCount = 0;
|
||||
qCDebug(lcFolderWatcher) << u"(+) Watcher:" << path;
|
||||
|
||||
QDir inPath(path);
|
||||
inotifyRegisterPath(inPath.absolutePath());
|
||||
|
||||
QStringList allSubfolders;
|
||||
if (!findFoldersBelow(QDir(path), allSubfolders)) {
|
||||
qCWarning(lcFolderWatcher).nospace() << u"Could not traverse all sub folders of '" << path << u"'";
|
||||
}
|
||||
|
||||
for (const auto &subfolder : std::as_const(allSubfolders)) {
|
||||
QDir folder(subfolder);
|
||||
if (folder.exists() && !_pathToWatch.contains(folder.absolutePath())) {
|
||||
++subdirCount;
|
||||
if (_parent->pathIsIgnored(subfolder)) {
|
||||
qCDebug(lcFolderWatcher) << u"* Not adding" << folder.path();
|
||||
continue;
|
||||
}
|
||||
inotifyRegisterPath(folder.absolutePath());
|
||||
} else {
|
||||
qCDebug(lcFolderWatcher) << u" `-> discarded:" << folder.path();
|
||||
}
|
||||
}
|
||||
|
||||
if (subdirCount > 0) {
|
||||
qCDebug(lcFolderWatcher) << u" `-> and" << subdirCount << u"subdirectories";
|
||||
}
|
||||
|
||||
qCDebug(lcFolderWatcher) << u" --- Finished scanning" << path;
|
||||
}
|
||||
|
||||
void FolderWatcherPrivate::slotReceivedNotification(int fd)
|
||||
{
|
||||
int len;
|
||||
QVarLengthArray<char, 2048> buffer(2048);
|
||||
|
||||
while (true) {
|
||||
len = read(fd, buffer.data(), buffer.size());
|
||||
auto error = errno;
|
||||
/**
|
||||
* From inotify documentation:
|
||||
*
|
||||
* The behavior when the buffer given to read(2) is too
|
||||
* small to return information about the next event
|
||||
* depends on the kernel version: in kernels before 2.6.21,
|
||||
* read(2) returns 0; since kernel 2.6.21, read(2) fails with
|
||||
* the error EINVAL.
|
||||
*/
|
||||
if (len < 0 && error == EINVAL) {
|
||||
// double the buffer size
|
||||
buffer.resize(buffer.size() * 2);
|
||||
/* and try again ... */
|
||||
} else {
|
||||
// successful read
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QSet<QString> paths;
|
||||
// iterate over events in buffer
|
||||
struct inotify_event *event = nullptr;
|
||||
for (size_t bytePosition = 0; // start at the beginning of the buffer
|
||||
bytePosition + sizeof(inotify_event) < static_cast<unsigned>(len); // check that we still have at least sizeof(inotify_event) left in the buffer
|
||||
bytePosition += sizeof(inotify_event) + (event ? event->len : 0)) { // skip over the header and event-payload
|
||||
|
||||
// cast into an inotify_event
|
||||
event = reinterpret_cast<struct inotify_event *>(&buffer[bytePosition]);
|
||||
|
||||
if (event == nullptr) {
|
||||
qCDebug(lcFolderWatcher) << u"NULL event";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event->len == 0 || event->wd <= -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QByteArray fileName(event->name);
|
||||
|
||||
// Filter out journal changes - redundant with filtering in FolderWatcher::pathIsIgnored.
|
||||
if (fileName.startsWith("._sync_")
|
||||
|| fileName.startsWith(".csync_journal.db")
|
||||
|| fileName.startsWith(".sync_")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString p = _watchToPath[event->wd] + QLatin1Char('/') + QString::fromUtf8(fileName);
|
||||
paths.insert(p);
|
||||
|
||||
if ((event->mask & (IN_MOVED_TO | IN_CREATE))
|
||||
&& QFileInfo(p).isDir()
|
||||
&& !_parent->pathIsIgnored(p)) {
|
||||
slotAddFolderRecursive(p);
|
||||
}
|
||||
if (event->mask & (IN_MOVED_FROM | IN_DELETE)) {
|
||||
removeFoldersBelow(p);
|
||||
}
|
||||
}
|
||||
if (!paths.isEmpty()) {
|
||||
_parent->addChanges(std::move(paths));
|
||||
}
|
||||
}
|
||||
|
||||
void FolderWatcherPrivate::removeFoldersBelow(const QString &path)
|
||||
{
|
||||
auto it = _pathToWatch.find(path);
|
||||
if (it == _pathToWatch.end())
|
||||
return;
|
||||
|
||||
const QString pathSlash = path + QLatin1Char('/');
|
||||
|
||||
// Remove the entry and all subentries
|
||||
while (it != _pathToWatch.end()) {
|
||||
auto itPath = it.key();
|
||||
if (!itPath.startsWith(path))
|
||||
break;
|
||||
if (itPath != path && !itPath.startsWith(pathSlash)) {
|
||||
// order is 'foo', 'foo bar', 'foo/bar'
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto wid = it.value();
|
||||
inotify_rm_watch(_fd, wid);
|
||||
_watchToPath.remove(wid);
|
||||
it = _pathToWatch.erase(it);
|
||||
qCDebug(lcFolderWatcher) << u"Removed watch for" << itPath;
|
||||
}
|
||||
}
|
||||
|
||||
} // ns mirall
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QSocketNotifier>
|
||||
#include <QHash>
|
||||
#include <QDir>
|
||||
|
||||
#include "folderwatcher.h"
|
||||
|
||||
class QTimer;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* @brief Linux (inotify) API implementation of FolderWatcher
|
||||
* @ingroup gui
|
||||
*/
|
||||
class QSFERA_GUI_EXPORT FolderWatcherPrivate : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FolderWatcherPrivate() {}
|
||||
FolderWatcherPrivate(FolderWatcher *p, const QString &path);
|
||||
|
||||
int testWatchCount() const { return _pathToWatch.size(); }
|
||||
|
||||
/// On linux the watcher is ready when the ctor finished.
|
||||
constexpr bool isReady() const { return true; }
|
||||
|
||||
protected Q_SLOTS:
|
||||
void slotReceivedNotification(int fd);
|
||||
void slotAddFolderRecursive(const QString &path);
|
||||
|
||||
protected:
|
||||
bool findFoldersBelow(const QDir &dir, QStringList &fullList);
|
||||
void inotifyRegisterPath(const QString &path);
|
||||
void removeFoldersBelow(const QString &path);
|
||||
|
||||
private:
|
||||
FolderWatcher *_parent;
|
||||
|
||||
QString _folder;
|
||||
QHash<int, QString> _watchToPath;
|
||||
QMap<QString, int> _pathToWatch;
|
||||
QScopedPointer<QSocketNotifier> _socket;
|
||||
int _fd;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) by Markus Goetz <markus@woboq.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "folderwatcher.h"
|
||||
#include "folderwatcher_mac.h"
|
||||
|
||||
#include <cerrno>
|
||||
|
||||
#include <QScopeGuard>
|
||||
#include <QStringList>
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path)
|
||||
: _parent(p)
|
||||
, _folder(path)
|
||||
{
|
||||
this->startWatching();
|
||||
}
|
||||
|
||||
FolderWatcherPrivate::~FolderWatcherPrivate()
|
||||
{
|
||||
FSEventStreamStop(_stream);
|
||||
FSEventStreamInvalidate(_stream);
|
||||
FSEventStreamRelease(_stream);
|
||||
}
|
||||
|
||||
static void callback(
|
||||
ConstFSEventStreamRef streamRef,
|
||||
void *clientCallBackInfo,
|
||||
size_t numEvents,
|
||||
void *eventPathsVoid,
|
||||
const FSEventStreamEventFlags eventFlags[],
|
||||
const FSEventStreamEventId eventIds[])
|
||||
{
|
||||
Q_UNUSED(streamRef)
|
||||
Q_UNUSED(eventFlags)
|
||||
Q_UNUSED(eventIds)
|
||||
|
||||
const FSEventStreamEventFlags c_interestingFlags =
|
||||
kFSEventStreamEventFlagItemCreated // for new folder/file
|
||||
| kFSEventStreamEventFlagItemRemoved // for rm
|
||||
| kFSEventStreamEventFlagItemInodeMetaMod // for mtime change
|
||||
| kFSEventStreamEventFlagItemRenamed // also coming for moves to trash in finder
|
||||
| kFSEventStreamEventFlagItemModified; // for content change
|
||||
// We ignore other flags, e.g. for owner change, xattr change, Finder label change etc
|
||||
|
||||
QSet<QString> paths;
|
||||
CFArrayRef eventPaths = static_cast<CFArrayRef>(eventPathsVoid);
|
||||
|
||||
for (CFIndex i = 0; i < static_cast<CFIndex>(numEvents); ++i) {
|
||||
auto cfPath = reinterpret_cast<CFStringRef>(CFArrayGetValueAtIndex(eventPaths, i));
|
||||
const auto qPath = QString::fromCFString(cfPath).normalized(QString::NormalizationForm_C);
|
||||
|
||||
if (!(eventFlags[i] & c_interestingFlags)) {
|
||||
qCDebug(lcFolderWatcher) << u"Ignoring non-content changes for" << qPath;
|
||||
continue;
|
||||
}
|
||||
|
||||
paths.insert(qPath);
|
||||
}
|
||||
|
||||
if (!paths.isEmpty()) {
|
||||
reinterpret_cast<FolderWatcherPrivate *>(clientCallBackInfo)->doNotifyParent(std::move(paths));
|
||||
}
|
||||
}
|
||||
|
||||
void FolderWatcherPrivate::startWatching()
|
||||
{
|
||||
qCDebug(lcFolderWatcher) << u"FolderWatcherPrivate::startWatching()" << _folder;
|
||||
|
||||
CFStringRef cfFolder = _folder.toCFString();
|
||||
QScopeGuard freeFolder([cfFolder]() { CFRelease(cfFolder); });
|
||||
|
||||
CFArrayRef pathsToWatch = CFStringCreateArrayBySeparatingStrings(nullptr, cfFolder, CFSTR(":"));
|
||||
QScopeGuard freePaths([pathsToWatch]() { CFRelease(pathsToWatch); });
|
||||
|
||||
FSEventStreamContext ctx = { 0, this, nullptr, nullptr, nullptr };
|
||||
|
||||
// TODO: Add kFSEventStreamCreateFlagFileEvents ?
|
||||
|
||||
_stream = FSEventStreamCreate(nullptr,
|
||||
&callback,
|
||||
&ctx,
|
||||
pathsToWatch,
|
||||
kFSEventStreamEventIdSinceNow,
|
||||
0, // latency
|
||||
kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagIgnoreSelf);
|
||||
|
||||
FSEventStreamScheduleWithRunLoop(_stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
|
||||
FSEventStreamStart(_stream);
|
||||
}
|
||||
|
||||
void FolderWatcherPrivate::doNotifyParent(QSet<QString> &&paths)
|
||||
{
|
||||
_parent->addChanges(std::move(paths));
|
||||
}
|
||||
|
||||
|
||||
} // ns mirall
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) by Markus Goetz <markus@woboq.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef MIRALL_FOLDERWATCHER_MAC_H
|
||||
#define MIRALL_FOLDERWATCHER_MAC_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <CoreServices/CoreServices.h>
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class FolderWatcher;
|
||||
|
||||
/**
|
||||
* @brief Mac OS X API implementation of FolderWatcher
|
||||
* @ingroup gui
|
||||
*/
|
||||
class FolderWatcherPrivate
|
||||
{
|
||||
public:
|
||||
FolderWatcherPrivate(FolderWatcher *p, const QString &path);
|
||||
~FolderWatcherPrivate();
|
||||
|
||||
void startWatching();
|
||||
void doNotifyParent(QSet<QString> &&);
|
||||
|
||||
/// On OSX the watcher is ready when the ctor finished.
|
||||
constexpr bool isReady() const { return true; }
|
||||
|
||||
private:
|
||||
FolderWatcher *_parent;
|
||||
|
||||
QString _folder;
|
||||
|
||||
FSEventStreamRef _stream;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <QDir>
|
||||
#include <QScopeGuard>
|
||||
#include <QThread>
|
||||
|
||||
#include "common/asserts.h"
|
||||
#include "common/utility.h"
|
||||
#include "common/utility_win.h"
|
||||
#include "filesystem.h"
|
||||
#include "folderwatcher.h"
|
||||
#include "folderwatcher_win.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <tchar.h>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
WatcherThread::WatchChanges WatcherThread::watchChanges(size_t fileNotifyBufferSize)
|
||||
{
|
||||
_directory = Utility::Handle::createHandle(FileSystem::toFilesystemPath(_longPath), {.accessMode = FILE_LIST_DIRECTORY, .async = true});
|
||||
|
||||
if (!_directory) {
|
||||
qCWarning(lcFolderWatcher) << u"Failed to create handle for" << _path << u", error:" << _directory.errorMessage();
|
||||
return WatchChanges::Error;
|
||||
}
|
||||
|
||||
QScopeGuard todoBeforeReturn([this]() {
|
||||
CancelIo(_directory);
|
||||
closeHandle();
|
||||
});
|
||||
|
||||
OVERLAPPED overlapped = {};
|
||||
overlapped.hEvent = _resultEvent;
|
||||
|
||||
// QVarLengthArray ensures the stack-buffer is aligned like double and qint64.
|
||||
QVarLengthArray<char, 4096 * 10> fileNotifyBuffer;
|
||||
fileNotifyBuffer.resize(fileNotifyBufferSize);
|
||||
|
||||
while (true) {
|
||||
ResetEvent(_resultEvent);
|
||||
|
||||
FILE_NOTIFY_INFORMATION *pFileNotifyBuffer =
|
||||
reinterpret_cast<FILE_NOTIFY_INFORMATION *>(fileNotifyBuffer.data());
|
||||
DWORD dwBytesReturned = 0;
|
||||
if (!ReadDirectoryChangesW(_directory, pFileNotifyBuffer,
|
||||
static_cast<DWORD>(fileNotifyBufferSize), true,
|
||||
FILE_NOTIFY_CHANGE_FILE_NAME
|
||||
| FILE_NOTIFY_CHANGE_DIR_NAME
|
||||
| FILE_NOTIFY_CHANGE_LAST_WRITE
|
||||
| FILE_NOTIFY_CHANGE_ATTRIBUTES, // attributes are for vfs pin state changes
|
||||
&dwBytesReturned, &overlapped, nullptr)) {
|
||||
const DWORD errorCode = GetLastError();
|
||||
if (errorCode == ERROR_NOTIFY_ENUM_DIR) {
|
||||
qCDebug(lcFolderWatcher) << u"The buffer for changes overflowed! Triggering a generic change and resizing";
|
||||
Q_EMIT changed({_path});
|
||||
return WatchChanges::NeedBiggerBuffer;
|
||||
} else {
|
||||
qCWarning(lcFolderWatcher) << u"ReadDirectoryChangesW error" << Utility::formatWinError(errorCode);
|
||||
return WatchChanges::Error;
|
||||
}
|
||||
}
|
||||
|
||||
_parent->_ready = true;
|
||||
|
||||
HANDLE handles[] = { _resultEvent, _stopEvent };
|
||||
DWORD result = WaitForMultipleObjects(
|
||||
2, handles,
|
||||
false, // awake once one of them arrives
|
||||
INFINITE);
|
||||
const auto error = GetLastError();
|
||||
if (result == 1) {
|
||||
qCDebug(lcFolderWatcher) << u"Received stop event, aborting folder watcher thread";
|
||||
return WatchChanges::Done;
|
||||
}
|
||||
if (result != 0) {
|
||||
qCWarning(lcFolderWatcher) << u"WaitForMultipleObjects failed" << result << Utility::formatWinError(error);
|
||||
return WatchChanges::Error;
|
||||
}
|
||||
|
||||
bool ok = GetOverlappedResult(_directory, &overlapped, &dwBytesReturned, false);
|
||||
if (!ok) {
|
||||
const DWORD errorCode = GetLastError();
|
||||
if (errorCode == ERROR_NOTIFY_ENUM_DIR) {
|
||||
qCDebug(lcFolderWatcher) << u"The buffer for changes overflowed! Triggering a generic change and resizing";
|
||||
Q_EMIT lostChanges();
|
||||
Q_EMIT changed({_path});
|
||||
return WatchChanges::NeedBiggerBuffer;
|
||||
} else {
|
||||
qCWarning(lcFolderWatcher) << u"GetOverlappedResult error" << Utility::formatWinError(errorCode);
|
||||
return WatchChanges::Error;
|
||||
}
|
||||
}
|
||||
|
||||
processEntries(pFileNotifyBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
void WatcherThread::processEntries(FILE_NOTIFY_INFORMATION *curEntry)
|
||||
{
|
||||
if (!curEntry) {
|
||||
return;
|
||||
}
|
||||
QSet<QString> paths;
|
||||
const size_t fileNameBufferSize = 4096;
|
||||
wchar_t fileNameBuffer[fileNameBufferSize];
|
||||
do {
|
||||
const auto action = static_cast<FolderWatcherPrivate::ChangeAction>(curEntry->Action);
|
||||
QString longfile = _longPath + QString::fromWCharArray(curEntry->FileName, curEntry->FileNameLength / sizeof(wchar_t));
|
||||
|
||||
// Unless the file was removed or renamed, get its full long name
|
||||
// TODO: We could still try expanding the path in the tricky cases...
|
||||
if (action != FolderWatcherPrivate::ChangeAction::ACTION_REMOVED && action != FolderWatcherPrivate::ChangeAction::ACTION_RENAMED_OLD_NAME) {
|
||||
const int longNameSize = GetLongPathNameW(reinterpret_cast<const wchar_t*>(longfile.utf16()), fileNameBuffer, fileNameBufferSize);
|
||||
const auto error = GetLastError();
|
||||
if (longNameSize > 0) {
|
||||
longfile = QString::fromWCharArray(fileNameBuffer, longNameSize);
|
||||
} else {
|
||||
if (error == ERROR_FILE_NOT_FOUND) {
|
||||
qCInfo(lcFolderWatcher) << u"Ignoring change in" << longfile << u"the file no longer exists, probably a temporary file." << action;
|
||||
continue;
|
||||
} else {
|
||||
qCWarning(lcFolderWatcher) << u"Error converting file name" << longfile << u"to full length, keeping original name." << action
|
||||
<< Utility::formatWinError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
longfile = QDir::cleanPath(longfile);
|
||||
// Skip modifications of folders: One of these is triggered for changes
|
||||
// and new files in a folder, probably because of the folder's mtime
|
||||
// changing. We don't need them.
|
||||
if (action == FolderWatcherPrivate::ChangeAction::ACTION_MODIFIED && QFileInfo(longfile).isDir()) {
|
||||
continue;
|
||||
}
|
||||
paths.insert(longfile);
|
||||
} while (curEntry->NextEntryOffset != 0
|
||||
// FILE_NOTIFY_INFORMATION has no fixed size and the offset is in bytes therefor we first need to cast to char
|
||||
&& (curEntry = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(reinterpret_cast<char *>(curEntry) + curEntry->NextEntryOffset)));
|
||||
if (!paths.isEmpty()) {
|
||||
Q_EMIT changed(paths);
|
||||
}
|
||||
}
|
||||
|
||||
void WatcherThread::closeHandle()
|
||||
{
|
||||
if (_directory) {
|
||||
_directory.close();
|
||||
}
|
||||
}
|
||||
|
||||
void WatcherThread::run()
|
||||
{
|
||||
_resultEvent = CreateEvent(nullptr, true, false, nullptr);
|
||||
_stopEvent = CreateEvent(nullptr, true, false, nullptr);
|
||||
|
||||
// If this buffer fills up before we've extracted its data we will lose
|
||||
// change information. Therefore start big.
|
||||
size_t bufferSize = 4096 * 10;
|
||||
const size_t maxBuffer = 64 * 1024;
|
||||
|
||||
while (true) {
|
||||
switch (watchChanges(bufferSize)) {
|
||||
case WatchChanges::NeedBiggerBuffer:
|
||||
bufferSize = qMin(bufferSize * 2, maxBuffer);
|
||||
continue;
|
||||
case WatchChanges::Done:
|
||||
return;
|
||||
case WatchChanges::Error:
|
||||
// Other errors shouldn't actually happen,
|
||||
// so sleep a bit to avoid running into the same error case in a
|
||||
// tight loop.
|
||||
sleep(2);
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WatcherThread::WatcherThread(FolderWatcherPrivate *parent, const QString &path)
|
||||
: QThread()
|
||||
, _parent(parent)
|
||||
, _path(path + (path.endsWith(QLatin1Char('/')) ? QString() : QStringLiteral("/")))
|
||||
, _longPath(FileSystem::longWinPath(_path))
|
||||
, _directory(nullptr)
|
||||
, _resultEvent(nullptr)
|
||||
, _stopEvent(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
WatcherThread::~WatcherThread()
|
||||
{
|
||||
closeHandle();
|
||||
}
|
||||
|
||||
void WatcherThread::stop()
|
||||
{
|
||||
SetEvent(_stopEvent);
|
||||
}
|
||||
|
||||
FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path)
|
||||
: _parent(p)
|
||||
{
|
||||
_thread.reset(new WatcherThread(this, path));
|
||||
// we are using connects instead of directly emitting on p as we need to cross thread borders, the signal must receive a copy
|
||||
connect(_thread.get(), &WatcherThread::changed, _parent, [this](auto paths) { _parent->addChanges(std::move(paths)); }, Qt::QueuedConnection);
|
||||
connect(_thread.get(), &WatcherThread::lostChanges, _parent, &FolderWatcher::lostChanges, Qt::QueuedConnection);
|
||||
_thread->start();
|
||||
}
|
||||
|
||||
FolderWatcherPrivate::~FolderWatcherPrivate()
|
||||
{
|
||||
_thread->stop();
|
||||
_thread->wait();
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef MIRALL_FOLDERWATCHER_WIN_H
|
||||
#define MIRALL_FOLDERWATCHER_WIN_H
|
||||
|
||||
#include "libsync/common/utility_win.h"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class FolderWatcher;
|
||||
class FolderWatcherPrivate;
|
||||
|
||||
/**
|
||||
* @brief The WatcherThread class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class WatcherThread : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
WatcherThread(FolderWatcherPrivate *parent, const QString &path);
|
||||
~WatcherThread() override;
|
||||
|
||||
void stop();
|
||||
|
||||
protected:
|
||||
enum class WatchChanges {
|
||||
Done,
|
||||
NeedBiggerBuffer,
|
||||
Error,
|
||||
};
|
||||
|
||||
void run() override;
|
||||
WatchChanges watchChanges(size_t fileNotifyBufferSize);
|
||||
void processEntries(FILE_NOTIFY_INFORMATION *curEntry);
|
||||
void closeHandle();
|
||||
|
||||
Q_SIGNALS:
|
||||
void changed(QSet<QString> path);
|
||||
void lostChanges();
|
||||
|
||||
private:
|
||||
FolderWatcherPrivate *_parent;
|
||||
const QString _path;
|
||||
const QString _longPath;
|
||||
Utility::Handle _directory;
|
||||
HANDLE _resultEvent;
|
||||
HANDLE _stopEvent;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Windows implementation of FolderWatcher
|
||||
* @ingroup gui
|
||||
*/
|
||||
class FolderWatcherPrivate : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class ChangeAction : uint8_t {
|
||||
ACTION_ADDED = FILE_ACTION_ADDED,
|
||||
ACTION_REMOVED = FILE_ACTION_REMOVED,
|
||||
ACTION_MODIFIED = FILE_ACTION_MODIFIED,
|
||||
ACTION_RENAMED_OLD_NAME = FILE_ACTION_RENAMED_OLD_NAME,
|
||||
ACTION_RENAMED_NEW_NAME = FILE_ACTION_RENAMED_NEW_NAME
|
||||
};
|
||||
Q_ENUM(ChangeAction)
|
||||
FolderWatcherPrivate(FolderWatcher *p, const QString &path);
|
||||
~FolderWatcherPrivate() override;
|
||||
|
||||
/// Set to non-zero once the WatcherThread is capturing events.
|
||||
bool isReady() const
|
||||
{
|
||||
return _ready;
|
||||
}
|
||||
|
||||
private:
|
||||
FolderWatcher *_parent;
|
||||
QScopedPointer<WatcherThread> _thread;
|
||||
bool _ready = false;
|
||||
friend class WatcherThread;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // MIRALL_FOLDERWATCHER_WIN_H
|
||||
@@ -0,0 +1,12 @@
|
||||
target_sources(QSferaGui PRIVATE
|
||||
folderwizard.cpp
|
||||
folderwizardlocalpath.cpp
|
||||
folderwizardsourcepage.ui
|
||||
|
||||
folderwizardtargetpage.ui
|
||||
|
||||
folderwizardselectivesync.cpp
|
||||
|
||||
spacespage.cpp
|
||||
spacespage.ui
|
||||
)
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "folderwizard.h"
|
||||
#include "folderwizard_p.h"
|
||||
|
||||
#include "folderwizardlocalpath.h"
|
||||
#include "folderwizardselectivesync.h"
|
||||
|
||||
#include "spacespage.h"
|
||||
|
||||
#include "account.h"
|
||||
#include "gui/application.h"
|
||||
#include "gui/settingsdialog.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include "gui/accountstate.h"
|
||||
#include "gui/folderman.h"
|
||||
|
||||
#include "libsync/graphapi/space.h"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QMessageBox>
|
||||
#include <QUrl>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcFolderWizard, "gui.folderwizard", QtInfoMsg)
|
||||
|
||||
QString FolderWizardPrivate::formatWarnings(const QStringList &warnings, bool isError)
|
||||
{
|
||||
QString ret;
|
||||
if (warnings.count() == 1) {
|
||||
ret = isError ? QCoreApplication::translate("FolderWizard", "<b>Error:</b> %1").arg(warnings.first()) : QCoreApplication::translate("FolderWizard", "<b>Warning:</b> %1").arg(warnings.first());
|
||||
} else if (warnings.count() > 1) {
|
||||
QStringList w2;
|
||||
for (const auto &warning : warnings) {
|
||||
w2.append(QStringLiteral("<li>%1</li>").arg(warning));
|
||||
}
|
||||
ret = isError ? QCoreApplication::translate("FolderWizard", "<b>Error:</b><ul>%1</ul>").arg(w2.join(QString()))
|
||||
: QCoreApplication::translate("FolderWizard", "<b>Warning:</b><ul>%1</ul>").arg(w2.join(QString()));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString FolderWizardPrivate::defaultSyncRoot() const
|
||||
{
|
||||
if (!_account->account()->hasDefaultSyncRoot()) {
|
||||
return FolderMan::suggestSyncFolder(FolderMan::NewFolderType::SpacesSyncRoot, _account->account()->uuid());
|
||||
} else {
|
||||
return _account->account()->defaultSyncRoot();
|
||||
}
|
||||
}
|
||||
|
||||
FolderWizardPrivate::FolderWizardPrivate(FolderWizard *q, const AccountStatePtr &account)
|
||||
: q_ptr(q)
|
||||
, _account(account)
|
||||
, _spacesPage(new SpacesPage(account->account(), q))
|
||||
{
|
||||
if (!_account->account()->hasDefaultSyncRoot()) {
|
||||
_folderWizardSourcePage = new FolderWizardLocalPath(this);
|
||||
q->setPage(FolderWizard::Page_Source, _folderWizardSourcePage);
|
||||
}
|
||||
|
||||
q->setPage(FolderWizard::Page_Space, _spacesPage);
|
||||
|
||||
if (VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::Mode::Off) {
|
||||
_folderWizardSelectiveSyncPage = new FolderWizardSelectiveSync(this);
|
||||
q->setPage(FolderWizard::Page_SelectiveSync, _folderWizardSelectiveSyncPage);
|
||||
}
|
||||
}
|
||||
|
||||
QString FolderWizardPrivate::localPath() const
|
||||
{
|
||||
return FolderMan::findGoodPathForNewSyncFolder(
|
||||
defaultSyncRoot(), _spacesPage->currentSpace()->displayName(), FolderMan::NewFolderType::SpacesFolder, _account->account()->uuid());
|
||||
}
|
||||
|
||||
uint32_t FolderWizardPrivate::priority() const
|
||||
{
|
||||
return _spacesPage->currentSpace()->priority();
|
||||
}
|
||||
|
||||
QUrl FolderWizardPrivate::davUrl() const
|
||||
{
|
||||
auto url = _spacesPage->currentSpace()->webdavUrl();
|
||||
if (!url.path().endsWith(QLatin1Char('/'))) {
|
||||
url.setPath(url.path() + QLatin1Char('/'));
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
QString FolderWizardPrivate::spaceId() const
|
||||
{
|
||||
return _spacesPage->currentSpace()->id();
|
||||
}
|
||||
|
||||
QString FolderWizardPrivate::displayName() const
|
||||
{
|
||||
return _spacesPage->currentSpace()->displayName();
|
||||
}
|
||||
|
||||
const AccountStatePtr &FolderWizardPrivate::accountState()
|
||||
{
|
||||
return _account;
|
||||
}
|
||||
|
||||
bool FolderWizardPrivate::useVirtualFiles() const
|
||||
{
|
||||
return VfsPluginManager::instance().bestAvailableVfsMode() != Vfs::Mode::Off;
|
||||
}
|
||||
|
||||
FolderWizard::FolderWizard(const AccountStatePtr &account, QWidget *parent)
|
||||
: QWizard(parent)
|
||||
, d_ptr(new FolderWizardPrivate(this, account))
|
||||
{
|
||||
setWindowTitle(tr("Add Space"));
|
||||
setOptions(QWizard::CancelButtonOnLeft);
|
||||
setButtonText(QWizard::FinishButton, tr("Add Space"));
|
||||
setWizardStyle(QWizard::ModernStyle);
|
||||
}
|
||||
|
||||
FolderWizard::~FolderWizard()
|
||||
{
|
||||
}
|
||||
|
||||
FolderMan::SyncConnectionDescription FolderWizard::result()
|
||||
{
|
||||
Q_D(FolderWizard);
|
||||
if (d->_folderWizardSourcePage) {
|
||||
d->_account->account()->setDefaultSyncRoot(d->_folderWizardSourcePage->localPath());
|
||||
}
|
||||
const QString localPath = d->localPath();
|
||||
// the local path must be a child of defaultSyncRoot
|
||||
Q_ASSERT(FileSystem::isChildPathOf(localPath, d->defaultSyncRoot()));
|
||||
|
||||
return {
|
||||
d->davUrl(), //
|
||||
d->spaceId(), //
|
||||
localPath, //
|
||||
d->displayName(), //
|
||||
d->useVirtualFiles(), //
|
||||
d->priority(), //
|
||||
d->_folderWizardSelectiveSyncPage ? d->_folderWizardSelectiveSyncPage->selectiveSyncBlackList() : QSet<QString>{} //
|
||||
};
|
||||
}
|
||||
|
||||
} // end namespace
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QUrl>
|
||||
#include <QWizard>
|
||||
|
||||
#include "accountfwd.h"
|
||||
#include "gui/folderman.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
class Ui_FolderWizardTargetPage;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class FolderWizardPrivate;
|
||||
|
||||
/**
|
||||
* @brief The FolderWizard class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class FolderWizard : public QWizard
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum PageType { Page_Source, Page_Space, Page_SelectiveSync };
|
||||
Q_ENUM(PageType)
|
||||
|
||||
explicit FolderWizard(const AccountStatePtr &account, QWidget *parent = nullptr);
|
||||
~FolderWizard() override;
|
||||
|
||||
FolderMan::SyncConnectionDescription result();
|
||||
|
||||
Q_DECLARE_PRIVATE(FolderWizard)
|
||||
|
||||
private:
|
||||
QScopedPointer<FolderWizardPrivate> d_ptr;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "folderwizard.h"
|
||||
|
||||
#include "libsync/accountfwd.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QLoggingCategory>
|
||||
#include <QStringList>
|
||||
|
||||
namespace OCC {
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcFolderWizard);
|
||||
|
||||
class FolderWizardPrivate
|
||||
{
|
||||
public:
|
||||
FolderWizardPrivate(FolderWizard *q, const AccountStatePtr &account);
|
||||
static QString formatWarnings(const QStringList &warnings, bool isError = false);
|
||||
|
||||
QString localPath() const;
|
||||
|
||||
QString remotePath() const;
|
||||
|
||||
uint32_t priority() const;
|
||||
|
||||
QString defaultSyncRoot() const;
|
||||
|
||||
QUrl davUrl() const;
|
||||
QString spaceId() const;
|
||||
bool useVirtualFiles() const;
|
||||
QString displayName() const;
|
||||
|
||||
const AccountStatePtr &accountState();
|
||||
|
||||
private:
|
||||
Q_DECLARE_PUBLIC(FolderWizard)
|
||||
FolderWizard *q_ptr;
|
||||
|
||||
AccountStatePtr _account;
|
||||
class SpacesPage *_spacesPage;
|
||||
class FolderWizardLocalPath *_folderWizardSourcePage = nullptr;
|
||||
class FolderWizardRemotePath *_folderWizardTargetPage = nullptr;
|
||||
class FolderWizardSelectiveSync *_folderWizardSelectiveSyncPage = nullptr;
|
||||
};
|
||||
|
||||
|
||||
class FolderWizardPage : public QWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FolderWizardPage(FolderWizardPrivate *parent)
|
||||
: QWizardPage(nullptr)
|
||||
, _parent(parent)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
inline FolderWizardPrivate *folderWizardPrivate() const
|
||||
{
|
||||
return _parent;
|
||||
}
|
||||
|
||||
private:
|
||||
FolderWizardPrivate *_parent;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
#include "folderwizardlocalpath.h"
|
||||
#include "ui_folderwizardsourcepage.h"
|
||||
|
||||
#include "folderwizard.h"
|
||||
#include "folderwizard_p.h"
|
||||
|
||||
#include "gui/folderman.h"
|
||||
#include "libsync/account.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QStandardPaths>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
FolderWizardLocalPath::FolderWizardLocalPath(FolderWizardPrivate *parent)
|
||||
: FolderWizardPage(parent)
|
||||
, _ui(new Ui_FolderWizardSourcePage)
|
||||
{
|
||||
_ui->setupUi(this);
|
||||
connect(_ui->localFolderChooseBtn, &QAbstractButton::clicked, this, &FolderWizardLocalPath::slotChooseLocalFolder);
|
||||
connect(_ui->localFolderLineEdit, &QLineEdit::textChanged, this, &FolderWizardLocalPath::completeChanged);
|
||||
|
||||
_ui->warnLabel->setTextFormat(Qt::RichText);
|
||||
_ui->warnLabel->hide();
|
||||
}
|
||||
|
||||
FolderWizardLocalPath::~FolderWizardLocalPath()
|
||||
{
|
||||
delete _ui;
|
||||
}
|
||||
|
||||
void FolderWizardLocalPath::initializePage()
|
||||
{
|
||||
_ui->warnLabel->hide();
|
||||
_ui->localFolderLineEdit->setText(QDir::toNativeSeparators(folderWizardPrivate()->defaultSyncRoot()));
|
||||
}
|
||||
|
||||
QString FolderWizardLocalPath::localPath() const
|
||||
{
|
||||
return QDir::fromNativeSeparators(_ui->localFolderLineEdit->text());
|
||||
}
|
||||
|
||||
bool FolderWizardLocalPath::isComplete() const
|
||||
{
|
||||
auto accountUuid = folderWizardPrivate()->accountState()->account()->uuid();
|
||||
QString errorStr = FolderMan::instance()->checkPathValidityForNewFolder(localPath(), FolderMan::NewFolderType::SpacesSyncRoot, accountUuid);
|
||||
if (errorStr.isEmpty()) {
|
||||
if (auto result = VfsPluginManager::instance().prepare(localPath(), accountUuid, VfsPluginManager::instance().bestAvailableVfsMode()); !result) {
|
||||
errorStr = result.error();
|
||||
}
|
||||
}
|
||||
|
||||
bool isOk = errorStr.isEmpty();
|
||||
QStringList warnStrings;
|
||||
if (!isOk) {
|
||||
warnStrings << errorStr;
|
||||
}
|
||||
|
||||
_ui->warnLabel->setWordWrap(true);
|
||||
if (isOk) {
|
||||
_ui->warnLabel->hide();
|
||||
_ui->warnLabel->clear();
|
||||
} else {
|
||||
_ui->warnLabel->show();
|
||||
QString warnings = FolderWizardPrivate::formatWarnings(warnStrings);
|
||||
_ui->warnLabel->setText(warnings);
|
||||
}
|
||||
return isOk;
|
||||
}
|
||||
|
||||
void FolderWizardLocalPath::slotChooseLocalFolder()
|
||||
{
|
||||
QString sf = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
|
||||
QDir d(sf);
|
||||
|
||||
// open the first entry of the home dir. Otherwise the dir picker comes
|
||||
// up with the closed home dir icon, stupid Qt default...
|
||||
QStringList dirs = d.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks,
|
||||
QDir::DirsFirst | QDir::Name);
|
||||
|
||||
if (dirs.count() > 0)
|
||||
sf += QLatin1Char('/') + dirs.at(0); // Take the first dir in home dir.
|
||||
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Select the Spaces root folder"), sf);
|
||||
if (!dir.isEmpty()) {
|
||||
// set the last directory component name as alias
|
||||
_ui->localFolderLineEdit->setText(QDir::toNativeSeparators(dir));
|
||||
}
|
||||
Q_EMIT completeChanged();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "gui/folder.h"
|
||||
#include "gui/folderwizard/folderwizard_p.h"
|
||||
|
||||
class Ui_FolderWizardSourcePage;
|
||||
namespace OCC {
|
||||
|
||||
|
||||
/**
|
||||
* @brief Page to ask for the local source folder
|
||||
* @ingroup gui
|
||||
*/
|
||||
class FolderWizardLocalPath : public FolderWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FolderWizardLocalPath(FolderWizardPrivate *parent);
|
||||
~FolderWizardLocalPath() override;
|
||||
|
||||
bool isComplete() const override;
|
||||
void initializePage() override;
|
||||
|
||||
QString localPath() const;
|
||||
protected Q_SLOTS:
|
||||
void slotChooseLocalFolder();
|
||||
|
||||
private:
|
||||
Ui_FolderWizardSourcePage *_ui;
|
||||
QMap<QString, Folder *> _folderMap;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
#include "folderwizardselectivesync.h"
|
||||
|
||||
#include "folderwizard.h"
|
||||
#include "folderwizard_p.h"
|
||||
|
||||
#include "gui/application.h"
|
||||
#include "gui/selectivesyncwidget.h"
|
||||
|
||||
#include "libsync/theme.h"
|
||||
|
||||
#include "gui/settingsdialog.h"
|
||||
#include "libsync/vfs/vfs.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
FolderWizardSelectiveSync::FolderWizardSelectiveSync(FolderWizardPrivate *parent)
|
||||
: FolderWizardPage(parent)
|
||||
{
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
_selectiveSync = new SelectiveSyncWidget(folderWizardPrivate()->accountState()->account(), this);
|
||||
layout->addWidget(_selectiveSync);
|
||||
}
|
||||
|
||||
FolderWizardSelectiveSync::~FolderWizardSelectiveSync()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
void FolderWizardSelectiveSync::initializePage()
|
||||
{
|
||||
const auto *wizardPrivate = dynamic_cast<FolderWizard *>(wizard())->d_func();
|
||||
_selectiveSync->setDavUrl(wizardPrivate->davUrl());
|
||||
_selectiveSync->setFolderInfo(wizardPrivate->displayName());
|
||||
QWizardPage::initializePage();
|
||||
}
|
||||
|
||||
bool FolderWizardSelectiveSync::validatePage()
|
||||
{
|
||||
_selectiveSyncBlackList = _selectiveSync->createBlackList();
|
||||
return true;
|
||||
}
|
||||
|
||||
const QSet<QString> &FolderWizardSelectiveSync::selectiveSyncBlackList() const
|
||||
{
|
||||
return _selectiveSyncBlackList;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/folderwizard/folderwizard_p.h"
|
||||
#include "libsync/accountfwd.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class SelectiveSyncWidget;
|
||||
|
||||
/**
|
||||
* @brief The FolderWizardSelectiveSync class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class FolderWizardSelectiveSync : public FolderWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FolderWizardSelectiveSync(FolderWizardPrivate *parent);
|
||||
~FolderWizardSelectiveSync() override;
|
||||
|
||||
bool validatePage() override;
|
||||
|
||||
void initializePage() override;
|
||||
|
||||
const QSet<QString> &selectiveSyncBlackList() const;
|
||||
|
||||
private:
|
||||
SelectiveSyncWidget *_selectiveSync;
|
||||
QSet<QString> _selectiveSyncBlackList;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FolderWizardSourcePage</class>
|
||||
<widget class="QWidget" name="FolderWizardSourcePage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>423</width>
|
||||
<height>174</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Select a local folder to synchronize your Spaces to:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>localFolderLineEdit</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="localFolderLineEdit">
|
||||
<property name="toolTip">
|
||||
<string>Enter the path to the Spaces root folder. This folder will contain all your synchronized Spaces.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="localFolderChooseBtn">
|
||||
<property name="toolTip">
|
||||
<string>Click to select the Spaces root folder.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Choose...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="warnLabel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::TextFormat::RichText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>349</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,169 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FolderWizardTargetPage</class>
|
||||
<widget class="QWidget" name="FolderWizardTargetPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>520</width>
|
||||
<height>350</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Select a remote destination folder</string>
|
||||
</property>
|
||||
<property name="flat">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="folderTreeWidget">
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="headerHidden">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Folders</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="addFolderButton">
|
||||
<property name="text">
|
||||
<string>Create Folder</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="refreshButton">
|
||||
<property name="text">
|
||||
<string>Refresh</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<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>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="folderEntry"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="warningFrame">
|
||||
<property name="palette">
|
||||
<palette>
|
||||
<active/>
|
||||
<inactive/>
|
||||
<disabled/>
|
||||
</palette>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="warningIcon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::AutoText</enum>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="../../resources/client.qrc">:/client/resources/light/warning.svg</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="warningLabel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../resources/client.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#include "spacespage.h"
|
||||
#include "ui_spacespage.h"
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
SpacesPage::SpacesPage(AccountPtr acc, QWidget *parent)
|
||||
: QWizardPage(parent)
|
||||
, ui(new Ui::SpacesPage)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
ui->widget->setAccount(acc);
|
||||
|
||||
connect(ui->widget, &Spaces::SpacesBrowser::currentSpaceChanged, this, &QWizardPage::completeChanged);
|
||||
}
|
||||
|
||||
SpacesPage::~SpacesPage()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
bool OCC::SpacesPage::isComplete() const
|
||||
{
|
||||
return ui->widget->currentSpace();
|
||||
}
|
||||
|
||||
GraphApi::Space *OCC::SpacesPage::currentSpace() const
|
||||
{
|
||||
return ui->widget->currentSpace();
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "gui/spaces/spacesmodel.h"
|
||||
|
||||
#include "accountfwd.h"
|
||||
|
||||
#include <QWizardPage>
|
||||
|
||||
|
||||
namespace Ui {
|
||||
class SpacesPage;
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
class SpacesPage : public QWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SpacesPage(AccountPtr acc, QWidget *parent = nullptr);
|
||||
~SpacesPage();
|
||||
|
||||
bool isComplete() const override;
|
||||
|
||||
|
||||
GraphApi::Space *currentSpace() const;
|
||||
|
||||
private:
|
||||
::Ui::SpacesPage *ui;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SpacesPage</class>
|
||||
<widget class="QWizardPage" name="SpacesPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>362</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Choose a Space to sync</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="OCC::Spaces::SpacesBrowser" name="widget" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::StrongFocus</enum>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Spaces list</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>OCC::Spaces::SpacesBrowser</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>gui/spaces/spacesbrowser.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#include "fonticonmessagebox.h"
|
||||
|
||||
#include <QStyle>
|
||||
|
||||
OCC::FontIconMessageBox::FontIconMessageBox(
|
||||
Resources::FontIcon icon, const QString &title, const QString &text, StandardButtons buttons, QWidget *parent, Qt::WindowFlags flags)
|
||||
: QMessageBox(NoIcon, title, text, buttons, parent, flags)
|
||||
{
|
||||
const int iconSize = style()->pixelMetric(QStyle::PM_MessageBoxIconSize, nullptr, this);
|
||||
setIconPixmap(icon.pixmap({iconSize, iconSize}, devicePixelRatio()));
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include "resources/fonticon.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
namespace OCC {
|
||||
class QSFERA_GUI_EXPORT FontIconMessageBox : public QMessageBox
|
||||
{
|
||||
public:
|
||||
FontIconMessageBox(Resources::FontIcon icon, const QString &title, const QString &text, StandardButtons buttons = NoButton, QWidget *parent = nullptr,
|
||||
Qt::WindowFlags flags = Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "generalsettings.h"
|
||||
#include "ui_generalsettings.h"
|
||||
|
||||
#include "common/restartmanager.h"
|
||||
#include "gui/application.h"
|
||||
#include "gui/folderman.h"
|
||||
#include "gui/ignorelisteditor.h"
|
||||
#include "gui/logbrowser.h"
|
||||
#include "gui/settingsdialog.h"
|
||||
#include "gui/translations.h"
|
||||
#include "libsync/configfile.h"
|
||||
#include "libsync/theme.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QOperatingSystemVersion>
|
||||
#include <QScopedValueRollback>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcGeneralSettings, "gui.generalsettings", QtInfoMsg)
|
||||
namespace OCC {
|
||||
|
||||
GeneralSettings::GeneralSettings(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, _ui(new Ui::GeneralSettings)
|
||||
, _currentlyLoading(false)
|
||||
{
|
||||
_ui->setupUi(this);
|
||||
|
||||
reloadConfig();
|
||||
loadMiscSettings();
|
||||
|
||||
// misc
|
||||
connect(_ui->crashreporterCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings);
|
||||
|
||||
connect(_ui->languageDropdown, QOverload<int>::of(&QComboBox::activated), this, [this]() {
|
||||
// first, store selected language in config file
|
||||
saveMiscSettings();
|
||||
|
||||
// warn user that a language change requires a restart to take effect
|
||||
QMessageBox::warning(this, tr("Warning"), tr("Language changes require a restart of this application to take effect."), QMessageBox::Ok);
|
||||
});
|
||||
|
||||
/* handle the hidden file checkbox */
|
||||
|
||||
/* the ignoreHiddenFiles flag is a folder specific setting, but for now, it is
|
||||
* handled globally. Save it to every folder that is defined.
|
||||
*/
|
||||
connect(_ui->syncHiddenFilesCheckBox, &QCheckBox::toggled, this, [](bool checked) { FolderMan::instance()->setIgnoreHiddenFiles(!checked); });
|
||||
|
||||
_ui->crashreporterCheckBox->setVisible(Theme::instance()->withCrashReporter());
|
||||
|
||||
_ui->moveToTrashCheckBox->setVisible(Theme::instance()->enableMoveToTrash());
|
||||
connect(_ui->moveToTrashCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
ConfigFile().setMoveToTrash(checked);
|
||||
Q_EMIT syncOptionsChanged();
|
||||
});
|
||||
|
||||
connect(_ui->ignoredFilesButton, &QAbstractButton::clicked, this, &GeneralSettings::slotIgnoreFilesEditor);
|
||||
connect(_ui->logSettingsButton, &QPushButton::clicked, this, [] {
|
||||
// only access occApp after things are set up
|
||||
auto logBrowser = new LogBrowser(ocApp()->settingsDialog());
|
||||
logBrowser->setAttribute(Qt::WA_DeleteOnClose);
|
||||
ocApp()->showSettings();
|
||||
logBrowser->open();
|
||||
});
|
||||
|
||||
connect(_ui->about_pushButton, &QPushButton::clicked, ocApp(), &Application::showAbout);
|
||||
}
|
||||
|
||||
GeneralSettings::~GeneralSettings()
|
||||
{
|
||||
delete _ui;
|
||||
}
|
||||
|
||||
void GeneralSettings::loadMiscSettings()
|
||||
{
|
||||
QScopedValueRollback<bool> scope(_currentlyLoading, true);
|
||||
ConfigFile cfgFile;
|
||||
_ui->crashreporterCheckBox->setChecked(cfgFile.crashReporter());
|
||||
|
||||
// the dropdown has to be populated before we can can pick an entry below based on the stored setting
|
||||
loadLanguageNamesIntoDropdown();
|
||||
|
||||
const auto &locale = cfgFile.uiLanguage();
|
||||
const auto index = _ui->languageDropdown->findData(locale);
|
||||
_ui->languageDropdown->setCurrentIndex(index < 0 ? 0 : index);
|
||||
}
|
||||
|
||||
void GeneralSettings::showEvent(QShowEvent *)
|
||||
{
|
||||
reloadConfig();
|
||||
}
|
||||
|
||||
void GeneralSettings::saveMiscSettings()
|
||||
{
|
||||
if (_currentlyLoading)
|
||||
return;
|
||||
ConfigFile cfgFile;
|
||||
cfgFile.setCrashReporter(_ui->crashreporterCheckBox->isChecked());
|
||||
|
||||
// the first entry, identified by index 0, means "use default", which is a special case handled below
|
||||
const QString pickedLocale = _ui->languageDropdown->currentData().toString();
|
||||
cfgFile.setUiLanguage(pickedLocale);
|
||||
}
|
||||
|
||||
void GeneralSettings::slotToggleLaunchOnStartup(bool enable)
|
||||
{
|
||||
Theme *theme = Theme::instance();
|
||||
Utility::setLaunchOnStartup(theme->appName(), theme->appNameGUI(), enable);
|
||||
}
|
||||
|
||||
void GeneralSettings::slotIgnoreFilesEditor()
|
||||
{
|
||||
if (_ignoreEditor.isNull()) {
|
||||
_ignoreEditor = new IgnoreListEditor(ocApp()->settingsDialog());
|
||||
_ignoreEditor->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
ocApp()->showSettings();
|
||||
_ignoreEditor->open();
|
||||
}
|
||||
}
|
||||
|
||||
void GeneralSettings::reloadConfig()
|
||||
{
|
||||
_ui->syncHiddenFilesCheckBox->setChecked(!FolderMan::instance()->ignoreHiddenFiles());
|
||||
_ui->moveToTrashCheckBox->setChecked(ConfigFile().moveToTrash());
|
||||
if (Utility::isWindows() && Utility::isInstalledByStore()) {
|
||||
_ui->autostartCheckBox->setVisible(false);
|
||||
} else {
|
||||
if (Utility::hasSystemLaunchOnStartup(Theme::instance()->appName())) {
|
||||
_ui->autostartCheckBox->setChecked(true);
|
||||
_ui->autostartCheckBox->setDisabled(true);
|
||||
_ui->autostartCheckBox->setToolTip(tr("You cannot disable autostart because system-wide autostart is enabled."));
|
||||
} else {
|
||||
const bool hasAutoStart = Utility::hasLaunchOnStartup(Theme::instance()->appName());
|
||||
// make sure the binary location is correctly set
|
||||
slotToggleLaunchOnStartup(hasAutoStart);
|
||||
_ui->autostartCheckBox->setChecked(hasAutoStart);
|
||||
connect(_ui->autostartCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotToggleLaunchOnStartup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GeneralSettings::loadLanguageNamesIntoDropdown()
|
||||
{
|
||||
// allow method to be called more than once
|
||||
_ui->languageDropdown->clear();
|
||||
|
||||
// if no option has been chosen explicitly by the user, the first entry shall be used
|
||||
_ui->languageDropdown->addItem(tr("(use default)"));
|
||||
|
||||
// initialize map of locales to language names
|
||||
const auto availableLocales = []() {
|
||||
auto rv = Translations::listAvailableTranslations().values();
|
||||
rv.sort(Qt::CaseInsensitive);
|
||||
return rv;
|
||||
}();
|
||||
|
||||
for (const auto &availableLocale : availableLocales) {
|
||||
auto nativeLanguageName = QLocale(availableLocale).nativeLanguageName();
|
||||
|
||||
// fallback if there's a locale whose name Qt doesn't know
|
||||
// this indicates a broken filename
|
||||
if (nativeLanguageName.isEmpty()) {
|
||||
qCDebug(lcGeneralSettings) << u"Warning: could not find native language name for locale" << availableLocale;
|
||||
nativeLanguageName = tr("unknown (%1)").arg(availableLocale);
|
||||
}
|
||||
|
||||
QString entryText = QStringLiteral("%1 (%2)").arg(nativeLanguageName, availableLocale);
|
||||
_ui->languageDropdown->addItem(entryText, availableLocale);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef MIRALL_GENERALSETTINGS_H
|
||||
#define MIRALL_GENERALSETTINGS_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QWidget>
|
||||
#include <QPointer>
|
||||
|
||||
namespace OCC {
|
||||
class IgnoreListEditor;
|
||||
class SyncLogDialog;
|
||||
|
||||
namespace Ui {
|
||||
class GeneralSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The GeneralSettings class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class GeneralSettings : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GeneralSettings(QWidget *parent = nullptr);
|
||||
~GeneralSettings() override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void syncOptionsChanged();
|
||||
|
||||
private Q_SLOTS:
|
||||
void saveMiscSettings();
|
||||
void slotToggleLaunchOnStartup(bool);
|
||||
void slotIgnoreFilesEditor();
|
||||
void loadMiscSettings();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
private:
|
||||
void reloadConfig();
|
||||
void loadLanguageNamesIntoDropdown();
|
||||
|
||||
Ui::GeneralSettings *_ui;
|
||||
QPointer<IgnoreListEditor> _ignoreEditor;
|
||||
bool _currentlyLoading;
|
||||
};
|
||||
|
||||
|
||||
} // namespace OCC
|
||||
#endif // MIRALL_GENERALSETTINGS_H
|
||||
@@ -0,0 +1,253 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>OCC::GeneralSettings</class>
|
||||
<widget class="QWidget" name="OCC::GeneralSettings">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>791</width>
|
||||
<height>686</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>765</width>
|
||||
<height>618</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="generalGroupBox">
|
||||
<property name="title">
|
||||
<string>General Settings</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="languageLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Language</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>languageDropdown</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="languageDropdown">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Language selector</string>
|
||||
</property>
|
||||
<property name="currentText">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="autostartCheckBox">
|
||||
<property name="text">
|
||||
<string>Start on Login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="advancedGroupBox">
|
||||
<property name="title">
|
||||
<string>Advanced</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="syncHiddenFilesCheckBox">
|
||||
<property name="text">
|
||||
<string>Sync hidden files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="crashreporterCheckBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Show crash reporter</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="moveToTrashCheckBox">
|
||||
<property name="text">
|
||||
<string>Move remotely deleted files to the local trash bin instead of deleting them</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QPushButton" name="ignoredFilesButton">
|
||||
<property name="text">
|
||||
<string>Edit Ignored Files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="logSettingsButton">
|
||||
<property name="text">
|
||||
<string>Log Settings</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="spacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>555</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Network</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="NetworkSettings" name="widget" native="true">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::StrongFocus</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="versionInfoWidget" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<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>
|
||||
<spacer name="spacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="about_pushButton">
|
||||
<property name="text">
|
||||
<string>About</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>NetworkSettings</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>networksettings.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>autostartCheckBox</tabstop>
|
||||
<tabstop>languageDropdown</tabstop>
|
||||
<tabstop>syncHiddenFilesCheckBox</tabstop>
|
||||
<tabstop>crashreporterCheckBox</tabstop>
|
||||
<tabstop>moveToTrashCheckBox</tabstop>
|
||||
<tabstop>ignoredFilesButton</tabstop>
|
||||
<tabstop>logSettingsButton</tabstop>
|
||||
<tabstop>widget</tabstop>
|
||||
<tabstop>scrollArea</tabstop>
|
||||
<tabstop>about_pushButton</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (C) by Christian Kamm <mail@ckamm.de>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "guiutility.h"
|
||||
#include "gui/application.h"
|
||||
#include "gui/settingsdialog.h"
|
||||
#include "libsync/filesystem.h"
|
||||
#include "libsync/theme.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMessageBox>
|
||||
#include <QQuickWidget>
|
||||
#include <QUrlQuery>
|
||||
|
||||
namespace OCC {
|
||||
Q_LOGGING_CATEGORY(lcGuiUtility, "gui.utility", QtInfoMsg)
|
||||
}
|
||||
|
||||
namespace {
|
||||
const QString dirTag()
|
||||
{
|
||||
return QStringLiteral("eu.qsfera.spaces.app");
|
||||
}
|
||||
|
||||
const QString uuidTag()
|
||||
{
|
||||
return QStringLiteral("eu.qsfera.spaces.account-uuid");
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
bool Utility::openBrowser(const QUrl &url, QWidget *errorWidgetParent)
|
||||
{
|
||||
if (!QDesktopServices::openUrl(url)) {
|
||||
if (errorWidgetParent) {
|
||||
QMessageBox::warning(
|
||||
errorWidgetParent,
|
||||
QCoreApplication::translate("utility", "Could not open browser"),
|
||||
QCoreApplication::translate("utility",
|
||||
"There was an error when launching the browser to go to "
|
||||
"URL %1. Maybe no default browser is configured?")
|
||||
.arg(url.toString()));
|
||||
}
|
||||
qCWarning(lcGuiUtility) << u"QDesktopServices::openUrl failed for" << url;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Utility::openEmailComposer(const QString &subject, const QString &body, QWidget *errorWidgetParent)
|
||||
{
|
||||
QUrl url(QStringLiteral("mailto:"));
|
||||
QUrlQuery query;
|
||||
query.setQueryItems({ { QLatin1String("subject"), subject },
|
||||
{ QLatin1String("body"), body } });
|
||||
url.setQuery(query);
|
||||
|
||||
if (!QDesktopServices::openUrl(url)) {
|
||||
if (errorWidgetParent) {
|
||||
QMessageBox::warning(
|
||||
errorWidgetParent,
|
||||
QCoreApplication::translate("utility", "Could not open email client"),
|
||||
QCoreApplication::translate("utility",
|
||||
"There was an error when launching the email client to "
|
||||
"create a new message. Maybe no default email client is "
|
||||
"configured?"));
|
||||
}
|
||||
qCWarning(lcGuiUtility) << u"QDesktopServices::openUrl failed for" << url;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString Utility::vfsPinActionText()
|
||||
{
|
||||
return QCoreApplication::translate("utility", "Make always available locally");
|
||||
}
|
||||
|
||||
QString Utility::vfsFreeSpaceActionText()
|
||||
{
|
||||
return QCoreApplication::translate("utility", "Free up local space");
|
||||
}
|
||||
|
||||
void Utility::markDirectoryAsSyncRoot(const QString &path, const QUuid &accountUuid)
|
||||
{
|
||||
Q_ASSERT(getDirectorySyncRootMarkings(path).first.isEmpty());
|
||||
Q_ASSERT(getDirectorySyncRootMarkings(path).second.isNull());
|
||||
|
||||
auto result1 = FileSystem::Tags::set(path, dirTag(), Theme::instance()->orgDomainName());
|
||||
if (!result1) {
|
||||
qCWarning(lcGuiUtility) << QStringLiteral("Failed to set tag on »%1«: %2").arg(path, result1.error())
|
||||
#ifdef Q_OS_WIN
|
||||
<< QStringLiteral("(filesystem %1)").arg(FileSystem::fileSystemForPath(path))
|
||||
#endif // Q_OS_WIN
|
||||
;
|
||||
return;
|
||||
}
|
||||
|
||||
auto result2 = FileSystem::Tags::set(path, uuidTag(), accountUuid.toString());
|
||||
if (!result2) {
|
||||
qCWarning(lcGuiUtility) << QStringLiteral("Failed to set tag on »%1«: %2").arg(path, result2.error())
|
||||
#ifdef Q_OS_WIN
|
||||
<< QStringLiteral("(filesystem %1)").arg(FileSystem::fileSystemForPath(path))
|
||||
#endif // Q_OS_WIN
|
||||
;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<QString, QUuid> Utility::getDirectorySyncRootMarkings(const QString &path)
|
||||
{
|
||||
auto existingDirTag = FileSystem::Tags::get(path, dirTag());
|
||||
auto existingUuidTag = FileSystem::Tags::get(path, uuidTag());
|
||||
|
||||
if (existingDirTag.has_value() && existingUuidTag.has_value()) {
|
||||
return {existingDirTag.value(), QUuid::fromString(existingUuidTag.value())};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void Utility::unmarkDirectoryAsSyncRoot(const QString &path)
|
||||
{
|
||||
if (QFileInfo::exists(path)) {
|
||||
if (!FileSystem::Tags::remove(path, dirTag())) {
|
||||
qCWarning(lcGuiUtility) << u"Failed to remove tag on" << path;
|
||||
Q_ASSERT(false);
|
||||
}
|
||||
if (!FileSystem::Tags::remove(path, uuidTag())) {
|
||||
qCWarning(lcGuiUtility) << u"Failed to remove uuid tag on" << path;
|
||||
Q_ASSERT(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) by Christian Kamm <mail@ckamm.de>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include "common/pinstate.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QWidget>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcGuiUtility)
|
||||
|
||||
namespace Utility {
|
||||
|
||||
/** Open a URL in the browser.
|
||||
*
|
||||
* If launching the browser fails, display a message.
|
||||
*/
|
||||
bool openBrowser(const QUrl &url, QWidget *errorWidgetParent);
|
||||
|
||||
/** Start composing a new email message.
|
||||
*
|
||||
* If launching the email program fails, display a message.
|
||||
*/
|
||||
bool openEmailComposer(const QString &subject, const QString &body,
|
||||
QWidget *errorWidgetParent);
|
||||
|
||||
/** Translated text for "making items always available locally" */
|
||||
QString vfsPinActionText();
|
||||
|
||||
/** Translated text for "free up local space" (and unpinning the item) */
|
||||
QString vfsFreeSpaceActionText();
|
||||
|
||||
void startShellIntegration();
|
||||
|
||||
QString socketApiSocketPath();
|
||||
|
||||
bool isInstalledByStore();
|
||||
|
||||
QSFERA_GUI_EXPORT void markDirectoryAsSyncRoot(const QString &path, const QUuid &accountUuid);
|
||||
std::pair<QString, QUuid> getDirectorySyncRootMarkings(const QString &path);
|
||||
void unmarkDirectoryAsSyncRoot(const QString &path);
|
||||
} // namespace Utility
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
* Copyright (C) by Erik Verbruggen <erik@verbruggen.consulting>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "application.h"
|
||||
#include "guiutility.h"
|
||||
|
||||
#include "libsync/theme.h"
|
||||
|
||||
#include <QProcess>
|
||||
|
||||
#import <Foundation/NSBundle.h>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
void Utility::startShellIntegration()
|
||||
{
|
||||
QString bundlePath = QUrl::fromNSURL([NSBundle mainBundle].bundleURL).path();
|
||||
|
||||
auto _system = [](const QString &cmd, const QStringList &args) {
|
||||
QProcess process;
|
||||
process.setProcessChannelMode(QProcess::MergedChannels);
|
||||
process.start(cmd, args);
|
||||
if (!process.waitForFinished()) {
|
||||
qCWarning(lcGuiUtility) << "Failed to load shell extension:" << cmd
|
||||
<< args.join(QLatin1Char(' ')) << process.errorString();
|
||||
} else {
|
||||
qCInfo(lcGuiUtility) << (process.exitCode() != 0 ? "Failed to load" : "Loaded")
|
||||
<< "shell extension:" << cmd << args.join(QLatin1Char(' '))
|
||||
<< process.readAll();
|
||||
}
|
||||
};
|
||||
|
||||
// Add it again. This was needed for Mojave to trigger a load.
|
||||
_system(QStringLiteral("pluginkit"), { QStringLiteral("-a"), QStringLiteral("%1Contents/PlugIns/FinderSyncExt.appex/").arg(bundlePath) });
|
||||
|
||||
// Tell Finder to use the Extension (checking it from System Preferences -> Extensions)
|
||||
_system(QStringLiteral("pluginkit"),
|
||||
{QStringLiteral("-e"), QStringLiteral("use"), QStringLiteral("-i"), Theme::instance()->orgDomainName() + QStringLiteral(".FinderSyncExt")});
|
||||
}
|
||||
|
||||
QString Utility::socketApiSocketPath()
|
||||
{
|
||||
// This must match the code signing Team setting of the extension
|
||||
// Example for developer builds (with ad-hoc signing identity): "" "eu.qsfera.desktop" ".socketApi"
|
||||
// Example for official signed packages: "9B5WD74GWJ." "eu.qsfera.desktop" ".socketApi"
|
||||
return QStringLiteral("%1%2.socketApi").arg(QStringLiteral(SOCKETAPI_TEAM_IDENTIFIER_PREFIX), Theme::instance()->orgDomainName());
|
||||
}
|
||||
|
||||
bool Utility::isInstalledByStore()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) by Erik Verbruggen <erik@verbruggen.consulting>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "guiutility.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
void Utility::startShellIntegration()
|
||||
{
|
||||
}
|
||||
|
||||
QString Utility::socketApiSocketPath()
|
||||
{
|
||||
return QStringLiteral("%1/QSfera/socket").arg(QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation));
|
||||
}
|
||||
|
||||
bool Utility::isInstalledByStore()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) by Erik Verbruggen <erik@verbruggen.consulting>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "application.h"
|
||||
#include "guiutility.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
// windows.h mus be included before appmodel.h
|
||||
#include <windows.h>
|
||||
|
||||
#include <appmodel.h>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
void Utility::startShellIntegration()
|
||||
{
|
||||
}
|
||||
|
||||
QString Utility::socketApiSocketPath()
|
||||
{
|
||||
return QStringLiteral(R"(\\.\pipe\QSfera-%1)").arg(qEnvironmentVariable("USERNAME"));
|
||||
}
|
||||
|
||||
bool Utility::isInstalledByStore()
|
||||
{
|
||||
uint32_t length = 0;
|
||||
return GetCurrentPackageFamilyName(&length, nullptr) != APPMODEL_ERROR_NO_PACKAGE;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "ignorelisteditor.h"
|
||||
#include "ui_ignorelisteditor.h"
|
||||
|
||||
#include "configfile.h"
|
||||
#include "gui/folderman.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QInputDialog>
|
||||
#include <QListWidgetItem>
|
||||
#include <QMessageBox>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
static const int patternCol = 0;
|
||||
static const int deletableCol = 1;
|
||||
static const int skippedLinesRole = Qt::UserRole;
|
||||
static const int isGlobalRole = Qt::UserRole + 1;
|
||||
|
||||
IgnoreListEditor::IgnoreListEditor(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, ui(new Ui::IgnoreListEditor)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
ConfigFile cfgFile;
|
||||
readOnlyTooltip = tr("This entry is provided by the system at %1 "
|
||||
"and cannot be modified in this view.")
|
||||
.arg(QDir::toNativeSeparators(cfgFile.excludeFile(ConfigFile::SystemScope)));
|
||||
|
||||
addPattern(QStringLiteral(".csync_journal.db*"), /*deletable=*/false, /*readonly=*/true, /*global=*/true);
|
||||
addPattern(QStringLiteral("._sync_*.db*"), /*deletable=*/false, /*readonly=*/true, /*global=*/true);
|
||||
addPattern(QStringLiteral(".sync_*.db*"), /*deletable=*/false, /*readonly=*/true, /*global=*/true);
|
||||
readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), /*global=*/true);
|
||||
readIgnoreFile(cfgFile.excludeFile(ConfigFile::UserScope), /*global=*/false);
|
||||
|
||||
connect(this, &QDialog::accepted, this, &IgnoreListEditor::slotUpdateLocalIgnoreList);
|
||||
ui->removePushButton->setEnabled(false);
|
||||
connect(ui->tableWidget, &QTableWidget::itemSelectionChanged, this, &IgnoreListEditor::slotItemSelectionChanged);
|
||||
connect(ui->removePushButton, &QAbstractButton::clicked, this, &IgnoreListEditor::slotRemoveCurrentItem);
|
||||
connect(ui->addPushButton, &QAbstractButton::clicked, this, &IgnoreListEditor::slotAddPattern);
|
||||
|
||||
ui->tableWidget->resizeColumnsToContents();
|
||||
ui->tableWidget->horizontalHeader()->setSectionResizeMode(patternCol, QHeaderView::Stretch);
|
||||
ui->tableWidget->verticalHeader()->setVisible(false);
|
||||
}
|
||||
|
||||
IgnoreListEditor::~IgnoreListEditor()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void IgnoreListEditor::slotItemSelectionChanged()
|
||||
{
|
||||
QTableWidgetItem *item = ui->tableWidget->currentItem();
|
||||
if (!item) {
|
||||
ui->removePushButton->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
bool enable = item->flags() & Qt::ItemIsEnabled;
|
||||
ui->removePushButton->setEnabled(enable);
|
||||
}
|
||||
|
||||
void IgnoreListEditor::slotRemoveCurrentItem()
|
||||
{
|
||||
ui->tableWidget->removeRow(ui->tableWidget->currentRow());
|
||||
}
|
||||
|
||||
void IgnoreListEditor::slotUpdateLocalIgnoreList()
|
||||
{
|
||||
ConfigFile cfgFile;
|
||||
QString ignoreFile = cfgFile.excludeFile(ConfigFile::UserScope);
|
||||
QFile ignores(ignoreFile);
|
||||
if (ignores.open(QIODevice::WriteOnly)) {
|
||||
for (int row = 0; row < ui->tableWidget->rowCount(); ++row) {
|
||||
QTableWidgetItem *patternItem = ui->tableWidget->item(row, patternCol);
|
||||
QTableWidgetItem *deletableItem = ui->tableWidget->item(row, deletableCol);
|
||||
|
||||
if (patternItem->data(isGlobalRole).toBool())
|
||||
continue;
|
||||
|
||||
const auto &skippedLines = patternItem->data(skippedLinesRole).toStringList();
|
||||
for (const auto &line : skippedLines)
|
||||
ignores.write(line.toUtf8() + '\n');
|
||||
|
||||
QByteArray prepend;
|
||||
if (deletableItem->checkState() == Qt::Checked) {
|
||||
prepend = "]";
|
||||
} else if (patternItem->text().startsWith(QLatin1Char('#'))) {
|
||||
prepend = "\\";
|
||||
}
|
||||
ignores.write(prepend + patternItem->text().toUtf8() + '\n');
|
||||
}
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Could not open file"),
|
||||
tr("Cannot write changes to »%1«.").arg(ignoreFile));
|
||||
}
|
||||
ignores.close(); //close the file before reloading stuff.
|
||||
|
||||
FolderMan *folderMan = FolderMan::instance();
|
||||
|
||||
// We need to force a remote discovery after a change of the ignore list.
|
||||
// Otherwise we would not download the files/directories that are no longer
|
||||
// ignored (because the remote etag did not change) (issue #3172)
|
||||
for (auto *folder : folderMan->folders()) {
|
||||
if (folder->isReady()) {
|
||||
folder->journalDb()->forceRemoteDiscoveryNextSync();
|
||||
folder->reloadExcludes();
|
||||
folder->slotNextSyncFullLocalDiscovery();
|
||||
folderMan->scheduler()->enqueueFolder(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IgnoreListEditor::slotAddPattern()
|
||||
{
|
||||
bool okClicked;
|
||||
QString pattern = QInputDialog::getText(this, tr("Add Ignore Pattern"),
|
||||
tr("Add a new ignore pattern:"),
|
||||
QLineEdit::Normal, QString(), &okClicked);
|
||||
|
||||
if (!okClicked || pattern.isEmpty())
|
||||
return;
|
||||
|
||||
addPattern(pattern, /*deletable=*/false, /*readonly=*/false, /*global=*/false);
|
||||
ui->tableWidget->scrollToBottom();
|
||||
}
|
||||
|
||||
void IgnoreListEditor::readIgnoreFile(const QString &file, bool global)
|
||||
{
|
||||
QFile ignores(file);
|
||||
if (!ignores.open(QIODevice::ReadOnly))
|
||||
return;
|
||||
|
||||
QStringList skippedLines;
|
||||
bool readonly = global; // global ignores default to read-only
|
||||
|
||||
while (!ignores.atEnd()) {
|
||||
QString line = QString::fromUtf8(ignores.readLine());
|
||||
line.chop(1);
|
||||
|
||||
// Collect empty lines and comments, we want to preserve them
|
||||
if (line.isEmpty() || line.startsWith(QLatin1String("#"))) {
|
||||
skippedLines.append(line);
|
||||
// A directive that prohibits editing in the ui
|
||||
if (line == QLatin1String("#!readonly"))
|
||||
readonly = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool deletable = false;
|
||||
if (line.startsWith(QLatin1Char(']'))) {
|
||||
deletable = true;
|
||||
line = line.mid(1);
|
||||
}
|
||||
|
||||
// Add and reset
|
||||
addPattern(line, deletable, readonly, global, skippedLines);
|
||||
skippedLines.clear();
|
||||
readonly = global;
|
||||
}
|
||||
}
|
||||
|
||||
int IgnoreListEditor::addPattern(const QString &pattern, bool deletable, bool readOnly, bool global, const QStringList &skippedLines)
|
||||
{
|
||||
int newRow = ui->tableWidget->rowCount();
|
||||
ui->tableWidget->setRowCount(newRow + 1);
|
||||
|
||||
QTableWidgetItem *patternItem = new QTableWidgetItem;
|
||||
patternItem->setText(pattern);
|
||||
patternItem->setData(skippedLinesRole, skippedLines);
|
||||
patternItem->setData(isGlobalRole, global);
|
||||
ui->tableWidget->setItem(newRow, patternCol, patternItem);
|
||||
|
||||
QTableWidgetItem *deletableItem = new QTableWidgetItem;
|
||||
deletableItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
|
||||
deletableItem->setCheckState(deletable ? Qt::Checked : Qt::Unchecked);
|
||||
ui->tableWidget->setItem(newRow, deletableCol, deletableItem);
|
||||
|
||||
if (readOnly) {
|
||||
patternItem->setFlags(patternItem->flags() ^ Qt::ItemIsEnabled);
|
||||
patternItem->setToolTip(readOnlyTooltip);
|
||||
deletableItem->setFlags(deletableItem->flags() ^ Qt::ItemIsEnabled);
|
||||
}
|
||||
|
||||
return newRow;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef IGNORELISTEDITOR_H
|
||||
#define IGNORELISTEDITOR_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QListWidgetItem;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace Ui {
|
||||
class IgnoreListEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The IgnoreListEditor class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class IgnoreListEditor : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit IgnoreListEditor(QWidget *parent = nullptr);
|
||||
~IgnoreListEditor() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotItemSelectionChanged();
|
||||
void slotRemoveCurrentItem();
|
||||
void slotUpdateLocalIgnoreList();
|
||||
void slotAddPattern();
|
||||
|
||||
private:
|
||||
void readIgnoreFile(const QString &file, bool global);
|
||||
int addPattern(const QString &pattern, bool deletable, bool readOnly, bool global,
|
||||
const QStringList &skippedLines = QStringList());
|
||||
QString readOnlyTooltip;
|
||||
Ui::IgnoreListEditor *ui;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#endif // IGNORELISTEDITOR_H
|
||||
@@ -0,0 +1,164 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>OCC::IgnoreListEditor</class>
|
||||
<widget class="QDialog" name="OCC::IgnoreListEditor">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>555</width>
|
||||
<height>609</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Ignored Files Editor</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Files Ignored by Patterns</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="tableWidget">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Pattern</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Allow Deletion</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="addPushButton">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removePushButton">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>213</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="descriptionLabel">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Files or folders matching a pattern will not be synchronized. Changes take effect the next time folders are synchronized.
|
||||
|
||||
Items where deletion is allowed will be deleted if they prevent a directory from being removed. This is useful for meta data.</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>OCC::IgnoreListEditor</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>348</x>
|
||||
<y>333</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>361</x>
|
||||
<y>355</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>OCC::IgnoreListEditor</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>210</x>
|
||||
<y>333</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>385</x>
|
||||
<y>297</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <QtGui>
|
||||
#include <QtWidgets>
|
||||
|
||||
#include "account.h"
|
||||
#include "accountmanager.h"
|
||||
#include "commonstrings.h"
|
||||
#include "folder.h"
|
||||
#include "folderman.h"
|
||||
#include "gui/models/expandingheaderview.h"
|
||||
#include "issueswidget.h"
|
||||
#include "libsync/configfile.h"
|
||||
#include "models/models.h"
|
||||
#include "protocolwidget.h"
|
||||
#include "syncengine.h"
|
||||
#include "syncfileitem.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include "ui_issueswidget.h"
|
||||
|
||||
namespace {
|
||||
bool persistsUntilLocalDiscovery(const OCC::ProtocolItem &data)
|
||||
{
|
||||
return data.status() == OCC::SyncFileItem::Conflict
|
||||
|| (data.status() == OCC::SyncFileItem::FileIgnored && data.direction() == OCC::SyncFileItem::Up)
|
||||
|| data.status() == OCC::SyncFileItem::Excluded;
|
||||
}
|
||||
|
||||
}
|
||||
namespace OCC {
|
||||
|
||||
class SyncFileItemStatusSetSortFilterProxyModel : public Models::SignalledQSortFilterProxyModel
|
||||
{
|
||||
public:
|
||||
using StatusSet = std::array<bool, SyncFileItem::StatusCount>;
|
||||
|
||||
explicit SyncFileItemStatusSetSortFilterProxyModel(QObject *parent = nullptr)
|
||||
: Models::SignalledQSortFilterProxyModel(parent)
|
||||
{
|
||||
restoreFilter();
|
||||
}
|
||||
|
||||
~SyncFileItemStatusSetSortFilterProxyModel() override
|
||||
{
|
||||
}
|
||||
|
||||
StatusSet filter() const
|
||||
{
|
||||
return _filter;
|
||||
}
|
||||
|
||||
void setFilter(const StatusSet &newFilter, bool save = true)
|
||||
{
|
||||
if (_filter != newFilter) {
|
||||
_filter = newFilter;
|
||||
if (save) {
|
||||
saveFilter();
|
||||
}
|
||||
invalidateFilter();
|
||||
Q_EMIT filterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void resetFilter()
|
||||
{
|
||||
setFilter(defaultFilter());
|
||||
}
|
||||
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
|
||||
{
|
||||
QModelIndex idx = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent);
|
||||
|
||||
bool ok = false;
|
||||
int sourceData = sourceModel()->data(idx, filterRole()).toInt(&ok);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _filter[static_cast<SyncFileItem::Status>(sourceData)];
|
||||
}
|
||||
|
||||
int filterCount()
|
||||
{
|
||||
StatusSet defaultSet = defaultFilter();
|
||||
OC_ASSERT(defaultSet.size() == _filter.size());
|
||||
|
||||
int count = 0;
|
||||
// The number of filters is the number of items in the current filter that differ from the default set.
|
||||
for (size_t i = 0, ei = defaultSet.size(); i != ei; ++i) {
|
||||
// All errors are shown as a single filter, and they are all turned on or off together.
|
||||
// So to show them as 1 filter, ignore the first three errors...
|
||||
switch (i) {
|
||||
case SyncFileItem::Status::FatalError:
|
||||
Q_FALLTHROUGH();
|
||||
case SyncFileItem::Status::NormalError:
|
||||
Q_FALLTHROUGH();
|
||||
case SyncFileItem::Status::SoftError:
|
||||
break;
|
||||
|
||||
// ... but *do* count the last one ...
|
||||
case SyncFileItem::Status::DetailError:
|
||||
Q_FALLTHROUGH();
|
||||
default:
|
||||
// ... just like the other status items:
|
||||
if (defaultSet[i] != _filter[i]) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private:
|
||||
static StatusSet defaultFilter()
|
||||
{
|
||||
StatusSet defaultSet;
|
||||
defaultSet.fill(true);
|
||||
defaultSet[SyncFileItem::NoStatus] = false;
|
||||
defaultSet[SyncFileItem::Success] = false;
|
||||
return defaultSet;
|
||||
}
|
||||
|
||||
void saveFilter()
|
||||
{
|
||||
QStringList checked;
|
||||
for (std::underlying_type_t<SyncFileItem::Status> s = SyncFileItem::NoStatus; s < SyncFileItem::StatusCount; ++s) {
|
||||
if (_filter[s]) {
|
||||
checked.append(Utility::enumToString(static_cast<SyncFileItem::Status>(s)));
|
||||
}
|
||||
}
|
||||
ConfigFile().setIssuesWidgetFilter(checked);
|
||||
}
|
||||
|
||||
void restoreFilter()
|
||||
{
|
||||
StatusSet filter = {};
|
||||
bool filterNeedsReset = true; // If there is no filter, the `true` value will cause a reset.
|
||||
std::optional<QStringList> checked = ConfigFile().issuesWidgetFilter();
|
||||
|
||||
if (checked.has_value()) {
|
||||
// There is a filter, but it can be empty (user unchecked all checkboxes), and in that case we do not want to reset the filter.
|
||||
filterNeedsReset = false;
|
||||
|
||||
for (const QString &s : checked.value()) {
|
||||
const auto status = Utility::stringToEnum<SyncFileItem::Status>(s);
|
||||
if (status == static_cast<SyncFileItem::Status>(-1)) {
|
||||
// The string value is not a valid enum value, so stop processing, and queue a reset.
|
||||
filterNeedsReset = true;
|
||||
break;
|
||||
} else {
|
||||
filter[status] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filterNeedsReset) {
|
||||
// If there was no filter in the config file, or if one of the values is invalid, reset the filter.
|
||||
resetFilter();
|
||||
} else {
|
||||
// There is a valid filter, so apply it. Also don't save it, because we just loaded it successfully
|
||||
setFilter(filter, false);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
StatusSet _filter = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* If more issues are reported than this they will not show up
|
||||
* to avoid performance issues around sorting this many issues.
|
||||
*/
|
||||
|
||||
IssuesWidget::IssuesWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, _ui(new Ui::IssuesWidget)
|
||||
{
|
||||
_ui->setupUi(this);
|
||||
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
|
||||
this, &IssuesWidget::slotProgressInfo);
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
|
||||
this, &IssuesWidget::slotItemCompleted);
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
|
||||
this, [this](Folder *folder, const QString &message, ErrorCategory) {
|
||||
auto item = SyncFileItemPtr::create();
|
||||
item->_status = SyncFileItem::NormalError;
|
||||
item->_errorString = message;
|
||||
_model->addProtocolItem(ProtocolItem { folder, item });
|
||||
});
|
||||
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::excluded, this, [this](Folder *f, const QString &file) {
|
||||
auto item = SyncFileItemPtr::create(file);
|
||||
item->_status = SyncFileItem::FilenameReserved;
|
||||
item->_errorString = tr("The file »%1« was ignored as its name is reserved by %2").arg(file, Theme::instance()->appNameGUI());
|
||||
_model->addProtocolItem(ProtocolItem { f, item });
|
||||
});
|
||||
|
||||
_model = new ProtocolItemModel(20000, true, this);
|
||||
_sortModel = new Models::SignalledQSortFilterProxyModel(this);
|
||||
connect(_sortModel, &Models::SignalledQSortFilterProxyModel::filterChanged, this, &IssuesWidget::filterDidChange);
|
||||
_sortModel->setSourceModel(_model);
|
||||
_statusSortModel = new SyncFileItemStatusSetSortFilterProxyModel(this); // Note: this will restore a previously set filter, if there was one.
|
||||
connect(_statusSortModel, &Models::SignalledQSortFilterProxyModel::filterChanged, this, &IssuesWidget::filterDidChange);
|
||||
_statusSortModel->setSourceModel(_sortModel);
|
||||
_statusSortModel->setSortRole(Qt::DisplayRole); // Sorting should be done based on the text in the column cells, but...
|
||||
_statusSortModel->setFilterRole(Models::UnderlyingDataRole); // ... filtering should be done on the underlying enum value.
|
||||
_statusSortModel->setFilterKeyColumn(static_cast<int>(ProtocolItemModel::ProtocolItemRole::Status));
|
||||
_ui->_tableView->setModel(_statusSortModel);
|
||||
|
||||
auto header = new ExpandingHeaderView(QStringLiteral("ActivityErrorListHeaderV2"), _ui->_tableView);
|
||||
_ui->_tableView->setHorizontalHeader(header);
|
||||
header->setSectionResizeMode(QHeaderView::Interactive);
|
||||
header->setExpandingColumn(static_cast<int>(ProtocolItemModel::ProtocolItemRole::Action));
|
||||
header->setSortIndicator(static_cast<int>(ProtocolItemModel::ProtocolItemRole::Time), Qt::DescendingOrder);
|
||||
|
||||
connect(_ui->_tableView, &QTreeView::customContextMenuRequested, this, &IssuesWidget::slotItemContextMenu);
|
||||
_ui->_tableView->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(header, &QHeaderView::customContextMenuRequested, this, [this, header]() {
|
||||
auto menu = showFilterMenu(header);
|
||||
menu->addAction(tr("Reset column sizes"), header, [header] { header->resizeColumns(true); });
|
||||
});
|
||||
|
||||
connect(_ui->_filterButton, &QAbstractButton::clicked, this, [this] {
|
||||
showFilterMenu(_ui->_filterButton);
|
||||
});
|
||||
filterDidChange(); // Set the appropriate label.
|
||||
|
||||
_ui->_tooManyIssuesWarning->hide();
|
||||
connect(_model, &ProtocolItemModel::rowsInserted, this, [this] {
|
||||
Q_EMIT issueCountUpdated(_model->rowCount());
|
||||
_ui->_tooManyIssuesWarning->setVisible(_model->isModelFull());
|
||||
});
|
||||
connect(_model, &ProtocolItemModel::modelReset, this, [this] {
|
||||
Q_EMIT issueCountUpdated(_model->rowCount());
|
||||
_ui->_tooManyIssuesWarning->setVisible(_model->isModelFull());
|
||||
});
|
||||
|
||||
connect(FolderMan::instance(), &FolderMan::folderRemoved, this, [this](Folder *f) {
|
||||
_model->remove_if([f](const ProtocolItem &item) {
|
||||
return item.folder() == f;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
IssuesWidget::~IssuesWidget()
|
||||
{
|
||||
delete _ui;
|
||||
}
|
||||
|
||||
QMenu *IssuesWidget::showFilterMenu(QWidget *parent)
|
||||
{
|
||||
auto menu = new QMenu(parent);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
menu->setAccessibleName(tr("Filter menu"));
|
||||
|
||||
auto accountFilterReset = Models::addFilterMenuItems(menu, AccountManager::instance()->accountNames(), _sortModel, static_cast<int>(ProtocolItemModel::ProtocolItemRole::Account), tr("Account"), Qt::DisplayRole);
|
||||
menu->addSeparator();
|
||||
auto statusFilterReset = addStatusFilter(menu);
|
||||
menu->addSeparator();
|
||||
addResetFiltersAction(menu, { accountFilterReset, statusFilterReset });
|
||||
|
||||
QTimer::singleShot(0, menu, [menu] {
|
||||
// FIXME: when activated by the keyboard, this position can be anywhere.
|
||||
menu->popup(QCursor::pos());
|
||||
// accassebility
|
||||
menu->setFocus();
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
void IssuesWidget::addResetFiltersAction(QMenu *menu, const QList<std::function<void()>> &resetFunctions)
|
||||
{
|
||||
menu->addAction(QCoreApplication::translate("OCC::Models", "Reset Filters"), menu, [resetFunctions]() {
|
||||
for (const auto &reset : resetFunctions) {
|
||||
reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void IssuesWidget::slotProgressInfo(Folder *folder, const ProgressInfo &progress)
|
||||
{
|
||||
if (progress.status() == ProgressInfo::Reconcile) {
|
||||
// Wipe all non-persistent entries - as well as the persistent ones
|
||||
// in cases where a local discovery was done.
|
||||
const auto &engine = folder->syncEngine();
|
||||
const auto style = engine.lastLocalDiscoveryStyle();
|
||||
_model->remove_if([&](const ProtocolItem &item) {
|
||||
if (item.folder() != folder) {
|
||||
return false;
|
||||
}
|
||||
if (item.direction() == SyncFileItem::None && item.status() == SyncFileItem::FilenameReserved) {
|
||||
// TODO: don't clear syncErrors and excludes for now.
|
||||
// make them either unique or remove them on the next sync?
|
||||
return false;
|
||||
}
|
||||
if (style == LocalDiscoveryStyle::FilesystemOnly) {
|
||||
return true;
|
||||
}
|
||||
if (!persistsUntilLocalDiscovery(item)) {
|
||||
return true;
|
||||
}
|
||||
// Definitely wipe the entry if the file no longer exists
|
||||
if (!QFileInfo::exists(folder->path() + item.path())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto path = QFileInfo(item.path()).dir().path();
|
||||
if (path == QLatin1Char('.'))
|
||||
path.clear();
|
||||
|
||||
return engine.shouldDiscoverLocally(path);
|
||||
});
|
||||
}
|
||||
if (progress.status() == ProgressInfo::Done) {
|
||||
// We keep track very well of pending conflicts.
|
||||
// Inform other components about them.
|
||||
QStringList conflicts;
|
||||
for (const auto &rawData : _model->rawData()) {
|
||||
if (rawData.folder() == folder && rawData.status() == SyncFileItem::Conflict) {
|
||||
conflicts.append(rawData.path());
|
||||
}
|
||||
}
|
||||
Q_EMIT ProgressDispatcher::instance() -> folderConflicts(folder, conflicts);
|
||||
}
|
||||
}
|
||||
|
||||
void IssuesWidget::slotItemCompleted(Folder *folder, const SyncFileItemPtr &item)
|
||||
{
|
||||
if (!item->showInIssuesTab())
|
||||
return;
|
||||
_model->addProtocolItem(ProtocolItem { folder, item });
|
||||
}
|
||||
|
||||
void IssuesWidget::filterDidChange()
|
||||
{
|
||||
// We have two filters here: the filter by status (which can have multiple items checked *off*...
|
||||
int filterCount = _statusSortModel->filterCount();
|
||||
// .. and the filter on the account name, which can only be 1 item checked:
|
||||
if (!_sortModel->filterRegularExpression().pattern().isEmpty()) {
|
||||
filterCount += 1;
|
||||
}
|
||||
|
||||
_ui->_filterButton->setText(filterCount > 0 ? CommonStrings::filterButtonText(filterCount) : tr("Filter"));
|
||||
}
|
||||
|
||||
void IssuesWidget::slotItemContextMenu(const QPoint &pos)
|
||||
{
|
||||
auto rows = _ui->_tableView->selectionModel()->selectedRows();
|
||||
for (int i = 0; i < rows.size(); ++i) {
|
||||
rows[i] = _statusSortModel->mapToSource(rows[i]);
|
||||
rows[i] = _sortModel->mapToSource(rows[i]);
|
||||
}
|
||||
ProtocolWidget::showContextMenu(this, _ui->_tableView, _sortModel, _model, rows, pos);
|
||||
}
|
||||
|
||||
std::function<void(void)> IssuesWidget::addStatusFilter(QMenu *menu)
|
||||
{
|
||||
menu->addAction(QCoreApplication::translate("OCC::Models", "Status Filter:"))->setEnabled(false);
|
||||
|
||||
// Use a QActionGroup to contain all status filter items, so we can find them back easily to reset.
|
||||
auto statusFilterGroup = new QActionGroup(menu);
|
||||
statusFilterGroup->setExclusive(false);
|
||||
|
||||
const auto initialFilter = _statusSortModel->filter();
|
||||
|
||||
{ // Add all errors under 1 action:
|
||||
auto action = menu->addAction(Utility::enumToDisplayName(SyncFileItem::NormalError), this, [this](bool checked) {
|
||||
auto currentFilter = _statusSortModel->filter();
|
||||
for (const auto &item : SyncFileItem::ErrorStatusItems) {
|
||||
currentFilter[item] = checked;
|
||||
}
|
||||
_statusSortModel->setFilter(currentFilter);
|
||||
});
|
||||
action->setCheckable(true);
|
||||
action->setChecked(initialFilter[SyncFileItem::ErrorStatusItems[0]]);
|
||||
statusFilterGroup->addAction(action);
|
||||
}
|
||||
menu->addSeparator();
|
||||
|
||||
// list of OtherDisplayableStatusItems with the localised name
|
||||
std::vector<std::pair<QString, SyncFileItem::Status>> otherStatusItems;
|
||||
otherStatusItems.reserve(SyncFileItem::OtherDisplayableStatusItems.size());
|
||||
for (const auto &item : SyncFileItem::OtherDisplayableStatusItems) {
|
||||
otherStatusItems.emplace_back(Utility::enumToDisplayName(item), item);
|
||||
}
|
||||
std::sort(otherStatusItems.begin(), otherStatusItems.end(), [](const auto &a, const auto &b) {
|
||||
return a.first < b.first;
|
||||
});
|
||||
for (const auto &item : otherStatusItems) {
|
||||
auto action = menu->addAction(item.first, menu, [this, item](bool checked) {
|
||||
auto currentFilter = _statusSortModel->filter();
|
||||
currentFilter[item.second] = checked;
|
||||
_statusSortModel->setFilter(currentFilter);
|
||||
});
|
||||
action->setCheckable(true);
|
||||
action->setChecked(initialFilter[item.second]);
|
||||
statusFilterGroup->addAction(action);
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
// Add action to reset all filters at once:
|
||||
return [statusFilterGroup, this]() {
|
||||
for (QAction *action : statusFilterGroup->actions()) {
|
||||
action->setChecked(true);
|
||||
}
|
||||
_statusSortModel->resetFilter();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef ISSUESWIDGET_H
|
||||
#define ISSUESWIDGET_H
|
||||
|
||||
#include "models/models.h"
|
||||
#include "models/protocolitemmodel.h"
|
||||
#include "progressdispatcher.h"
|
||||
|
||||
class QSortFilterProxyModel;
|
||||
|
||||
namespace OCC {
|
||||
class SyncResult;
|
||||
class SyncFileItemStatusSetSortFilterProxyModel;
|
||||
|
||||
namespace Ui {
|
||||
class IssuesWidget;
|
||||
}
|
||||
class Application;
|
||||
|
||||
/**
|
||||
* @brief The ProtocolWidget class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class IssuesWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit IssuesWidget(QWidget *parent = nullptr);
|
||||
~IssuesWidget() override;
|
||||
|
||||
public Q_SLOTS:
|
||||
void slotProgressInfo(Folder *folder, const ProgressInfo &progress);
|
||||
void slotItemCompleted(Folder *folder, const SyncFileItemPtr &item);
|
||||
void filterDidChange();
|
||||
|
||||
Q_SIGNALS:
|
||||
void issueCountUpdated(int);
|
||||
|
||||
private Q_SLOTS:
|
||||
QMenu *showFilterMenu(QWidget *parent);
|
||||
void slotItemContextMenu(const QPoint &pos);
|
||||
|
||||
private:
|
||||
static void addResetFiltersAction(QMenu *menu, const QList<std::function<void()>> &resetFunctions);
|
||||
std::function<void()> addStatusFilter(QMenu *menu);
|
||||
|
||||
ProtocolItemModel *_model;
|
||||
Models::SignalledQSortFilterProxyModel *_sortModel;
|
||||
SyncFileItemStatusSetSortFilterProxyModel *_statusSortModel;
|
||||
|
||||
Ui::IssuesWidget *_ui;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>OCC::IssuesWidget</class>
|
||||
<widget class="QWidget" name="OCC::IssuesWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>580</width>
|
||||
<height>578</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="_filterButton"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableView" name="_tableView">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Issues table</string>
|
||||
</property>
|
||||
<property name="tabKeyNavigation">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="_tooManyIssuesWarning">
|
||||
<property name="text">
|
||||
<string>There were too many issues. Not all will be visible here.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>_filterButton</tabstop>
|
||||
<tabstop>_tableView</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) by Christian Kamm <mail@ckamm.de>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "lockwatcher.h"
|
||||
#include "filesystem.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QTimer>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcLockWatcher, "gui.lockwatcher", QtInfoMsg)
|
||||
|
||||
namespace {
|
||||
const auto check_frequency = 20s;
|
||||
}
|
||||
|
||||
LockWatcher::LockWatcher(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(&_timer, &QTimer::timeout,
|
||||
this, &LockWatcher::checkFiles);
|
||||
_timer.start(check_frequency);
|
||||
}
|
||||
|
||||
void LockWatcher::addFile(const QString &path, FileSystem::LockMode mode)
|
||||
{
|
||||
qCInfo(lcLockWatcher) << u"Watching for lock of" << path << mode << u"being released";
|
||||
_watchedPaths.insert({ path, mode });
|
||||
}
|
||||
|
||||
void LockWatcher::setCheckInterval(std::chrono::milliseconds interval)
|
||||
{
|
||||
_timer.start(interval.count());
|
||||
}
|
||||
|
||||
bool LockWatcher::contains(const QString &path, OCC::FileSystem::LockMode mode) const
|
||||
{
|
||||
return _watchedPaths.find({ path, mode }) != _watchedPaths.cend();
|
||||
}
|
||||
|
||||
void LockWatcher::checkFiles()
|
||||
{
|
||||
// copy as Q_EMIT fileUnlocked might trigger a new insert
|
||||
const auto watchedPathsCopy = _watchedPaths;
|
||||
decltype(_watchedPaths) unlocked;
|
||||
for (const auto &p : watchedPathsCopy) {
|
||||
if (!FileSystem::isFileLocked(p.first, p.second)) {
|
||||
qCInfo(lcLockWatcher) << u"Lock of" << p.first << p.second << u"was released";
|
||||
Q_EMIT fileUnlocked(p.first, p.second);
|
||||
unlocked.insert(p);
|
||||
}
|
||||
}
|
||||
for (const auto &removed : std::as_const(unlocked)) {
|
||||
_watchedPaths.erase(removed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) by Christian Kamm <mail@ckamm.de>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include "filesystem.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
|
||||
#include <chrono>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* @brief Monitors files that are locked, signaling when they become unlocked
|
||||
*
|
||||
* Only relevant on Windows. Some high-profile applications like Microsoft
|
||||
* Word lock the document that is currently being edited. The synchronization
|
||||
* client will be unable to update them while they are locked.
|
||||
*
|
||||
* In this situation we do want to start a sync run as soon as the file
|
||||
* becomes available again. To do that, we need to regularly check whether
|
||||
* the file is still being locked.
|
||||
*
|
||||
* @ingroup gui
|
||||
*/
|
||||
|
||||
class QSFERA_GUI_EXPORT LockWatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LockWatcher(QObject *parent = nullptr);
|
||||
|
||||
/** Start watching a file.
|
||||
*
|
||||
* If the file is not locked later on, the fileUnlocked signal will be
|
||||
* emitted once.
|
||||
*/
|
||||
void addFile(const QString &path, OCC::FileSystem::LockMode mode);
|
||||
|
||||
/** Adjusts the default interval for checking whether the lock is still present */
|
||||
void setCheckInterval(std::chrono::milliseconds interval);
|
||||
|
||||
/** Whether the path is being watched for lock-changes */
|
||||
bool contains(const QString &path, OCC::FileSystem::LockMode mode) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
/** Emitted when one of the watched files is no longer
|
||||
* being locked. */
|
||||
void fileUnlocked(const QString &path, OCC::FileSystem::LockMode mode);
|
||||
|
||||
private Q_SLOTS:
|
||||
void checkFiles();
|
||||
|
||||
private:
|
||||
using LockKey = std::pair<QString, FileSystem::LockMode>;
|
||||
|
||||
struct HashLockKey
|
||||
{
|
||||
size_t operator()(const LockKey &k) const
|
||||
{
|
||||
return std::hash<QString> {}(k.first) ^ std::hash<int> {}(static_cast<int>(k.second));
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_set<LockKey, HashLockKey> _watchedPaths;
|
||||
QTimer _timer;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "logbrowser.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QLayout>
|
||||
#include <QMessageBox>
|
||||
#include <optional>
|
||||
|
||||
#include "configfile.h"
|
||||
#include "guiutility.h"
|
||||
#include "logger.h"
|
||||
#include "resources/fonticon.h"
|
||||
#include "ui_logbrowser.h"
|
||||
|
||||
#include "resources/resources.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
// ==============================================================================
|
||||
|
||||
LogBrowser::LogBrowser(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, ui(new Ui::LogBrowser)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
ui->warningIcon->setPixmap(Resources::FontIcon(u'').pixmap(ui->warningIcon->size()));
|
||||
ui->locationLabel->setText(Logger::instance()->temporaryFolderLogDirPath());
|
||||
|
||||
ui->enableLoggingButton->setChecked(ConfigFile().automaticLogDir());
|
||||
connect(ui->enableLoggingButton, &QCheckBox::toggled, this, &LogBrowser::togglePermanentLogging);
|
||||
|
||||
ui->httpLogButton->setChecked(ConfigFile().logHttp());
|
||||
connect(ui->httpLogButton, &QCheckBox::toggled, this, [](bool enable) {
|
||||
ConfigFile().configureHttpLogging(std::make_optional(enable));
|
||||
});
|
||||
|
||||
ui->spinBox_numberOflogsToKeep->setValue(ConfigFile().automaticDeleteOldLogs());
|
||||
connect(ui->spinBox_numberOflogsToKeep, qOverload<int>(&QSpinBox::valueChanged), this, [](int i) {
|
||||
ConfigFile().setAutomaticDeleteOldLogs(i);
|
||||
Logger::instance()->setMaxLogFiles(i);
|
||||
});
|
||||
|
||||
|
||||
connect(ui->openFolderButton, &QPushButton::clicked, this, []() {
|
||||
QString path = Logger::instance()->temporaryFolderLogDirPath();
|
||||
QDir().mkpath(path);
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
|
||||
});
|
||||
connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QWidget::close);
|
||||
|
||||
ConfigFile cfg;
|
||||
cfg.restoreGeometry(this);
|
||||
}
|
||||
|
||||
LogBrowser::~LogBrowser()
|
||||
{
|
||||
}
|
||||
|
||||
void LogBrowser::setupLoggingFromConfig()
|
||||
{
|
||||
ConfigFile config;
|
||||
auto logger = Logger::instance();
|
||||
|
||||
if (config.automaticLogDir()) {
|
||||
// Don't override other configured logging
|
||||
if (logger->isLoggingToFile())
|
||||
return;
|
||||
|
||||
logger->setupTemporaryFolderLogDir();
|
||||
Logger::instance()->setMaxLogFiles(config.automaticDeleteOldLogs());
|
||||
} else {
|
||||
logger->disableTemporaryFolderLogDir();
|
||||
}
|
||||
}
|
||||
|
||||
void LogBrowser::togglePermanentLogging(bool enabled)
|
||||
{
|
||||
ConfigFile config;
|
||||
config.setAutomaticLogDir(enabled);
|
||||
setupLoggingFromConfig();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/qsferaguilib.h"
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace Ui {
|
||||
class LogBrowser;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The LogBrowser class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class QSFERA_GUI_EXPORT LogBrowser : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LogBrowser(QWidget *parent);
|
||||
~LogBrowser() override;
|
||||
|
||||
/** Sets Logger settings depending on ConfigFile values.
|
||||
*
|
||||
* Currently used for establishing logging to a temporary directory.
|
||||
* Will only enable logging if it isn't enabled already.
|
||||
*/
|
||||
static void setupLoggingFromConfig();
|
||||
|
||||
protected Q_SLOTS:
|
||||
void togglePermanentLogging(bool enabled);
|
||||
|
||||
private:
|
||||
QScopedPointer<Ui::LogBrowser> ui;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>OCC::LogBrowser</class>
|
||||
<widget class="QDialog" name="OCC::LogBrowser">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>399</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Log Output</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="warningIcon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="../resources/client.qrc">:/client/resources/light/warning.svg</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p><b>The logs contain sensitive information which you should not make publicly available</b></span></p></body></html></string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>The client can write debug logs to a temporary folder. These logs are very helpful for diagnosing problems.
|
||||
Since log files can get large, the client will start a new one for each sync run and compress older ones.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>If enabled, logs will be written to:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="locationLabel">
|
||||
<property name="text">
|
||||
<string>C:/log</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</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>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="enableLoggingButton">
|
||||
<property name="text">
|
||||
<string>Enable logging to temporary folder</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="httpLogButton">
|
||||
<property name="text">
|
||||
<string>Log Http traffic </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Log files to keep:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_numberOflogsToKeep">
|
||||
<property name="minimum">
|
||||
<number>5</number>
|
||||
</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>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>These settings persist across client restarts.
|
||||
Note that using any logging command line options will override the settings.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="openFolderButton">
|
||||
<property name="text">
|
||||
<string>Open folder</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../resources/client.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,531 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#include <QtGlobal>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "common/restartmanager.h"
|
||||
#include "gui/application.h"
|
||||
#include "gui/folderman.h"
|
||||
#include "gui/logbrowser.h"
|
||||
#include "gui/networkinformation.h"
|
||||
#include "libsync/configfile.h"
|
||||
#include "libsync/platform.h"
|
||||
#include "libsync/theme.h"
|
||||
#include "resources/loadresources.h"
|
||||
|
||||
#include "common/version.h"
|
||||
#include "gui/translations.h"
|
||||
#include "libsync/logger.h"
|
||||
#include "socketapi/socketapi.h"
|
||||
|
||||
#include <kdsingleapplication.h>
|
||||
|
||||
#ifdef WITH_AUTO_UPDATER
|
||||
#include "updater/updater.h"
|
||||
#endif
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QLibraryInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
#ifdef Q_OS_WIN
|
||||
#include <qt_windows.h>
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcMain, "gui.main", QtInfoMsg)
|
||||
|
||||
namespace {
|
||||
inline auto msgParseOptionsC()
|
||||
{
|
||||
return QStringLiteral("MSG_PARSEOPTIONS:");
|
||||
}
|
||||
|
||||
// Helpers for displaying messages. Note that there is probably no console on Windows.
|
||||
void displayHelpText(const QString &t)
|
||||
{
|
||||
Logger::instance()->attacheToConsole();
|
||||
std::cout << qUtf8Printable(t) << std::endl;
|
||||
#ifdef Q_OS_WIN
|
||||
// No console on Windows.
|
||||
QString spaces(80, QLatin1Char(' ')); // Add a line of non-wrapped space to make the messagebox wide enough.
|
||||
QString text =
|
||||
QStringLiteral("<qt><pre style='white-space:pre-wrap'>") + t.toHtmlEscaped() + QStringLiteral("</pre><pre>") + spaces + QStringLiteral("</pre></qt>");
|
||||
QMessageBox::information(nullptr, Theme::instance()->appNameGUI(), text);
|
||||
#endif
|
||||
}
|
||||
|
||||
struct CommandLineOptions
|
||||
{
|
||||
bool show = false;
|
||||
bool quitInstance = false;
|
||||
|
||||
QString logDir;
|
||||
QString logFile;
|
||||
bool logFlush = false;
|
||||
bool logDebug = false;
|
||||
|
||||
bool debugMode = false;
|
||||
};
|
||||
|
||||
CommandLineOptions parseOptions(const QStringList &arguments)
|
||||
{
|
||||
QCommandLineParser parser;
|
||||
|
||||
QString descriptionText;
|
||||
QTextStream descriptionTextStream(&descriptionText);
|
||||
|
||||
descriptionTextStream << QApplication::translate("CommandLine", "%1 version %2\r\nFile synchronization desktop utility.")
|
||||
.arg(Theme::instance()->appName(), OCC::Version::displayString())
|
||||
<< Qt::endl;
|
||||
|
||||
if (Resources::isVanillaTheme() && !Theme::instance()->helpUrl().isEmpty()) {
|
||||
descriptionTextStream
|
||||
<< Qt::endl
|
||||
<< Qt::endl
|
||||
<< QApplication::translate("CommandLine", "For more information, see %1", "link to homepage").arg(Theme::instance()->helpUrl().toString());
|
||||
}
|
||||
|
||||
parser.setApplicationDescription(descriptionText);
|
||||
|
||||
auto helpOption = parser.addHelpOption();
|
||||
auto versionOption = parser.addVersionOption();
|
||||
|
||||
// this little snippet saves a few lines below
|
||||
auto addOption = [&parser](const QCommandLineOption &option) {
|
||||
parser.addOption(option);
|
||||
return option;
|
||||
};
|
||||
|
||||
auto showSettingsLegacyOption = QCommandLineOption{{QStringLiteral("showsettings")}, QStringLiteral("Hidden legacy option")};
|
||||
showSettingsLegacyOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
parser.addOption(showSettingsLegacyOption);
|
||||
|
||||
auto showOption = addOption({{QStringLiteral("s"), QStringLiteral("show")},
|
||||
QApplication::translate("CommandLine",
|
||||
"Start with the main window visible, or if it is already running, bring it to the front. By default, the client launches in the background.")});
|
||||
auto quitInstanceOption = addOption({{QStringLiteral("q"), QStringLiteral("quit")}, QApplication::translate("CommandLine", "Quit the running instance.")});
|
||||
auto logFileOption = addOption(
|
||||
{QStringLiteral("logfile"), QApplication::translate("CommandLine", "Write log to file (use - to write to stdout)."), QStringLiteral("filename")});
|
||||
auto logDirOption = addOption(
|
||||
{QStringLiteral("logdir"), QApplication::translate("CommandLine", "Write each sync log output in a new file in folder."), QStringLiteral("name")});
|
||||
auto logFlushOption = addOption({QStringLiteral("logflush"), QApplication::translate("CommandLine", "Flush the log file after every write.")});
|
||||
auto logDebugOption = addOption({QStringLiteral("logdebug"), QApplication::translate("CommandLine", "Output debug-level messages in the log.")});
|
||||
auto debugOption = addOption({QStringLiteral("debug"), QApplication::translate("CommandLine", "Enable debug mode.")});
|
||||
addOption({QStringLiteral("cmd"), QApplication::translate("CommandLine", "Forward all arguments to the cmd client. This argument must be the first.")});
|
||||
|
||||
parser.process(arguments);
|
||||
|
||||
CommandLineOptions out;
|
||||
if (parser.isSet(showOption) || parser.isSet(showSettingsLegacyOption)) {
|
||||
out.show = true;
|
||||
}
|
||||
if (parser.isSet(quitInstanceOption)) {
|
||||
out.quitInstance = true;
|
||||
}
|
||||
if (parser.isSet(logFileOption)) {
|
||||
out.logFile = parser.value(logFileOption);
|
||||
}
|
||||
if (parser.isSet(logDirOption)) {
|
||||
if (parser.isSet(logFileOption)) {
|
||||
displayHelpText(QApplication::translate("CommandLine", "--logfile and --logdir are mutually exclusive"));
|
||||
std::exit(1);
|
||||
}
|
||||
out.logDir = parser.value(logDirOption);
|
||||
}
|
||||
if (parser.isSet(logFlushOption)) {
|
||||
out.logFlush = true;
|
||||
}
|
||||
if (parser.isSet(logDebugOption)) {
|
||||
out.logDebug = true;
|
||||
}
|
||||
if (parser.isSet(debugOption)) {
|
||||
out.logDebug = true;
|
||||
out.debugMode = true;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void showDowngradeDialog()
|
||||
{
|
||||
QMessageBox box(QMessageBox::Warning, Theme::instance()->appNameGUI(),
|
||||
QCoreApplication::translate("version check",
|
||||
"Some settings were configured in newer versions of this client "
|
||||
"and use features that are not available in this version"));
|
||||
box.addButton(OCC::Application::tr("Quit"), QMessageBox::AcceptRole);
|
||||
box.exec();
|
||||
QTimer::singleShot(0, qApp, &QApplication::quit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the last version used to write the config file differs from the current version.
|
||||
* If the current version is newer, update the config file with our current version. If the
|
||||
* current version is older, refuse to do anything: this is a downgrade, and it is too risky to
|
||||
* assume that things might work "just fine".
|
||||
*/
|
||||
bool checkClientVersion()
|
||||
{
|
||||
ConfigFile configFile;
|
||||
|
||||
// Did the client version change?
|
||||
// (The client version is adjusted further down)
|
||||
auto configVersion = QVersionNumber::fromString(configFile.clientVersionWithBuildNumberString());
|
||||
auto clientVersion = OCC::Version::versionWithBuildNumber();
|
||||
|
||||
if (configVersion == clientVersion) {
|
||||
// no config backup needed
|
||||
return true;
|
||||
}
|
||||
|
||||
// We allow downgrades as long as the major version stays the same.
|
||||
if (clientVersion.majorVersion() < configVersion.majorVersion()) {
|
||||
// We refuse to downgrade, too much can go wrong.
|
||||
showDowngradeDialog();
|
||||
return false;
|
||||
}
|
||||
|
||||
// We're okay to continue. The settings will be updated in other parts, but here we bump the
|
||||
// version we store in the config file.
|
||||
configFile.backup();
|
||||
configFile.setClientVersionWithBuildNumberString(OCC::Version::versionWithBuildNumber().toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
void setupLogging(const CommandLineOptions &options)
|
||||
{
|
||||
// might be called from second instance
|
||||
auto logger = Logger::instance();
|
||||
logger->install();
|
||||
// call setLogFlush first, other log settings might already imply flushing
|
||||
// so setting it false in the end will have undesired results.
|
||||
logger->setLogFlush(options.logFlush);
|
||||
|
||||
if (!options.logDir.isEmpty()) {
|
||||
logger->setLogDir(options.logDir);
|
||||
}
|
||||
if (!options.logFile.isEmpty()) {
|
||||
Q_ASSERT(options.logDir.isEmpty());
|
||||
logger->setLogFile(options.logFile);
|
||||
}
|
||||
logger->setLogDebug(options.logDebug);
|
||||
|
||||
// Possibly configure logging from config file
|
||||
LogBrowser::setupLoggingFromConfig();
|
||||
|
||||
qCInfo(lcMain) << u"##################" << Theme::instance()->appName() << u"locale:" << QLocale::system().name() << u"version:"
|
||||
<< Theme::instance()->aboutVersions(Theme::VersionFormat::OneLiner);
|
||||
qCInfo(lcMain) << u"Arguments:" << qApp->arguments();
|
||||
}
|
||||
|
||||
QString setupTranslations(QApplication *app)
|
||||
{
|
||||
const auto trPath = Translations::translationsDirectoryPath();
|
||||
qCDebug(lcMain) << u"Translations directory path:" << trPath;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
QStringList uiLanguages = QLocale::system().uiLanguages(QLocale::TagSeparator::Underscore);
|
||||
#else
|
||||
QStringList uiLanguages;
|
||||
for (auto lang : QLocale::system().uiLanguages()) {
|
||||
uiLanguages << lang.replace(QLatin1Char('-'), QLatin1Char('_'));
|
||||
}
|
||||
#endif
|
||||
qCDebug(lcMain) << u"UI languages:" << uiLanguages;
|
||||
|
||||
// the user can also set a locale in the settings, so we need to load the config file
|
||||
const ConfigFile cfg;
|
||||
|
||||
// we need to track the enforced language separately, since we need to distinguish between locale-provided
|
||||
// and user-enforced one below
|
||||
const QString enforcedLocale = cfg.uiLanguage();
|
||||
qCDebug(lcMain) << u"Enforced language:" << enforcedLocale;
|
||||
|
||||
// note that user-enforced language are prioritized over the theme enforced one
|
||||
// to make testing easier.
|
||||
if (!enforcedLocale.isEmpty()) {
|
||||
uiLanguages.prepend(enforcedLocale);
|
||||
}
|
||||
|
||||
QString displayLanguage;
|
||||
|
||||
auto substLang = [](const QString &lang) {
|
||||
// Map the more appropriate script codes
|
||||
// to country codes as used by Qt and
|
||||
// transifex translation conventions.
|
||||
|
||||
if (lang == QLatin1String("zh_Hans")) {
|
||||
// Simplified Chinese
|
||||
return QStringLiteral("zh_CN");
|
||||
} else if (lang == QLatin1String("zh_Hant")) {
|
||||
// Traditional Chinese
|
||||
return QStringLiteral("zh_TW");
|
||||
}
|
||||
|
||||
return lang;
|
||||
};
|
||||
|
||||
for (QString lang : std::as_const(uiLanguages)) {
|
||||
lang = substLang(lang);
|
||||
const QString trFile = Translations::translationsFilePrefix() + lang;
|
||||
if (QTranslator *translator = new QTranslator(app); translator->load(trFile, trPath) || lang.startsWith(QLatin1String("en"))) {
|
||||
// Permissive approach: Qt and keychain translations
|
||||
// may be missing, but Qt translations must be there in order
|
||||
// for us to accept the language. Otherwise, we try with the next.
|
||||
// "en" is an exception as it is the default language and may not
|
||||
// have a translation file provided.
|
||||
qCInfo(lcMain) << u"Using" << lang << u"translation" << translator->language();
|
||||
displayLanguage = lang;
|
||||
|
||||
const QString qtTrPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
||||
qCDebug(lcMain) << u"qtTrPath:" << qtTrPath;
|
||||
const QString qtTrFile = QLatin1String("qt_") + lang;
|
||||
qCDebug(lcMain) << u"qtTrFile:" << qtTrFile;
|
||||
const QString qtBaseTrFile = QLatin1String("qtbase_") + lang;
|
||||
qCDebug(lcMain) << u"qtBaseTrFile:" << qtBaseTrFile;
|
||||
|
||||
QTranslator *qtTranslator = new QTranslator(app);
|
||||
QTranslator *qtkeychainTranslator = new QTranslator(app);
|
||||
|
||||
if (!qtTranslator->load(qtTrFile, qtTrPath)) {
|
||||
if (!qtTranslator->load(qtTrFile, trPath)) {
|
||||
if (!qtTranslator->load(qtBaseTrFile, qtTrPath)) {
|
||||
if (!qtTranslator->load(qtBaseTrFile, trPath)) {
|
||||
qCCritical(lcMain) << u"Could not load Qt translations";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QString qtkeychainTrFile = QLatin1String("qtkeychain_") + lang;
|
||||
if (!qtkeychainTranslator->load(qtkeychainTrFile, qtTrPath)) {
|
||||
if (!qtkeychainTranslator->load(qtkeychainTrFile, trPath)) {
|
||||
qCCritical(lcMain) << u"Could not load qtkeychain translations";
|
||||
}
|
||||
}
|
||||
|
||||
if (!translator->isEmpty() && !qApp->installTranslator(translator)) {
|
||||
qCCritical(lcMain) << u"Failed to install translator";
|
||||
translator->deleteLater();
|
||||
}
|
||||
if (!qtTranslator->isEmpty() && !qApp->installTranslator(qtTranslator)) {
|
||||
qCCritical(lcMain) << u"Failed to install Qt translator";
|
||||
qtTranslator->deleteLater();
|
||||
}
|
||||
if (!qtkeychainTranslator->isEmpty() && !qApp->installTranslator(qtkeychainTranslator)) {
|
||||
qCCritical(lcMain) << u"Failed to install qtkeychain translator";
|
||||
qtkeychainTranslator->deleteLater();
|
||||
}
|
||||
|
||||
// makes sure widgets with locale-dependent formatting, e.g., QDateEdit, display the correct formatting
|
||||
// if the language is provided by the system locale anyway (i.e., coming from QLocale::system().uiLanguages()), we should
|
||||
// not mess with the system locale, though
|
||||
// if we did, we would enforce a locale for no apparent reason
|
||||
// see https://github.com/owncloud/client/issues/8608 for more information
|
||||
if (enforcedLocale == lang) {
|
||||
QLocale newLocale(lang);
|
||||
qCDebug(lcMain) << u"language" << lang << u"was enforced, changing default locale to" << newLocale;
|
||||
QLocale::setDefault(newLocale);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
translator->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
return displayLanguage;
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
return RestartManager([](int argc, char **argv) {
|
||||
// when called with --cmd we run the cmd client in a sub process and forward everything
|
||||
if (argc > 1 && argv[1] == QByteArrayLiteral("--cmd")) {
|
||||
#ifdef Q_OS_WIN
|
||||
// On Windows ui applications don't have console access by default
|
||||
// We can't use our normal workaround to attach to the parent console as it breaks the stdin handling.
|
||||
// Therefore, we create a new console and redirect our streams.
|
||||
AllocConsole();
|
||||
freopen("CONIN$", "r", stdin);
|
||||
freopen("CONOUT$", "w", stdout);
|
||||
freopen("CONOUT$", "w", stderr);
|
||||
#endif
|
||||
QCoreApplication cmdApp(argc, argv);
|
||||
QProcess cmd;
|
||||
cmd.setProcessChannelMode(QProcess::ForwardedChannels);
|
||||
cmd.setInputChannelMode(QProcess::ForwardedInputChannel);
|
||||
|
||||
const QString app = []() -> QString {
|
||||
#ifdef Q_OS_WIN
|
||||
return QCoreApplication::applicationFilePath().chopped(4) + QStringLiteral("cmd.exe");
|
||||
#else
|
||||
return QCoreApplication::applicationFilePath() + QStringLiteral("cmd");
|
||||
#endif
|
||||
}();
|
||||
cmd.start(app, cmdApp.arguments().mid(2));
|
||||
if (!cmd.waitForFinished(-1)) {
|
||||
std::cout << "Failed to start" << qPrintable(cmd.program()) << std::endl;
|
||||
}
|
||||
#ifdef Q_OS_WIN
|
||||
// readline to keep the console window open until closed by the user
|
||||
std::string dummy;
|
||||
std::cout << "Press enter to close";
|
||||
std::getline(std::cin, dummy);
|
||||
#endif
|
||||
return cmd.exitCode();
|
||||
}
|
||||
|
||||
// load the resources
|
||||
const OCC::ResourcesLoader resource;
|
||||
|
||||
// Create a `Platform` instance so it can set-up/tear-down stuff for us, and do any
|
||||
// initialisation that needs to be done before creating a QApplication
|
||||
const auto platform = Platform::create(Platform::Type::Gui);
|
||||
|
||||
// Create the (Q)Application instance:
|
||||
QApplication app(argc, argv);
|
||||
app.setOrganizationDomain(Theme::instance()->orgDomainName());
|
||||
app.setApplicationName(Theme::instance()->appName());
|
||||
app.setWindowIcon(Theme::instance()->applicationIcon());
|
||||
app.setApplicationVersion(Theme::instance()->versionSwitchOutput());
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// HACK:
|
||||
// With X11 arg0.name is used to map WM_CLASS to the desktop file.
|
||||
// With Wayland it uses app.desktopFileName() or app.organizationDomain()
|
||||
// As we preferably use AppImages the real deskop file name is unknown, we just ensure it mathces the StartupWMClass entry in our desktop file
|
||||
if (qgetenv("XDG_SESSION_TYPE") == "wayland") {
|
||||
// https://bugreports.qt.io/browse/QTBUG-77182 Qt 6.9 brings setAppId() but its private api
|
||||
app.setDesktopFileName(QFileInfo(app.applicationFilePath()).baseName());
|
||||
}
|
||||
#endif
|
||||
|
||||
// Load the translations before option parsing, so we can localize help text and error messages.
|
||||
const QString displayLanguage = setupTranslations(&app);
|
||||
|
||||
// parse the arguments before we handle singleApplication
|
||||
// errors and help/version need to be handled in this instance
|
||||
const auto options = parseOptions(app.arguments());
|
||||
|
||||
KDSingleApplication singleApplication;
|
||||
|
||||
if (!singleApplication.isPrimaryInstance()) {
|
||||
// if the application is already running, notify it.
|
||||
qCInfo(lcMain) << u"Already running, exiting...";
|
||||
if (app.isSessionRestored()) {
|
||||
// This call is mirrored with the one in Application::slotParseMessage
|
||||
qCInfo(lcMain) << u"Session was restored, don't notify app!";
|
||||
return -1;
|
||||
}
|
||||
|
||||
QStringList args = app.arguments();
|
||||
if (args.size() > 1) {
|
||||
QString msg = args.join(QLatin1String("|"));
|
||||
if (!singleApplication.sendMessage((msgParseOptionsC() + msg).toUtf8()))
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if the user upgraded or downgraded. We do this as early as possible, to detect
|
||||
// a possible downgrade.
|
||||
if (!checkClientVersion()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
setupLogging(options);
|
||||
NetworkInformation::instance(); //
|
||||
|
||||
platform->setApplication(&app);
|
||||
|
||||
auto folderManager = FolderMan::createInstance();
|
||||
|
||||
if (!AccountManager::instance()->restore()) {
|
||||
qCCritical(lcMain) << u"Could not read the account settings, quitting";
|
||||
QMessageBox::critical(nullptr, QCoreApplication::translate("account loading", "Error accessing the configuration file"),
|
||||
QCoreApplication::translate("account loading", "There was an error while accessing the configuration file at %1.")
|
||||
.arg(ConfigFile::configFile()),
|
||||
QMessageBox::Close);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Setup the folders. This includes a downgrade-detection, in which case the return value
|
||||
// is empty. Note that the value 0 (zero) is a valid return value (non-empty), in which case
|
||||
// the dialog is not shown.
|
||||
if (!FolderMan::instance()->loadFolders().has_value()) {
|
||||
// Empty return value: there was a downgrade detected on one of the databases
|
||||
showDowngradeDialog();
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto ocApp = Application::createInstance(displayLanguage, options.debugMode);
|
||||
|
||||
QObject::connect(platform.get(), &Platform::requestAttention, ocApp.get(), &Application::showSettings);
|
||||
|
||||
QObject::connect(&singleApplication, &KDSingleApplication::messageReceived, ocApp.get(), [&](const QByteArray &message) {
|
||||
const QString msg = QString::fromUtf8(message);
|
||||
qCInfo(lcMain) << Q_FUNC_INFO << msg;
|
||||
if (msg.startsWith(msgParseOptionsC())) {
|
||||
const QStringList optionsStrings = msg.mid(msgParseOptionsC().size()).split(QLatin1Char('|'));
|
||||
CommandLineOptions options = parseOptions(optionsStrings);
|
||||
if (options.show) {
|
||||
ocApp->showSettings();
|
||||
}
|
||||
if (options.quitInstance) {
|
||||
qApp->quit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
platform->startServices();
|
||||
|
||||
#ifdef WITH_AUTO_UPDATER
|
||||
if (Updater::instance()) {
|
||||
// validate whether the last update was successful.
|
||||
Updater::instance()->validateUpdate();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Enable syncing. We cannot do this earlier, because the UI needs to be set up in order to
|
||||
// show sync errors. We also want to wait for the auto-updater to finish, in case it needs
|
||||
// to install an update.
|
||||
folderManager->setSyncEnabled(true);
|
||||
|
||||
if (options.show) {
|
||||
ocApp->showSettings();
|
||||
// The user explicitly requested the settings dialog, so don't start the new-account wizard.
|
||||
}
|
||||
|
||||
// Display the wizard if we don't have an account yet, and no other UI is showing.
|
||||
if (AccountManager::instance()->accounts().isEmpty()) {
|
||||
QTimer::singleShot(0, ocApp.get(), &Application::runNewAccountWizard);
|
||||
}
|
||||
|
||||
// Now that everything is up and running, start accepting connections/requests from the shell integration.
|
||||
folderManager->socketApi()->startShellIntegration();
|
||||
|
||||
return app.exec();
|
||||
}).exec(argc, argv);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#include "expandingheaderview.h"
|
||||
#include "models.h"
|
||||
|
||||
#include "configfile.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
ExpandingHeaderView::ExpandingHeaderView(const QString &objectName, QWidget *parent)
|
||||
: QHeaderView(Qt::Horizontal, parent)
|
||||
{
|
||||
setSectionsClickable(true);
|
||||
setHighlightSections(true);
|
||||
|
||||
connect(this, &QHeaderView::sectionCountChanged, this, [this] { resizeColumns(false); });
|
||||
|
||||
setObjectName(objectName);
|
||||
ConfigFile cfg;
|
||||
if (!cfg.restoreGeometryHeader(this)) {
|
||||
_requiresReset = true;
|
||||
}
|
||||
}
|
||||
|
||||
ExpandingHeaderView::~ExpandingHeaderView()
|
||||
{
|
||||
ConfigFile cfg;
|
||||
cfg.saveGeometryHeader(this);
|
||||
}
|
||||
|
||||
int ExpandingHeaderView::expandingColumn() const
|
||||
{
|
||||
return _expandingColumn;
|
||||
}
|
||||
|
||||
void ExpandingHeaderView::setExpandingColumn(int newExpandingColumn)
|
||||
{
|
||||
_expandingColumn = newExpandingColumn;
|
||||
}
|
||||
|
||||
void ExpandingHeaderView::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QHeaderView::resizeEvent(event);
|
||||
resizeColumns();
|
||||
}
|
||||
|
||||
bool ExpandingHeaderView::resizeToContent() const
|
||||
{
|
||||
return _resizeToContent;
|
||||
}
|
||||
|
||||
void ExpandingHeaderView::setResizeToContent(bool newResizeToContent)
|
||||
{
|
||||
_resizeToContent = newResizeToContent;
|
||||
}
|
||||
|
||||
void ExpandingHeaderView::resizeColumns(bool reset)
|
||||
{
|
||||
int availableWidth = width();
|
||||
const auto defaultSize = defaultSectionSize();
|
||||
if (_requiresReset && _resizeToContent) {
|
||||
// wee need some rows to adjust to the content
|
||||
if (model()->rowCount() == 0) {
|
||||
return;
|
||||
}
|
||||
reset = true;
|
||||
}
|
||||
if (reset) {
|
||||
_requiresReset = false;
|
||||
if (_resizeToContent) {
|
||||
resizeSections(QHeaderView::ResizeToContents);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (i == _expandingColumn || isSectionHidden(i)) {
|
||||
continue;
|
||||
}
|
||||
if (reset) {
|
||||
resizeSection(i, _resizeToContent ? sectionSize(i) : defaultSize);
|
||||
}
|
||||
availableWidth -= sectionSize(i);
|
||||
}
|
||||
resizeSection(_expandingColumn, availableWidth);
|
||||
}
|
||||
|
||||
void ExpandingHeaderView::addResetActionToMenu(QMenu *menu)
|
||||
{
|
||||
menu->addAction(tr("Reset column sizes"), this, [this] {
|
||||
resizeColumns(true);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QHeaderView>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class ExpandingHeaderView : public QHeaderView
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ExpandingHeaderView(const QString &objectName, QWidget *parent = nullptr);
|
||||
~ExpandingHeaderView();
|
||||
|
||||
int expandingColumn() const;
|
||||
void setExpandingColumn(int newExpandingColumn);
|
||||
|
||||
void resizeColumns(bool reset = false);
|
||||
void addResetActionToMenu(QMenu *menu);
|
||||
|
||||
bool resizeToContent() const;
|
||||
void setResizeToContent(bool newResizeToContent);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
|
||||
private:
|
||||
bool _requiresReset = false;
|
||||
bool _resizeToContent = false;
|
||||
int _expandingColumn = 0;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#include "models.h"
|
||||
|
||||
#include <QActionGroup>
|
||||
#include <QApplication>
|
||||
#include <QItemSelectionRange>
|
||||
#include <QMenu>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QTextStream>
|
||||
#include <QTimer>
|
||||
|
||||
#include <functional>
|
||||
|
||||
void OCC::Models::SignalledQSortFilterProxyModel::setFilterFixedStringSignalled(const QString &pattern)
|
||||
{
|
||||
setFilterFixedString(pattern);
|
||||
Q_EMIT filterChanged();
|
||||
}
|
||||
|
||||
QString OCC::Models::formatSelection(const QModelIndexList &items, int dataRole)
|
||||
{
|
||||
if (items.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const auto columns = items.first().model()->columnCount();
|
||||
QString out;
|
||||
QTextStream stream(&out);
|
||||
|
||||
QString begin;
|
||||
QString end;
|
||||
|
||||
const auto iterate = [columns](const std::function<void(int)> &f) {
|
||||
if (qApp->layoutDirection() != Qt::RightToLeft) {
|
||||
for (int c = 0; c < columns; ++c) {
|
||||
f(c);
|
||||
}
|
||||
} else {
|
||||
for (int c = columns - 1; c >= 0; --c) {
|
||||
f(c);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (qApp->layoutDirection() == Qt::RightToLeft) {
|
||||
stream << Qt::right;
|
||||
begin = QLatin1Char(',');
|
||||
} else {
|
||||
stream << Qt::left;
|
||||
end = QLatin1Char(',');
|
||||
}
|
||||
|
||||
iterate([&](int c) {
|
||||
const auto width = items.first().model()->headerData(c, Qt::Horizontal, StringFormatWidthRole).toInt();
|
||||
Q_ASSERT(width);
|
||||
stream << begin
|
||||
<< qSetFieldWidth(width)
|
||||
<< items.first().model()->headerData(c, Qt::Horizontal).toString()
|
||||
<< qSetFieldWidth(0)
|
||||
<< end;
|
||||
});
|
||||
stream << Qt::endl;
|
||||
for (const auto &index : items) {
|
||||
iterate([&](int c) {
|
||||
const auto &child = index.siblingAtColumn(c);
|
||||
stream << begin
|
||||
<< qSetFieldWidth(child.model()->headerData(c, Qt::Horizontal, StringFormatWidthRole).toInt())
|
||||
<< child.data(dataRole).toString()
|
||||
<< qSetFieldWidth(0)
|
||||
<< end;
|
||||
});
|
||||
stream << Qt::endl;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::function<void()> OCC::Models::addFilterMenuItems(QMenu *menu, const QStringList &candidates, SignalledQSortFilterProxyModel *model, int column, const QString &columnName, int role)
|
||||
{
|
||||
menu->addAction(QApplication::translate("OCC::Models", "%1 Filter:").arg(columnName))->setEnabled(false);
|
||||
|
||||
auto filterGroup = new QActionGroup(menu);
|
||||
filterGroup->setExclusive(true);
|
||||
|
||||
const auto currentFilter = model->filterRegularExpression().pattern();
|
||||
auto addAction = [=](const QString &s, const QString &filter) {
|
||||
auto action = menu->addAction(s, menu, [=]() {
|
||||
model->setFilterRole(role);
|
||||
model->setFilterKeyColumn(column);
|
||||
model->setFilterFixedStringSignalled(filter);
|
||||
});
|
||||
action->setCheckable(true);
|
||||
action->setChecked(currentFilter == QRegularExpression::escape(filter));
|
||||
filterGroup->addAction(action);
|
||||
return action;
|
||||
};
|
||||
|
||||
|
||||
auto noFilter = addAction(QApplication::translate("OCC::Models", "All"), QString());
|
||||
|
||||
for (const auto &c : candidates) {
|
||||
addAction(c, c);
|
||||
}
|
||||
|
||||
auto resetFunction = [noFilter]() {
|
||||
noFilter->setChecked(true);
|
||||
noFilter->trigger();
|
||||
};
|
||||
return resetFunction;
|
||||
}
|
||||
|
||||
bool OCC::Models::FilteringProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
// column doesn't matter, since we do not filter based on a specific column but on a specific item role
|
||||
const auto index = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent);
|
||||
return sourceModel()->data(index, static_cast<int>(filterRole())).toBool();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QModelIndexList>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
class QSortFilterProxyModel;
|
||||
class QMenu;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace Models {
|
||||
Q_NAMESPACE
|
||||
|
||||
enum DataRoles {
|
||||
UnderlyingDataRole = Qt::UserRole + 100,
|
||||
StringFormatWidthRole, // The width for a cvs formatted column
|
||||
|
||||
// data() should return boolean values for this role to work in conjunction with FilteringProxyModel
|
||||
FilterRole,
|
||||
};
|
||||
Q_ENUM_NS(DataRoles)
|
||||
|
||||
class SignalledQSortFilterProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using QSortFilterProxyModel::QSortFilterProxyModel;
|
||||
|
||||
void setFilterFixedStringSignalled(const QString &pattern);
|
||||
|
||||
Q_SIGNALS:
|
||||
void filterChanged();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns a cvs representation of a table
|
||||
*/
|
||||
QString formatSelection(const QModelIndexList &items, int dataRole = Qt::DisplayRole);
|
||||
|
||||
std::function<void()> addFilterMenuItems(QMenu *menu, const QStringList &candidates, SignalledQSortFilterProxyModel *model, int column, const QString &columnName, int role);
|
||||
|
||||
/**
|
||||
* Returns a vector with indices
|
||||
* This is handy to iterate over the columns
|
||||
*/
|
||||
template <typename T>
|
||||
auto range(T start, T end)
|
||||
{
|
||||
std::vector<T> out;
|
||||
out.reserve(end - start);
|
||||
for (auto i = start; i < end; ++i) {
|
||||
out.push_back(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
auto range(T end)
|
||||
{
|
||||
return range<T>(0, end);
|
||||
}
|
||||
|
||||
class FilteringProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
using QSortFilterProxyModel::QSortFilterProxyModel;
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
};
|
||||
} // OCC::Models namespace
|
||||
} // OCC namespace
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user