|
@ -2,6 +2,7 @@
|
|||
<qresource prefix="/client">
|
||||
<file>resources/settings.png</file>
|
||||
<file>resources/settings@2x.png</file>
|
||||
<file>resources/activity.svg</file>
|
||||
<file>resources/activity.png</file>
|
||||
<file>resources/activity@2x.png</file>
|
||||
<file>resources/network.png</file>
|
||||
|
@ -11,6 +12,7 @@
|
|||
<file>resources/lock-https.png</file>
|
||||
<file>resources/lock-https@2x.png</file>
|
||||
<file>resources/account.png</file>
|
||||
<file>resources/account.svg</file>
|
||||
<file>resources/more.svg</file>
|
||||
<file>resources/delete.png</file>
|
||||
<file>resources/close.svg</file>
|
||||
|
@ -28,7 +30,14 @@
|
|||
<file>resources/copy.svg</file>
|
||||
<file>resources/state-sync.svg</file>
|
||||
<file>resources/add.png</file>
|
||||
<file>resources/add-color.svg</file>
|
||||
<file>resources/state-info.svg</file>
|
||||
<file>resources/change.svg</file>
|
||||
<file>resources/delete-color.svg</file>
|
||||
</qresource>
|
||||
<qresource prefix="/"/>
|
||||
<qresource prefix="/qml">
|
||||
<file>src/gui/tray/Window.qml</file>
|
||||
<file>src/gui/tray/UserLine.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
1
resources/add-color.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path fill="#00d400" d="M9.02 13.98h-2v-5h-5v-2h5v-5h2v5l5-.028V8.98h-5z"/></svg>
|
After Width: | Height: | Size: 179 B |
1
resources/change.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" version="1.1" height="16"><path d="m8 2c-2.142 0-4.125 1.145-5.196 3l1.948 1.125c0.671-1.162 1.906-1.875 3.2476-1.875 1.1906 0 2.297 0.56157 3 1.5l-1.5 1.5h4.5v-4.5l-1.406 1.406c-1.129-1.348-2.802-2.1563-4.594-2.1563z"/><path d="m2 8.75v4.5l1.408-1.41c1.116 1.334 2.817 2.145 4.592 2.16 2.16 0.01827 4.116-1.132 5.196-3.002l-1.948-1.125c-0.677 1.171-1.9005 1.886-3.248 1.875-1.18-0.01-2.3047-0.572-3-1.5l1.5-1.5z"/></svg>
|
After Width: | Height: | Size: 493 B |
1
resources/delete-color.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16"><path d="m3.0503 4.4645 3.5355 3.5355-3.5355 3.536 1.4142 1.414 3.5355-3.5358 3.536 3.5358 1.414-1.414-3.5358-3.536 3.5358-3.5355-1.414-1.4142-3.536 3.5355-3.5355-3.5355-1.4142 1.4142z" fill="#d40000"/></svg>
|
After Width: | Height: | Size: 306 B |
|
@ -255,7 +255,7 @@ void Utility::usleep(int usec)
|
|||
}
|
||||
|
||||
// This can be overriden from the tests
|
||||
OCSYNC_EXPORT bool fsCasePreserving_override = []()-> bool {
|
||||
OCSYNC_EXPORT bool fsCasePreserving_override = []() -> bool {
|
||||
QByteArray env = qgetenv("OWNCLOUD_TEST_CASE_PRESERVING");
|
||||
if (!env.isEmpty())
|
||||
return env.toInt();
|
||||
|
@ -362,12 +362,12 @@ QString Utility::fileNameForGuiUse(const QString &fName)
|
|||
QByteArray Utility::normalizeEtag(QByteArray etag)
|
||||
{
|
||||
/* strip "XXXX-gzip" */
|
||||
if(etag.startsWith('"') && etag.endsWith("-gzip\"")) {
|
||||
if (etag.startsWith('"') && etag.endsWith("-gzip\"")) {
|
||||
etag.chop(6);
|
||||
etag.remove(0, 1);
|
||||
}
|
||||
/* strip trailing -gzip */
|
||||
if(etag.endsWith("-gzip")) {
|
||||
if (etag.endsWith("-gzip")) {
|
||||
etag.chop(5);
|
||||
}
|
||||
/* strip normal quotes */
|
||||
|
@ -400,7 +400,7 @@ void Utility::crash()
|
|||
// without compiler warnings about possible truncation
|
||||
uint Utility::convertSizeToUint(size_t &convertVar)
|
||||
{
|
||||
if( convertVar > UINT_MAX ) {
|
||||
if (convertVar > UINT_MAX) {
|
||||
//throw std::bad_cast();
|
||||
convertVar = UINT_MAX; // intentionally default to wrong value here to not crash: exception handling TBD
|
||||
}
|
||||
|
@ -409,7 +409,7 @@ uint Utility::convertSizeToUint(size_t &convertVar)
|
|||
|
||||
uint Utility::convertSizeToInt(size_t &convertVar)
|
||||
{
|
||||
if( convertVar > INT_MAX ) {
|
||||
if (convertVar > INT_MAX) {
|
||||
//throw std::bad_cast();
|
||||
convertVar = INT_MAX; // intentionally default to wrong value here to not crash: exception handling TBD
|
||||
}
|
||||
|
@ -465,7 +465,7 @@ QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from)
|
|||
|
||||
if (floor(secs / 3600.0) > 0) {
|
||||
int hours = floor(secs / 3600.0);
|
||||
if(hours == 1){
|
||||
if (hours == 1) {
|
||||
return (QObject::tr("%n hour ago", "", hours));
|
||||
} else {
|
||||
return (QObject::tr("%n hours ago", "", hours));
|
||||
|
@ -480,7 +480,7 @@ QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from)
|
|||
return QObject::tr("Less than a minute ago");
|
||||
}
|
||||
|
||||
} else if(minutes == 1){
|
||||
} else if (minutes == 1) {
|
||||
return (QObject::tr("%n minute ago", "", minutes));
|
||||
} else {
|
||||
return (QObject::tr("%n minutes ago", "", minutes));
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#ifndef UTILITY_H
|
||||
#define UTILITY_H
|
||||
|
||||
|
||||
#include "ocsynclib.h"
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
|
@ -29,6 +30,7 @@
|
|||
#include <QMap>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QtQuick/QQuickImageProvider>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
project(gui)
|
||||
find_package(Qt5 REQUIRED COMPONENTS Widgets)
|
||||
find_package(Qt5 REQUIRED COMPONENTS Widgets Svg)
|
||||
set(CMAKE_AUTOMOC TRUE)
|
||||
set(CMAKE_AUTOUIC TRUE)
|
||||
set(CMAKE_AUTORCC TRUE)
|
||||
|
@ -24,7 +24,6 @@ set(client_UI_SRCS
|
|||
ignorelisteditor.ui
|
||||
ignorelisttablewidget.ui
|
||||
networksettings.ui
|
||||
activitywidget.ui
|
||||
synclogdialog.ui
|
||||
settingsdialog.ui
|
||||
sharedialog.ui
|
||||
|
@ -35,6 +34,8 @@ set(client_UI_SRCS
|
|||
addcertificatedialog.ui
|
||||
proxyauthdialog.ui
|
||||
mnemonicdialog.ui
|
||||
tray/Window.qml
|
||||
tray/UserLine.qml
|
||||
wizard/flow2authwidget.ui
|
||||
wizard/owncloudadvancedsetuppage.ui
|
||||
wizard/owncloudconnectionmethoddialog.ui
|
||||
|
@ -73,10 +74,6 @@ set(client_SRCS
|
|||
openfilemanager.cpp
|
||||
owncloudgui.cpp
|
||||
owncloudsetupwizard.cpp
|
||||
activitydata.cpp
|
||||
activitylistmodel.cpp
|
||||
activitywidget.cpp
|
||||
activityitemdelegate.cpp
|
||||
selectivesyncdialog.cpp
|
||||
settingsdialog.cpp
|
||||
sharedialog.cpp
|
||||
|
@ -99,12 +96,15 @@ set(client_SRCS
|
|||
synclogdialog.cpp
|
||||
tooltipupdater.cpp
|
||||
notificationconfirmjob.cpp
|
||||
servernotificationhandler.cpp
|
||||
guiutility.cpp
|
||||
elidedlabel.cpp
|
||||
headerbanner.cpp
|
||||
iconjob.cpp
|
||||
remotewipe.cpp
|
||||
tray/ActivityData.cpp
|
||||
tray/ActivityListModel.cpp
|
||||
tray/UserModel.cpp
|
||||
tray/NotificationHandler.cpp
|
||||
creds/credentialsfactory.cpp
|
||||
creds/httpcredentialsgui.cpp
|
||||
creds/oauth.cpp
|
||||
|
@ -298,7 +298,7 @@ else()
|
|||
endif()
|
||||
|
||||
add_library(updater STATIC ${updater_SRCS})
|
||||
target_link_libraries(updater ${synclib_NAME} Qt5::Widgets Qt5::Network Qt5::Xml Qt5::WebEngineWidgets)
|
||||
target_link_libraries(updater ${synclib_NAME} Qt5::Widgets Qt5::Svg Qt5::Network Qt5::Xml Qt5::WebEngineWidgets)
|
||||
target_include_directories(updater PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
set_target_properties( ${APPLICATION_EXECUTABLE} PROPERTIES
|
||||
|
@ -308,7 +308,7 @@ set_target_properties( ${APPLICATION_EXECUTABLE} PROPERTIES
|
|||
set_target_properties( ${APPLICATION_EXECUTABLE} PROPERTIES
|
||||
INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}/${APPLICATION_EXECUTABLE};${CMAKE_INSTALL_RPATH}" )
|
||||
|
||||
target_link_libraries( ${APPLICATION_EXECUTABLE} Qt5::Widgets Qt5::Network Qt5::Xml)
|
||||
target_link_libraries( ${APPLICATION_EXECUTABLE} Qt5::Widgets Qt5::Svg Qt5::Network Qt5::Xml)
|
||||
target_link_libraries( ${APPLICATION_EXECUTABLE} ${synclib_NAME} )
|
||||
target_link_libraries( ${APPLICATION_EXECUTABLE} updater )
|
||||
target_link_libraries( ${APPLICATION_EXECUTABLE} ${OS_SPECIFIC_LINK_LIBRARIES} )
|
||||
|
|
|
@ -89,6 +89,9 @@ private:
|
|||
// Adds an account to the tracked list, emitting accountAdded()
|
||||
void addAccountState(AccountState *accountState);
|
||||
|
||||
AccountManager() {}
|
||||
QList<AccountStatePtr> _accounts;
|
||||
|
||||
public slots:
|
||||
/// Saves account data, not including the credentials
|
||||
void saveAccount(Account *a);
|
||||
|
@ -104,9 +107,5 @@ Q_SIGNALS:
|
|||
void accountAdded(AccountState *account);
|
||||
void accountRemoved(AccountState *account);
|
||||
void removeAccountFolders(AccountState *account);
|
||||
|
||||
private:
|
||||
AccountManager() {}
|
||||
QList<AccountStatePtr> _accounts;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -143,9 +143,6 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
|
|||
_ui->_folderList->setAttribute(Qt::WA_Hover, true);
|
||||
_ui->_folderList->installEventFilter(mouseCursorChanger);
|
||||
|
||||
createAccountToolbox();
|
||||
connect(AccountManager::instance(), &AccountManager::accountAdded,
|
||||
this, &AccountSettings::slotAccountAdded);
|
||||
connect(this, &AccountSettings::removeAccountFolders,
|
||||
AccountManager::instance(), &AccountManager::removeAccountFolders);
|
||||
connect(_ui->_folderList, &QWidget::customContextMenuRequested,
|
||||
|
@ -207,36 +204,13 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
|
|||
_ui->encryptionMessage->hide();
|
||||
}
|
||||
|
||||
connect(UserModel::instance(), &UserModel::addAccount,
|
||||
this, &AccountSettings::slotOpenAccountWizard);
|
||||
|
||||
customizeStyle();
|
||||
}
|
||||
|
||||
|
||||
void AccountSettings::createAccountToolbox()
|
||||
{
|
||||
QMenu *menu = new QMenu();
|
||||
|
||||
connect(menu, &QMenu::aboutToShow, this, &AccountSettings::slotMenuBeforeShow);
|
||||
|
||||
_addAccountAction = new QAction(tr("Add new"), this);
|
||||
menu->addAction(_addAccountAction);
|
||||
connect(_addAccountAction, &QAction::triggered, this, &AccountSettings::slotOpenAccountWizard);
|
||||
|
||||
_toggleSignInOutAction = new QAction(tr("Log out"), this);
|
||||
connect(_toggleSignInOutAction, &QAction::triggered, this, &AccountSettings::slotToggleSignInState);
|
||||
menu->addAction(_toggleSignInOutAction);
|
||||
|
||||
QAction *action = new QAction(tr("Remove"), this);
|
||||
menu->addAction(action);
|
||||
connect(action, &QAction::triggered, this, &AccountSettings::slotDeleteAccount);
|
||||
|
||||
_ui->_accountToolbox->setText(tr("Account") + QLatin1Char(' '));
|
||||
_ui->_accountToolbox->setMenu(menu);
|
||||
_ui->_accountToolbox->setPopupMode(QToolButton::InstantPopup);
|
||||
|
||||
slotAccountAdded(_accountState);
|
||||
}
|
||||
|
||||
|
||||
void AccountSettings::slotNewMnemonicGenerated()
|
||||
{
|
||||
_ui->encryptionMessage->setText(tr("This account supports end-to-end encryption"));
|
||||
|
@ -249,24 +223,6 @@ void AccountSettings::slotNewMnemonicGenerated()
|
|||
_ui->encryptionMessage->show();
|
||||
}
|
||||
|
||||
void AccountSettings::slotMenuBeforeShow() {
|
||||
if (_menuShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto menu = _ui->_accountToolbox->menu();
|
||||
|
||||
// We can't check this during the initial creation as there is no account yet then
|
||||
if (_accountState->account()->capabilities().clientSideEncryptionAvaliable()) {
|
||||
QAction *mnemonic = new QAction(tr("Show E2E mnemonic"), this);
|
||||
connect(mnemonic, &QAction::triggered, this, &AccountSettings::requesetMnemonic);
|
||||
menu->addAction(mnemonic);
|
||||
}
|
||||
|
||||
_menuShown = true;
|
||||
}
|
||||
|
||||
|
||||
QString AccountSettings::selectedFolderAlias() const
|
||||
{
|
||||
QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
|
||||
|
@ -1060,21 +1016,12 @@ void AccountSettings::slotAccountStateChanged()
|
|||
// sync user interface buttons.
|
||||
refreshSelectiveSyncStatus();
|
||||
|
||||
/* set the correct label for the Account toolbox button */
|
||||
if (_accountState) {
|
||||
if (_accountState->isSignedOut()) {
|
||||
_toggleSignInOutAction->setText(tr("Log in"));
|
||||
} else {
|
||||
_toggleSignInOutAction->setText(tr("Log out"));
|
||||
}
|
||||
}
|
||||
|
||||
if (state == AccountState::State::Connected) {
|
||||
/* TODO: We should probably do something better here.
|
||||
* Verify if the user has a private key already uploaded to the server,
|
||||
* if it has, do not offer to create one.
|
||||
*/
|
||||
qCInfo(lcAccountSettings) << "Accout" << accountsState()->account()->displayName()
|
||||
qCInfo(lcAccountSettings) << "Account" << accountsState()->account()->displayName()
|
||||
<< "Client Side Encryption" << accountsState()->account()->capabilities().clientSideEncryptionAvaliable();
|
||||
}
|
||||
}
|
||||
|
@ -1190,18 +1137,6 @@ void AccountSettings::refreshSelectiveSyncStatus()
|
|||
}
|
||||
}
|
||||
|
||||
void AccountSettings::slotAccountAdded(AccountState *)
|
||||
{
|
||||
// if the theme is limited to single account, the button must hide if
|
||||
// there is already one account.
|
||||
int s = AccountManager::instance()->accounts().size();
|
||||
if (s > 0 && !Theme::instance()->multiAccount()) {
|
||||
_addAccountAction->setVisible(false);
|
||||
} else {
|
||||
_addAccountAction->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void AccountSettings::slotDeleteAccount()
|
||||
{
|
||||
// Deleting the account potentially deletes 'this', so
|
||||
|
|
|
@ -90,7 +90,6 @@ protected slots:
|
|||
void slotDeleteAccount();
|
||||
void slotToggleSignInState();
|
||||
void slotOpenAccountWizard();
|
||||
void slotAccountAdded(AccountState *);
|
||||
void refreshSelectiveSyncStatus();
|
||||
void slotMarkSubfolderEncrypted(const FolderStatusModel::SubFolderInfo* folderInfo);
|
||||
void slotMarkSubfolderDecrypted(const FolderStatusModel::SubFolderInfo* folderInfo);
|
||||
|
@ -100,8 +99,6 @@ protected slots:
|
|||
void doExpand();
|
||||
void slotLinkActivated(const QString &link);
|
||||
|
||||
void slotMenuBeforeShow();
|
||||
|
||||
// Encryption Related Stuff.
|
||||
void slotShowMnemonic(const QString &mnemonic);
|
||||
void slotNewMnemonicGenerated();
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>582</width>
|
||||
<width>581</width>
|
||||
<height>557</height>
|
||||
</rect>
|
||||
</property>
|
||||
|
@ -184,13 +184,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QToolButton" name="_accountToolbox">
|
||||
<property name="text">
|
||||
<string notr="true">...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "creds/httpcredentials.h"
|
||||
#include "logger.h"
|
||||
#include "configfile.h"
|
||||
#include "ocsnavigationappsjob.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
|
@ -27,6 +28,7 @@
|
|||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QNetworkRequest>
|
||||
#include <QBuffer>
|
||||
|
||||
|
@ -40,9 +42,9 @@ AccountState::AccountState(AccountPtr account)
|
|||
, _state(AccountState::Disconnected)
|
||||
, _connectionStatus(ConnectionValidator::Undefined)
|
||||
, _waitingForNewCredentials(false)
|
||||
, _notificationsEtagResponseHeader("*")
|
||||
, _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay
|
||||
, _remoteWipe(new RemoteWipe(_account))
|
||||
, _hasTalk(false)
|
||||
{
|
||||
qRegisterMetaType<AccountState *>("AccountState*");
|
||||
|
||||
|
@ -74,6 +76,11 @@ AccountPtr AccountState::account() const
|
|||
return _account;
|
||||
}
|
||||
|
||||
bool AccountState::hasTalk() const
|
||||
{
|
||||
return _hasTalk;
|
||||
}
|
||||
|
||||
AccountState::ConnectionStatus AccountState::connectionStatus() const
|
||||
{
|
||||
return _connectionStatus;
|
||||
|
@ -237,6 +244,9 @@ void AccountState::checkConnectivity()
|
|||
// Use a small authed propfind as a minimal ping when we're
|
||||
// already connected.
|
||||
conValidator->checkAuthentication();
|
||||
|
||||
// Get the Apps available on the server.
|
||||
fetchNavigationApps();
|
||||
} else {
|
||||
// Check the server and then the auth.
|
||||
|
||||
|
@ -267,7 +277,7 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta
|
|||
// Come online gradually from 503 or maintenance mode
|
||||
if (status == ConnectionValidator::Connected
|
||||
&& (_connectionStatus == ConnectionValidator::ServiceUnavailable
|
||||
|| _connectionStatus == ConnectionValidator::MaintenanceMode)) {
|
||||
|| _connectionStatus == ConnectionValidator::MaintenanceMode)) {
|
||||
if (!_timeSinceMaintenanceOver.isValid()) {
|
||||
qCInfo(lcAccountState) << "AccountState reconnection: delaying for"
|
||||
<< _maintenanceToConnectedDelay << "ms";
|
||||
|
@ -293,6 +303,9 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta
|
|||
case ConnectionValidator::Connected:
|
||||
if (_state != Connected) {
|
||||
setState(Connected);
|
||||
|
||||
// Get the Apps available on the server.
|
||||
fetchNavigationApps();
|
||||
}
|
||||
break;
|
||||
case ConnectionValidator::Undefined:
|
||||
|
@ -405,4 +418,110 @@ std::unique_ptr<QSettings> AccountState::settings()
|
|||
return s;
|
||||
}
|
||||
|
||||
void AccountState::fetchNavigationApps(){
|
||||
OcsNavigationAppsJob *job = new OcsNavigationAppsJob(_account);
|
||||
job->addRawHeader("If-None-Match", navigationAppsEtagResponseHeader());
|
||||
connect(job, &OcsNavigationAppsJob::appsJobFinished, this, &AccountState::slotNavigationAppsFetched);
|
||||
connect(job, &OcsNavigationAppsJob::etagResponseHeaderReceived, this, &AccountState::slotEtagResponseHeaderReceived);
|
||||
connect(job, &OcsNavigationAppsJob::ocsError, this, &AccountState::slotOcsError);
|
||||
job->getNavigationApps();
|
||||
}
|
||||
|
||||
void AccountState::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){
|
||||
if(statusCode == 200){
|
||||
qCDebug(lcAccountState) << "New navigation apps ETag Response Header received " << value;
|
||||
setNavigationAppsEtagResponseHeader(value);
|
||||
}
|
||||
}
|
||||
|
||||
void AccountState::slotOcsError(int statusCode, const QString &message)
|
||||
{
|
||||
qCDebug(lcAccountState) << "Error " << statusCode << " while fetching new navigation apps: " << message;
|
||||
}
|
||||
|
||||
void AccountState::slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode)
|
||||
{
|
||||
if(_account){
|
||||
if (statusCode == 304) {
|
||||
qCWarning(lcAccountState) << "Status code " << statusCode << " Not Modified - No new navigation apps.";
|
||||
} else {
|
||||
_apps.clear();
|
||||
_hasTalk = false;
|
||||
|
||||
if(!reply.isEmpty()){
|
||||
auto element = reply.object().value("ocs").toObject().value("data");
|
||||
auto navLinks = element.toArray();
|
||||
|
||||
if(navLinks.size() > 0){
|
||||
foreach (const QJsonValue &value, navLinks) {
|
||||
auto navLink = value.toObject();
|
||||
|
||||
AccountApp *app = new AccountApp(navLink.value("name").toString(), QUrl(navLink.value("href").toString()),
|
||||
navLink.value("id").toString(), QUrl(navLink.value("icon").toString()));
|
||||
|
||||
_apps << app;
|
||||
|
||||
if(app->id() == QLatin1String("spreed"))
|
||||
_hasTalk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit hasFetchedNavigationApps();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccountAppList AccountState::appList() const
|
||||
{
|
||||
return _apps;
|
||||
}
|
||||
|
||||
AccountApp* AccountState::findApp(const QString &appId) const
|
||||
{
|
||||
if(!appId.isEmpty()) {
|
||||
foreach(AccountApp *app, appList()) {
|
||||
if(app->id() == appId)
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------------------*/
|
||||
|
||||
AccountApp::AccountApp(const QString &name, const QUrl &url,
|
||||
const QString &id, const QUrl &iconUrl,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, _name(name)
|
||||
, _url(url)
|
||||
, _id(id)
|
||||
, _iconUrl(iconUrl)
|
||||
{
|
||||
}
|
||||
|
||||
QString AccountApp::name() const
|
||||
{
|
||||
return _name;
|
||||
}
|
||||
|
||||
QUrl AccountApp::url() const
|
||||
{
|
||||
return _url;
|
||||
}
|
||||
|
||||
QString AccountApp::id() const
|
||||
{
|
||||
return _id;
|
||||
}
|
||||
|
||||
QUrl AccountApp::iconUrl() const
|
||||
{
|
||||
return _iconUrl;
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------------------*/
|
||||
|
||||
} // namespace OCC
|
||||
|
|
|
@ -29,9 +29,11 @@ namespace OCC {
|
|||
|
||||
class AccountState;
|
||||
class Account;
|
||||
class AccountApp;
|
||||
class RemoteWipe;
|
||||
|
||||
typedef QExplicitlySharedDataPointer<AccountState> AccountStatePtr;
|
||||
typedef QList<AccountApp*> AccountAppList;
|
||||
|
||||
/**
|
||||
* @brief Extra info about an ownCloud server account.
|
||||
|
@ -101,6 +103,11 @@ public:
|
|||
|
||||
bool isSignedOut() const;
|
||||
|
||||
bool hasTalk() const;
|
||||
|
||||
AccountAppList appList() const;
|
||||
AccountApp* findApp(const QString &appId) const;
|
||||
|
||||
/** A user-triggered sign out which disconnects, stops syncs
|
||||
* for the account and forgets the password. */
|
||||
void signOutByUi();
|
||||
|
@ -161,10 +168,12 @@ public slots:
|
|||
|
||||
private:
|
||||
void setState(State state);
|
||||
void fetchNavigationApps();
|
||||
|
||||
signals:
|
||||
void stateChanged(int state);
|
||||
void isConnectedChanged();
|
||||
void hasFetchedNavigationApps();
|
||||
|
||||
protected Q_SLOTS:
|
||||
void slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors);
|
||||
|
@ -176,12 +185,17 @@ protected Q_SLOTS:
|
|||
void slotCredentialsFetched(AbstractCredentials *creds);
|
||||
void slotCredentialsAsked(AbstractCredentials *creds);
|
||||
|
||||
void slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode);
|
||||
void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
|
||||
void slotOcsError(int statusCode, const QString &message);
|
||||
|
||||
private:
|
||||
AccountPtr _account;
|
||||
State _state;
|
||||
ConnectionStatus _connectionStatus;
|
||||
QStringList _connectionErrors;
|
||||
bool _waitingForNewCredentials;
|
||||
bool _hasTalk;
|
||||
QElapsedTimer _timeSinceLastETagCheck;
|
||||
QPointer<ConnectionValidator> _connectionValidator;
|
||||
QByteArray _notificationsEtagResponseHeader;
|
||||
|
@ -205,7 +219,34 @@ private:
|
|||
*/
|
||||
RemoteWipe *_remoteWipe;
|
||||
|
||||
/**
|
||||
* Holds the App names and URLs available on the server
|
||||
*/
|
||||
AccountAppList _apps;
|
||||
|
||||
};
|
||||
|
||||
class AccountApp : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AccountApp(const QString &name, const QUrl &url,
|
||||
const QString &id, const QUrl &iconUrl,
|
||||
QObject* parent = 0);
|
||||
|
||||
QString name() const;
|
||||
QUrl url() const;
|
||||
QString id() const;
|
||||
QUrl iconUrl() const;
|
||||
|
||||
private:
|
||||
QString _name;
|
||||
QUrl _url;
|
||||
|
||||
QString _id;
|
||||
QUrl _iconUrl;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::AccountState *)
|
||||
|
|
|
@ -1,407 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
* 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 "activityitemdelegate.h"
|
||||
#include "activitylistmodel.h"
|
||||
#include "folderstatusmodel.h"
|
||||
#include "folderman.h"
|
||||
#include "accountstate.h"
|
||||
#include "activitydata.h"
|
||||
#include <theme.h>
|
||||
#include <account.h>
|
||||
|
||||
#include <QFileIconProvider>
|
||||
#include <QPainter>
|
||||
#include <QApplication>
|
||||
|
||||
#define FIXME_USE_HIGH_DPI_RATIO
|
||||
#ifdef FIXME_USE_HIGH_DPI_RATIO
|
||||
// FIXME: Find a better way to calculate the text width on high-dpi displays (Retina).
|
||||
#include <QDesktopWidget>
|
||||
#endif
|
||||
|
||||
#define HASQT5_11 (QT_VERSION >= QT_VERSION_CHECK(5,11,0))
|
||||
|
||||
namespace OCC {
|
||||
|
||||
int ActivityItemDelegate::_iconHeight = 0;
|
||||
int ActivityItemDelegate::_margin = 0;
|
||||
int ActivityItemDelegate::_primaryButtonWidth = 0;
|
||||
int ActivityItemDelegate::_secondaryButtonWidth = 0;
|
||||
int ActivityItemDelegate::_spaceBetweenButtons = 0;
|
||||
int ActivityItemDelegate::_timeWidth = 0;
|
||||
int ActivityItemDelegate::_buttonHeight = 0;
|
||||
const QString ActivityItemDelegate::_remote_share("remote_share");
|
||||
const QString ActivityItemDelegate::_call("call");
|
||||
|
||||
ActivityItemDelegate::ActivityItemDelegate()
|
||||
: QStyledItemDelegate()
|
||||
{
|
||||
customizeStyle();
|
||||
}
|
||||
|
||||
int ActivityItemDelegate::iconHeight()
|
||||
{
|
||||
if (_iconHeight == 0) {
|
||||
QStyleOptionViewItem option;
|
||||
QFont font = option.font;
|
||||
|
||||
QFontMetrics fm(font);
|
||||
|
||||
_iconHeight = qRound(fm.height() / 5.0 * 8.0);
|
||||
}
|
||||
return _iconHeight;
|
||||
}
|
||||
|
||||
int ActivityItemDelegate::rowHeight()
|
||||
{
|
||||
if (_margin == 0) {
|
||||
QStyleOptionViewItem opt;
|
||||
|
||||
QFont f = opt.font;
|
||||
QFontMetrics fm(f);
|
||||
|
||||
_margin = fm.height() / 2;
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
_margin += 5;
|
||||
#endif
|
||||
}
|
||||
return iconHeight() + 5 * _margin;
|
||||
}
|
||||
|
||||
QSize ActivityItemDelegate::sizeHint(const QStyleOptionViewItem &option,
|
||||
const QModelIndex & /* index */) const
|
||||
{
|
||||
QFont font = option.font;
|
||||
|
||||
return QSize(0, rowHeight());
|
||||
}
|
||||
|
||||
void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const
|
||||
{
|
||||
QStyledItemDelegate::paint(painter, option, index);
|
||||
QFont font = option.font;
|
||||
QFontMetrics fm(font);
|
||||
int margin = fm.height() / 2.5;
|
||||
painter->save();
|
||||
int iconSize = 16;
|
||||
int iconOffset = qRound(fm.height() / 4.0 * 7.0);
|
||||
int offset = 4;
|
||||
const bool isSelected = (option.state & QStyle::State_Selected);
|
||||
#ifdef FIXME_USE_HIGH_DPI_RATIO
|
||||
// FIXME: Find a better way to calculate the text width on high-dpi displays (Retina).
|
||||
const int device_pixel_ration = QApplication::desktop()->devicePixelRatio();
|
||||
int pixel_ratio = (device_pixel_ration > 1 ? device_pixel_ration : 1);
|
||||
#endif
|
||||
|
||||
// get the data
|
||||
Activity::Type activityType = qvariant_cast<Activity::Type>(index.data(ActionRole));
|
||||
QIcon actionIcon;
|
||||
const ActivityListModel::ActionIcon icn = qvariant_cast<ActivityListModel::ActionIcon>(index.data(ActionIconRole));
|
||||
switch(icn.iconType) {
|
||||
case ActivityListModel::ActivityIconType::iconUseCached: actionIcon = icn.cachedIcon; break;
|
||||
case ActivityListModel::ActivityIconType::iconActivity: actionIcon = (isSelected ? _iconActivity_sel : _iconActivity); break;
|
||||
case ActivityListModel::ActivityIconType::iconBell: actionIcon = (isSelected ? _iconBell_sel : _iconBell); break;
|
||||
case ActivityListModel::ActivityIconType::iconStateError: actionIcon = _iconStateError; break;
|
||||
case ActivityListModel::ActivityIconType::iconStateWarning: actionIcon = _iconStateWarning; break;
|
||||
case ActivityListModel::ActivityIconType::iconStateInfo: actionIcon = _iconStateInfo; break;
|
||||
case ActivityListModel::ActivityIconType::iconStateSync: actionIcon = _iconStateSync; break;
|
||||
}
|
||||
QString objectType = qvariant_cast<QString>(index.data(ObjectTypeRole));
|
||||
QString actionText = qvariant_cast<QString>(index.data(ActionTextRole));
|
||||
QString messageText = qvariant_cast<QString>(index.data(MessageRole));
|
||||
QList<QVariant> customList = index.data(ActionsLinksRole).toList();
|
||||
QString timeText = qvariant_cast<QString>(index.data(PointInTimeRole));
|
||||
bool accountOnline = qvariant_cast<bool>(index.data(AccountConnectedRole));
|
||||
|
||||
// activity/notification icons
|
||||
QRect actionIconRect = option.rect;
|
||||
actionIconRect.setLeft(option.rect.left() + iconOffset/3);
|
||||
actionIconRect.setRight(option.rect.left() + iconOffset);
|
||||
actionIconRect.setTop(option.rect.top() + qRound((option.rect.height() - 16)/3.0));
|
||||
|
||||
// subject text rect
|
||||
QRect actionTextBox = actionIconRect;
|
||||
#if (HASQT5_11)
|
||||
int actionTextBoxWidth = fm.horizontalAdvance(actionText);
|
||||
#else
|
||||
int actionTextBoxWidth = fm.width(actionText);
|
||||
#endif
|
||||
actionTextBox.setTop(option.rect.top() + margin + offset/2);
|
||||
actionTextBox.setHeight(fm.height());
|
||||
actionTextBox.setLeft(actionIconRect.right() + margin);
|
||||
#ifdef FIXME_USE_HIGH_DPI_RATIO
|
||||
// FIXME: Find a better way to calculate the text width on high-dpi displays (Retina).
|
||||
actionTextBoxWidth *= pixel_ratio;
|
||||
#endif
|
||||
actionTextBox.setRight(actionTextBox.left() + actionTextBoxWidth + margin);
|
||||
|
||||
// message text rect
|
||||
QRect messageTextBox = actionTextBox;
|
||||
#if (HASQT5_11)
|
||||
int messageTextWidth = fm.horizontalAdvance(messageText);
|
||||
#else
|
||||
int messageTextWidth = fm.width(messageText);
|
||||
#endif
|
||||
int messageTextTop = option.rect.top() + fm.height() + margin;
|
||||
if(actionText.isEmpty()) messageTextTop = option.rect.top() + margin + offset/2;
|
||||
messageTextBox.setTop(messageTextTop);
|
||||
messageTextBox.setHeight(fm.height());
|
||||
messageTextBox.setBottom(messageTextBox.top() + fm.height());
|
||||
messageTextBox.setRight(messageTextBox.left() + messageTextWidth + margin);
|
||||
if(messageText.isEmpty()){
|
||||
messageTextBox.setHeight(0);
|
||||
messageTextBox.setBottom(messageTextBox.top());
|
||||
}
|
||||
|
||||
// time box rect
|
||||
QRect timeBox = messageTextBox;
|
||||
#if (HASQT5_11)
|
||||
int timeTextWidth = fm.horizontalAdvance(timeText);
|
||||
#else
|
||||
int timeTextWidth = fm.width(timeText);
|
||||
#endif
|
||||
int timeTop = option.rect.top() + fm.height() + fm.height() + margin + offset/2;
|
||||
if(messageText.isEmpty() || actionText.isEmpty())
|
||||
timeTop = option.rect.top() + fm.height() + margin;
|
||||
timeBox.setTop(timeTop);
|
||||
timeBox.setHeight(fm.height());
|
||||
timeBox.setBottom(timeBox.top() + fm.height());
|
||||
#ifdef FIXME_USE_HIGH_DPI_RATIO
|
||||
// FIXME: Find a better way to calculate the text width on high-dpi displays (Retina).
|
||||
timeTextWidth *= pixel_ratio;
|
||||
#endif
|
||||
timeBox.setRight(timeBox.left() + timeTextWidth + margin);
|
||||
|
||||
// buttons - default values
|
||||
int rightMargin = margin;
|
||||
int leftMargin = margin * offset;
|
||||
int top = option.rect.top() + margin;
|
||||
int buttonSize = option.rect.height()/2;
|
||||
int right = option.rect.right() - rightMargin;
|
||||
int left = right - buttonSize;
|
||||
|
||||
QStyleOptionButton secondaryButton;
|
||||
secondaryButton.rect = option.rect;
|
||||
secondaryButton.features |= QStyleOptionButton::Flat;
|
||||
secondaryButton.state |= QStyle::State_None;
|
||||
secondaryButton.rect.setLeft(left);
|
||||
secondaryButton.rect.setRight(right);
|
||||
secondaryButton.rect.setTop(top + margin);
|
||||
secondaryButton.rect.setHeight(iconSize);
|
||||
|
||||
QStyleOptionButton primaryButton;
|
||||
primaryButton.rect = option.rect;
|
||||
primaryButton.features |= QStyleOptionButton::DefaultButton;
|
||||
primaryButton.state |= QStyle::State_Raised;
|
||||
primaryButton.rect.setTop(top);
|
||||
primaryButton.rect.setHeight(buttonSize);
|
||||
|
||||
right = secondaryButton.rect.left() - rightMargin;
|
||||
left = secondaryButton.rect.left() - leftMargin;
|
||||
|
||||
primaryButton.rect.setRight(right);
|
||||
|
||||
if(activityType == Activity::Type::NotificationType){
|
||||
|
||||
// Secondary will be 'Dismiss' or '...' multiple options button
|
||||
secondaryButton.icon = (isSelected ? _iconClose_sel : _iconClose);
|
||||
if(customList.size() > 1)
|
||||
secondaryButton.icon = (isSelected ? _iconMore_sel : _iconMore);
|
||||
secondaryButton.iconSize = QSize(iconSize, iconSize);
|
||||
|
||||
// Primary button will be 'More Information' or 'Accept'
|
||||
primaryButton.text = tr("More information");
|
||||
if(objectType == _remote_share) primaryButton.text = tr("Accept");
|
||||
if(objectType == _call) primaryButton.text = tr("Join");
|
||||
|
||||
#if (HASQT5_11)
|
||||
primaryButton.rect.setLeft(left - margin * 2 - fm.horizontalAdvance(primaryButton.text));
|
||||
#else
|
||||
primaryButton.rect.setLeft(left - margin * 2 - fm.width(primaryButton.text));
|
||||
#endif
|
||||
|
||||
// save info to be able to filter mouse clicks
|
||||
_buttonHeight = buttonSize;
|
||||
_primaryButtonWidth = primaryButton.rect.size().width();
|
||||
_secondaryButtonWidth = secondaryButton.rect.size().width();
|
||||
_spaceBetweenButtons = secondaryButton.rect.left() - primaryButton.rect.right();
|
||||
|
||||
} else if(activityType == Activity::SyncResultType){
|
||||
|
||||
// Secondary will be 'open file manager' with the folder icon
|
||||
secondaryButton.icon = _iconFolder;
|
||||
secondaryButton.iconSize = QSize(iconSize, iconSize);
|
||||
|
||||
// Primary button will be 'open browser'
|
||||
primaryButton.text = tr("Open Browser");
|
||||
|
||||
#if (HASQT5_11)
|
||||
primaryButton.rect.setLeft(left - margin * 2 - fm.horizontalAdvance(primaryButton.text));
|
||||
#else
|
||||
primaryButton.rect.setLeft(left - margin * 2 - fm.width(primaryButton.text));
|
||||
#endif
|
||||
|
||||
// save info to be able to filter mouse clicks
|
||||
_buttonHeight = buttonSize;
|
||||
_primaryButtonWidth = primaryButton.rect.size().width();
|
||||
_secondaryButtonWidth = secondaryButton.rect.size().width();
|
||||
_spaceBetweenButtons = secondaryButton.rect.left() - primaryButton.rect.right();
|
||||
|
||||
} else if(activityType == Activity::SyncFileItemType){
|
||||
|
||||
// Secondary will be 'open file manager' with the folder icon
|
||||
secondaryButton.icon = _iconFolder;
|
||||
secondaryButton.iconSize = QSize(iconSize, iconSize);
|
||||
|
||||
// No primary button on this case
|
||||
// Whatever error we have at this case it is local, there is no point on opening the browser
|
||||
_primaryButtonWidth = 0;
|
||||
_secondaryButtonWidth = secondaryButton.rect.size().width();
|
||||
_spaceBetweenButtons = secondaryButton.rect.left() - primaryButton.rect.right();
|
||||
|
||||
} else {
|
||||
_spaceBetweenButtons = leftMargin;
|
||||
_primaryButtonWidth = 0;
|
||||
_secondaryButtonWidth = 0;
|
||||
}
|
||||
|
||||
// draw the icon
|
||||
QPixmap pm = actionIcon.pixmap(iconSize, iconSize, QIcon::Normal);
|
||||
painter->drawPixmap(QPoint(actionIconRect.left(), actionIconRect.top()), pm);
|
||||
|
||||
// change pen color if use is not online
|
||||
QPalette p = option.palette;
|
||||
if(!accountOnline)
|
||||
p.setCurrentColorGroup(QPalette::Disabled);
|
||||
|
||||
// change pen color if the line is selected
|
||||
if (isSelected)
|
||||
painter->setPen(p.color(QPalette::HighlightedText));
|
||||
else
|
||||
painter->setPen(p.color(QPalette::Text));
|
||||
|
||||
// calculate space for text - use the max possible before using the elipses
|
||||
int spaceLeftForText = option.rect.width() - (actionIconRect.width() + margin + rightMargin + leftMargin) -
|
||||
(_primaryButtonWidth + _secondaryButtonWidth + _spaceBetweenButtons);
|
||||
|
||||
// draw the subject
|
||||
const QString elidedAction = fm.elidedText(actionText, Qt::ElideRight, spaceLeftForText);
|
||||
painter->drawText(actionTextBox, elidedAction);
|
||||
|
||||
// draw the buttons
|
||||
if(activityType == Activity::Type::NotificationType || activityType == Activity::Type::SyncResultType) {
|
||||
primaryButton.palette = p;
|
||||
if (isSelected)
|
||||
primaryButton.palette.setColor(QPalette::ButtonText, p.color(QPalette::HighlightedText));
|
||||
else
|
||||
primaryButton.palette.setColor(QPalette::ButtonText, p.color(QPalette::Text));
|
||||
|
||||
QApplication::style()->drawControl(QStyle::CE_PushButton, &primaryButton, painter);
|
||||
}
|
||||
|
||||
// Since they are errors on local syncing, there is nothing to do in the server
|
||||
if(activityType != Activity::Type::ActivityType)
|
||||
QApplication::style()->drawControl(QStyle::CE_PushButton, &secondaryButton, painter);
|
||||
|
||||
// draw the message
|
||||
// change pen color for the message
|
||||
if(!messageText.isEmpty()){
|
||||
const QString elidedMessage = fm.elidedText(messageText, Qt::ElideRight, spaceLeftForText);
|
||||
painter->drawText(messageTextBox, elidedMessage);
|
||||
}
|
||||
|
||||
// change pen color for the time
|
||||
if (isSelected)
|
||||
painter->setPen(p.color(QPalette::Disabled, QPalette::HighlightedText));
|
||||
else
|
||||
painter->setPen(p.color(QPalette::Disabled, QPalette::Text));
|
||||
|
||||
// draw the time
|
||||
const QString elidedTime = fm.elidedText(timeText, Qt::ElideRight, spaceLeftForText);
|
||||
painter->drawText(timeBox, elidedTime);
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
bool ActivityItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
const QStyleOptionViewItem &option, const QModelIndex &index)
|
||||
{
|
||||
Activity::Type activityType = qvariant_cast<Activity::Type>(index.data(ActionRole));
|
||||
if(activityType != Activity::Type::ActivityType){
|
||||
if (event->type() == QEvent::MouseButtonRelease){
|
||||
QMouseEvent *mouseEvent = (QMouseEvent*)event;
|
||||
if(mouseEvent){
|
||||
int mouseEventX = mouseEvent->x();
|
||||
int mouseEventY = mouseEvent->y();
|
||||
int buttonsWidth = _primaryButtonWidth + _spaceBetweenButtons + _secondaryButtonWidth;
|
||||
int x = option.rect.left() + option.rect.width() - buttonsWidth - _timeWidth;
|
||||
int y = option.rect.top();
|
||||
|
||||
// clickable area for ...
|
||||
if (mouseEventX > x && mouseEventX < x + buttonsWidth){
|
||||
if(mouseEventY > y && mouseEventY < y + _buttonHeight){
|
||||
|
||||
// ...primary button ('more information' or 'accept' on notifications or 'open browser' on errors)
|
||||
if (mouseEventX > x && mouseEventX < x + _primaryButtonWidth){
|
||||
emit primaryButtonClickedOnItemView(index);
|
||||
|
||||
// ...secondary button ('dismiss' on notifications or 'open file manager' on errors)
|
||||
} else {
|
||||
x += _primaryButtonWidth + _spaceBetweenButtons;
|
||||
if (mouseEventX > x && mouseEventX < x + _secondaryButtonWidth)
|
||||
emit secondaryButtonClickedOnItemView(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QStyledItemDelegate::editorEvent(event, model, option, index);
|
||||
}
|
||||
|
||||
void ActivityItemDelegate::slotStyleChanged()
|
||||
{
|
||||
customizeStyle();
|
||||
}
|
||||
|
||||
void ActivityItemDelegate::customizeStyle()
|
||||
{
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Base, QColor(0,0,0)); // use dark background colour to invert icons
|
||||
|
||||
_iconClose = Theme::createColorAwareIcon(QLatin1String(":/client/resources/close.svg"));
|
||||
_iconClose_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/close.svg"), pal);
|
||||
_iconMore = Theme::createColorAwareIcon(QLatin1String(":/client/resources/more.svg"));
|
||||
_iconMore_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/more.svg"), pal);
|
||||
|
||||
_iconFolder = QIcon(QLatin1String(":/client/resources/folder.svg"));
|
||||
|
||||
_iconActivity = Theme::createColorAwareIcon(QLatin1String(":/client/resources/activity.png"));
|
||||
_iconActivity_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/activity.png"), pal);
|
||||
_iconBell = Theme::createColorAwareIcon(QLatin1String(":/client/resources/bell.svg"));
|
||||
_iconBell_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/bell.svg"), pal);
|
||||
|
||||
_iconStateError = QIcon(QLatin1String(":/client/resources/state-error.svg"));
|
||||
_iconStateWarning = QIcon(QLatin1String(":/client/resources/state-warning.svg"));
|
||||
_iconStateInfo = QIcon(QLatin1String(":/client/resources/state-info.svg"));
|
||||
_iconStateSync = QIcon(QLatin1String(":/client/resources/state-sync.svg"));
|
||||
}
|
||||
|
||||
} // namespace OCC
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* 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 <QStyledItemDelegate>
|
||||
#include <QMouseEvent>
|
||||
|
||||
class QMouseEvent;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* @brief The ActivityItemDelegate class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class ActivityItemDelegate : public QStyledItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum datarole { ActionIconRole = Qt::UserRole + 1,
|
||||
UserIconRole,
|
||||
AccountRole,
|
||||
ObjectTypeRole,
|
||||
ActionsLinksRole,
|
||||
ActionTextRole,
|
||||
ActionRole,
|
||||
MessageRole,
|
||||
PathRole,
|
||||
LinkRole,
|
||||
PointInTimeRole,
|
||||
AccountConnectedRole,
|
||||
SyncFileStatusRole };
|
||||
|
||||
ActivityItemDelegate();
|
||||
|
||||
void paint(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override;
|
||||
bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) override;
|
||||
|
||||
static int rowHeight();
|
||||
static int iconHeight();
|
||||
|
||||
public slots:
|
||||
void slotStyleChanged();
|
||||
|
||||
signals:
|
||||
void primaryButtonClickedOnItemView(const QModelIndex &index);
|
||||
void secondaryButtonClickedOnItemView(const QModelIndex &index);
|
||||
|
||||
private:
|
||||
void customizeStyle();
|
||||
|
||||
static int _margin;
|
||||
static int _iconHeight;
|
||||
static int _primaryButtonWidth;
|
||||
static int _secondaryButtonWidth;
|
||||
static int _spaceBetweenButtons;
|
||||
static int _timeWidth;
|
||||
static int _buttonHeight;
|
||||
static const QString _remote_share;
|
||||
static const QString _call;
|
||||
|
||||
QIcon _iconClose;
|
||||
QIcon _iconClose_sel;
|
||||
QIcon _iconMore;
|
||||
QIcon _iconMore_sel;
|
||||
|
||||
QIcon _iconFolder;
|
||||
|
||||
QIcon _iconActivity;
|
||||
QIcon _iconActivity_sel;
|
||||
QIcon _iconBell;
|
||||
QIcon _iconBell_sel;
|
||||
|
||||
QIcon _iconStateError;
|
||||
QIcon _iconStateWarning;
|
||||
QIcon _iconStateInfo;
|
||||
QIcon _iconStateSync;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
|
@ -1,653 +0,0 @@
|
|||
/*
|
||||
* 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 "activitylistmodel.h"
|
||||
#include "activitywidget.h"
|
||||
#include "syncresult.h"
|
||||
#include "logger.h"
|
||||
#include "theme.h"
|
||||
#include "folderman.h"
|
||||
#include "syncfileitem.h"
|
||||
#include "folder.h"
|
||||
#include "openfilemanager.h"
|
||||
#include "owncloudpropagator.h"
|
||||
#include "account.h"
|
||||
#include "accountstate.h"
|
||||
#include "accountmanager.h"
|
||||
#include "activityitemdelegate.h"
|
||||
#include "QProgressIndicator.h"
|
||||
#include "notificationconfirmjob.h"
|
||||
#include "servernotificationhandler.h"
|
||||
#include "theme.h"
|
||||
#include "ocsjob.h"
|
||||
#include "configfile.h"
|
||||
#include "guiutility.h"
|
||||
#include "socketapi.h"
|
||||
#include "ui_activitywidget.h"
|
||||
#include "syncengine.h"
|
||||
|
||||
#include <climits>
|
||||
|
||||
// time span in milliseconds which has to be between two
|
||||
// refreshes of the notifications
|
||||
#define NOTIFICATION_REQUEST_FREE_PERIOD 15000
|
||||
|
||||
namespace OCC {
|
||||
|
||||
ActivityWidget::ActivityWidget(AccountState *accountState, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, _ui(new Ui::ActivityWidget)
|
||||
, _notificationRequestsRunning(0)
|
||||
, _accountState(accountState)
|
||||
, _accept(tr("Accept"))
|
||||
, _remote_share("remote_share")
|
||||
{
|
||||
_ui->setupUi(this);
|
||||
|
||||
// Adjust copyToClipboard() when making changes here!
|
||||
#if defined(Q_OS_MAC)
|
||||
_ui->_activityList->setMinimumWidth(400);
|
||||
#endif
|
||||
|
||||
_model = new ActivityListModel(accountState, this);
|
||||
ActivityItemDelegate *delegate = new ActivityItemDelegate;
|
||||
delegate->setParent(this);
|
||||
_ui->_activityList->setItemDelegate(delegate);
|
||||
_ui->_activityList->setAlternatingRowColors(true);
|
||||
_ui->_activityList->setModel(_model);
|
||||
|
||||
showLabels();
|
||||
|
||||
connect(_model, &ActivityListModel::activityJobStatusCode,
|
||||
this, &ActivityWidget::slotAccountActivityStatus);
|
||||
|
||||
connect(_model, &QAbstractItemModel::rowsInserted, this, &ActivityWidget::rowsInserted);
|
||||
|
||||
connect(delegate, &ActivityItemDelegate::primaryButtonClickedOnItemView, this, &ActivityWidget::slotPrimaryButtonClickedOnListView);
|
||||
connect(delegate, &ActivityItemDelegate::secondaryButtonClickedOnItemView, this, &ActivityWidget::slotSecondaryButtonClickedOnListView);
|
||||
connect(_ui->_activityList, &QListView::activated, this, &ActivityWidget::slotOpenFile);
|
||||
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
|
||||
this, &ActivityWidget::slotProgressInfo);
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
|
||||
this, &ActivityWidget::slotItemCompleted);
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
|
||||
this, &ActivityWidget::addError);
|
||||
|
||||
_removeTimer.setInterval(1000);
|
||||
|
||||
// Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching)
|
||||
connect(this, &ActivityWidget::styleChanged, delegate, &ActivityItemDelegate::slotStyleChanged);
|
||||
}
|
||||
|
||||
ActivityWidget::~ActivityWidget()
|
||||
{
|
||||
delete _ui;
|
||||
}
|
||||
|
||||
void ActivityWidget::slotProgressInfo(const QString &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.
|
||||
auto f = FolderMan::instance()->folder(folder);
|
||||
if (!f)
|
||||
return;
|
||||
const auto &engine = f->syncEngine();
|
||||
const auto style = engine.lastLocalDiscoveryStyle();
|
||||
foreach (Activity activity, _model->errorsList()) {
|
||||
if (activity._folder != folder){
|
||||
continue;
|
||||
}
|
||||
|
||||
if (style == LocalDiscoveryStyle::FilesystemOnly){
|
||||
_model->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(activity._status == SyncFileItem::Conflict && !QFileInfo(f->path() + activity._file).exists()){
|
||||
_model->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(activity._status == SyncFileItem::FileLocked && !QFileInfo(f->path() + activity._file).exists()){
|
||||
_model->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if(activity._status == SyncFileItem::FileIgnored && !QFileInfo(f->path() + activity._file).exists()) {
|
||||
_model->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if(!QFileInfo(f->path() + activity._file).exists()){
|
||||
_model->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto path = QFileInfo(activity._file).dir().path().toUtf8();
|
||||
if (path == ".")
|
||||
path.clear();
|
||||
|
||||
if(engine.shouldDiscoverLocally(path))
|
||||
_model->removeActivityFromActivityList(activity);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (progress.status() == ProgressInfo::Done) {
|
||||
// We keep track very well of pending conflicts.
|
||||
// Inform other components about them.
|
||||
QStringList conflicts;
|
||||
foreach (Activity activity, _model->errorsList()) {
|
||||
if (activity._folder == folder
|
||||
&& activity._status == SyncFileItem::Conflict) {
|
||||
conflicts.append(activity._file);
|
||||
}
|
||||
}
|
||||
|
||||
emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts);
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWidget::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item){
|
||||
auto folderInstance = FolderMan::instance()->folder(folder);
|
||||
|
||||
if (!folderInstance)
|
||||
return;
|
||||
|
||||
// check if we are adding it to the right account and if it is useful information (protocol errors)
|
||||
if(folderInstance->accountState() == _accountState){
|
||||
qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in " << item->_errorString;
|
||||
|
||||
Activity activity;
|
||||
activity._type = Activity::SyncFileItemType; //client activity
|
||||
activity._status = item->_status;
|
||||
activity._dateTime = QDateTime::currentDateTime();
|
||||
activity._message = item->_originalFile;
|
||||
activity._link = folderInstance->accountState()->account()->url();
|
||||
activity._accName = folderInstance->accountState()->account()->displayName();
|
||||
activity._file = item->_file;
|
||||
activity._folder = folder;
|
||||
|
||||
if(item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success){
|
||||
qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully.";
|
||||
activity._message.prepend(" ");
|
||||
activity._message.prepend(tr("Synced"));
|
||||
_model->addSyncFileItemToActivityList(activity);
|
||||
} else {
|
||||
qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString;
|
||||
activity._subject = item->_errorString;
|
||||
|
||||
if(item->_status == SyncFileItem::Status::FileIgnored) {
|
||||
_model->addIgnoredFileToList(activity);
|
||||
} else {
|
||||
// add 'protocol error' to activity list
|
||||
_model->addErrorToActivityList(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWidget::addError(const QString &folderAlias, const QString &message,
|
||||
ErrorCategory category)
|
||||
{
|
||||
auto folderInstance = FolderMan::instance()->folder(folderAlias);
|
||||
if (!folderInstance)
|
||||
return;
|
||||
|
||||
if(folderInstance->accountState() == _accountState){
|
||||
qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << message;
|
||||
|
||||
Activity activity;
|
||||
activity._type = Activity::SyncResultType;
|
||||
activity._status = SyncResult::Error;
|
||||
activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate);
|
||||
activity._subject = message;
|
||||
activity._message = folderInstance->shortGuiLocalPath();
|
||||
activity._link = folderInstance->shortGuiLocalPath();
|
||||
activity._accName = folderInstance->accountState()->account()->displayName();
|
||||
activity._folder = folderAlias;
|
||||
|
||||
|
||||
if (category == ErrorCategory::InsufficientRemoteStorage) {
|
||||
ActivityLink link;
|
||||
link._label = tr("Retry all uploads");
|
||||
link._link = folderInstance->path();
|
||||
link._verb = "";
|
||||
link._isPrimary = true;
|
||||
activity._links.append(link);
|
||||
}
|
||||
|
||||
// add 'other errors' to activity list
|
||||
_model->addErrorToActivityList(activity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void ActivityWidget::slotPrimaryButtonClickedOnListView(const QModelIndex &index){
|
||||
QUrl link = qvariant_cast<QString>(index.data(ActivityItemDelegate::LinkRole));
|
||||
QString objectType = index.data(ActivityItemDelegate::ObjectTypeRole).toString();
|
||||
if(!link.isEmpty()){
|
||||
qCWarning(lcActivity) << "Opening" << link.toString() << "in browser for Notification/Activity" << qvariant_cast<QString>(index.data(ActivityItemDelegate::ActionTextRole));
|
||||
Utility::openBrowser(link, this);
|
||||
} else if(objectType == _remote_share){
|
||||
QVariant customItem = index.data(ActivityItemDelegate::ActionsLinksRole).toList().first();
|
||||
ActivityLink actionLink = qvariant_cast<ActivityLink>(customItem);
|
||||
if(actionLink._label == _accept){
|
||||
qCWarning(lcActivity) << objectType << "action" << actionLink._label << "for" << qvariant_cast<QString>(index.data(ActivityItemDelegate::ActionTextRole));
|
||||
const QString accountName = index.data(ActivityItemDelegate::AccountRole).toString();
|
||||
slotSendNotificationRequest(accountName, actionLink._link, actionLink._verb, index.row());
|
||||
} else {
|
||||
qCWarning(lcActivity) << "Failed: " << objectType << "action" << actionLink._label << "for" << qvariant_cast<QString>(index.data(ActivityItemDelegate::ActionTextRole));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWidget::slotSecondaryButtonClickedOnListView(const QModelIndex &index){
|
||||
QList<QVariant> customList = index.data(ActivityItemDelegate::ActionsLinksRole).toList();
|
||||
QString objectType = index.data(ActivityItemDelegate::ObjectTypeRole).toString();
|
||||
|
||||
QList<ActivityLink> actionLinks;
|
||||
foreach(QVariant customItem, customList){
|
||||
actionLinks << qvariant_cast<ActivityLink>(customItem);
|
||||
}
|
||||
|
||||
if(objectType == _remote_share && actionLinks.first()._label == _accept)
|
||||
actionLinks.removeFirst();
|
||||
|
||||
if(qvariant_cast<Activity::Type>(index.data(ActivityItemDelegate::ActionRole)) == Activity::Type::NotificationType){
|
||||
const QString accountName = index.data(ActivityItemDelegate::AccountRole).toString();
|
||||
if(actionLinks.size() == 1){
|
||||
if(actionLinks.at(0)._verb == "DELETE"){
|
||||
qCWarning(lcActivity) << "Dismissing Notification/Activity" << qvariant_cast<QString>(index.data(ActivityItemDelegate::ActionTextRole));
|
||||
slotSendNotificationRequest(index.data(ActivityItemDelegate::AccountRole).toString(), actionLinks.at(0)._link, actionLinks.at(0)._verb, index.row());
|
||||
}
|
||||
} else if(actionLinks.size() > 1){
|
||||
QMenu menu;
|
||||
qCWarning(lcActivity) << "Displaying menu for Notification/Activity" << qvariant_cast<QString>(index.data(ActivityItemDelegate::ActionTextRole));
|
||||
foreach (ActivityLink actionLink, actionLinks) {
|
||||
QAction *menuAction = new QAction(actionLink._label, &menu);
|
||||
connect(menuAction, &QAction::triggered, this, [this, index, accountName, actionLink] {
|
||||
this->slotSendNotificationRequest(accountName, actionLink._link, actionLink._verb, index.row());
|
||||
});
|
||||
menu.addAction(menuAction);
|
||||
}
|
||||
menu.exec(QCursor::pos());
|
||||
}
|
||||
}
|
||||
|
||||
Activity::Type activityType = qvariant_cast<Activity::Type>(index.data(ActivityItemDelegate::ActionRole));
|
||||
if(activityType == Activity::Type::SyncFileItemType || activityType == Activity::Type::SyncResultType)
|
||||
slotOpenFile(index);
|
||||
}
|
||||
|
||||
void ActivityWidget::slotNotificationRequestFinished(int statusCode)
|
||||
{
|
||||
int row = sender()->property("activityRow").toInt();
|
||||
|
||||
// the ocs API returns stat code 100 or 200 inside the xml if it succeeded.
|
||||
if (statusCode != OCS_SUCCESS_STATUS_CODE && statusCode != OCS_SUCCESS_STATUS_CODE_V2) {
|
||||
qCWarning(lcActivity) << "Notification Request to Server failed, leave notification visible.";
|
||||
} else {
|
||||
// to do use the model to rebuild the list or remove the item
|
||||
qCWarning(lcActivity) << "Notification Request to Server successed, rebuilding list.";
|
||||
_model->removeActivityFromActivityList(row);
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWidget::slotRefreshActivities()
|
||||
{
|
||||
_model->slotRefreshActivity();
|
||||
}
|
||||
|
||||
void ActivityWidget::slotRefreshNotifications()
|
||||
{
|
||||
// start a server notification handler if no notification requests
|
||||
// are running
|
||||
if (_notificationRequestsRunning == 0) {
|
||||
ServerNotificationHandler *snh = new ServerNotificationHandler(_accountState);
|
||||
connect(snh, &ServerNotificationHandler::newNotificationList,
|
||||
this, &ActivityWidget::slotBuildNotificationDisplay);
|
||||
|
||||
snh->slotFetchNotifications();
|
||||
} else {
|
||||
qCWarning(lcActivity) << "Notification request counter not zero.";
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWidget::slotRemoveAccount()
|
||||
{
|
||||
_model->slotRemoveAccount();
|
||||
}
|
||||
|
||||
void ActivityWidget::showLabels()
|
||||
{
|
||||
_ui->_bottomLabel->hide(); // hide whatever was there before
|
||||
QString t("");
|
||||
QSetIterator<QString> i(_accountsWithoutActivities);
|
||||
while (i.hasNext()) {
|
||||
t.append(tr("<br/>Account %1 does not have activities enabled.").arg(i.next()));
|
||||
}
|
||||
if(!t.isEmpty()){
|
||||
_ui->_bottomLabel->setTextFormat(Qt::RichText);
|
||||
_ui->_bottomLabel->setText(t);
|
||||
_ui->_bottomLabel->show();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWidget::slotAccountActivityStatus(int statusCode)
|
||||
{
|
||||
if (!(_accountState && _accountState->account())) {
|
||||
return;
|
||||
}
|
||||
if (statusCode == 999) {
|
||||
_accountsWithoutActivities.insert(_accountState->account()->displayName());
|
||||
} else {
|
||||
_accountsWithoutActivities.remove(_accountState->account()->displayName());
|
||||
}
|
||||
|
||||
checkActivityWidgetVisibility();
|
||||
showLabels();
|
||||
}
|
||||
|
||||
// FIXME: Reused from protocol widget. Move over to utilities.
|
||||
QString ActivityWidget::timeString(QDateTime dt, QLocale::FormatType format) const
|
||||
{
|
||||
const QLocale loc = QLocale::system();
|
||||
QString dtFormat = loc.dateTimeFormat(format);
|
||||
static const QRegExp re("(HH|H|hh|h):mm(?!:s)");
|
||||
dtFormat.replace(re, "\\1:mm:ss");
|
||||
return loc.toString(dt, dtFormat);
|
||||
}
|
||||
|
||||
void ActivityWidget::storeActivityList(QTextStream &ts)
|
||||
{
|
||||
ActivityList activities = _model->activityList();
|
||||
|
||||
foreach (Activity activity, activities) {
|
||||
ts << right
|
||||
// account name
|
||||
<< qSetFieldWidth(activity._accName.length())
|
||||
<< activity._accName
|
||||
// separator
|
||||
<< qSetFieldWidth(2) << " - "
|
||||
|
||||
// date and time
|
||||
<< qSetFieldWidth(activity._dateTime.toString().length())
|
||||
<< activity._dateTime.toString()
|
||||
// separator
|
||||
<< qSetFieldWidth(2) << " - "
|
||||
|
||||
// fileq
|
||||
<< qSetFieldWidth(activity._file.length())
|
||||
<< activity._file
|
||||
// separator
|
||||
<< qSetFieldWidth(2) << " - "
|
||||
|
||||
// subject
|
||||
<< qSetFieldWidth(activity._subject.length())
|
||||
<< activity._subject
|
||||
// separator
|
||||
<< qSetFieldWidth(2) << " - "
|
||||
|
||||
// message
|
||||
<< qSetFieldWidth(activity._message.length())
|
||||
<< activity._message
|
||||
<< endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWidget::checkActivityWidgetVisibility()
|
||||
{
|
||||
int accountCount = AccountManager::instance()->accounts().count();
|
||||
bool hasAccountsWithActivity =
|
||||
_accountsWithoutActivities.count() != accountCount;
|
||||
|
||||
_ui->_activityList->setVisible(hasAccountsWithActivity);
|
||||
|
||||
emit hideActivityTab(!hasAccountsWithActivity);
|
||||
}
|
||||
|
||||
void ActivityWidget::slotOpenFile(QModelIndex indx)
|
||||
{
|
||||
qCDebug(lcActivity) << indx.isValid() << indx.data(ActivityItemDelegate::PathRole).toString() << QFile::exists(indx.data(ActivityItemDelegate::PathRole).toString());
|
||||
if (indx.isValid()) {
|
||||
QString fullPath = indx.data(ActivityItemDelegate::PathRole).toString();
|
||||
if(!fullPath.isEmpty()){
|
||||
if (QFile::exists(fullPath)) {
|
||||
showInFileManager(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GUI: Display the notifications.
|
||||
// All notifications in list are coming from the same account
|
||||
// but in the _widgetForNotifId hash widgets for all accounts are
|
||||
// collected.
|
||||
void ActivityWidget::slotBuildNotificationDisplay(const ActivityList &list)
|
||||
{
|
||||
// Whether a new notification was added to the list
|
||||
bool newNotificationShown = false;
|
||||
|
||||
_model->clearNotifications();
|
||||
|
||||
foreach (auto activity, list) {
|
||||
if (_blacklistedNotifications.contains(activity)) {
|
||||
qCInfo(lcActivity) << "Activity in blacklist, skip";
|
||||
continue;
|
||||
}
|
||||
|
||||
// handle gui logs. In order to NOT annoy the user with every fetching of the
|
||||
// notifications the notification id is stored in a Set. Only if an id
|
||||
// is not in the set, it qualifies for guiLog.
|
||||
// Important: The _guiLoggedNotifications set must be wiped regularly which
|
||||
// will repeat the gui log.
|
||||
|
||||
// after one hour, clear the gui log notification store
|
||||
if (_guiLogTimer.elapsed() > 60 * 60 * 1000) {
|
||||
_guiLoggedNotifications.clear();
|
||||
}
|
||||
|
||||
if (!_guiLoggedNotifications.contains(activity._id)) {
|
||||
newNotificationShown = true;
|
||||
_guiLoggedNotifications.insert(activity._id);
|
||||
|
||||
// Assemble a tray notification for the NEW notification
|
||||
ConfigFile cfg;
|
||||
if(cfg.optionalServerNotifications()){
|
||||
if(AccountManager::instance()->accounts().count() == 1){
|
||||
emit guiLog(activity._subject, "");
|
||||
} else {
|
||||
emit guiLog(activity._subject, activity._accName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_model->addNotificationToActivityList(activity);
|
||||
}
|
||||
|
||||
// restart the gui log timer now that we show a new notification
|
||||
if(newNotificationShown) {
|
||||
_guiLogTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWidget::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row)
|
||||
{
|
||||
qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName;
|
||||
|
||||
const QStringList validVerbs = QStringList() << "GET"
|
||||
<< "PUT"
|
||||
<< "POST"
|
||||
<< "DELETE";
|
||||
|
||||
if (validVerbs.contains(verb)) {
|
||||
AccountStatePtr acc = AccountManager::instance()->account(accountName);
|
||||
if (acc) {
|
||||
NotificationConfirmJob *job = new NotificationConfirmJob(acc->account());
|
||||
QUrl l(link);
|
||||
job->setLinkAndVerb(l, verb);
|
||||
job->setProperty("activityRow", QVariant::fromValue(row));
|
||||
connect(job, &AbstractNetworkJob::networkError,
|
||||
this, &ActivityWidget::slotNotifyNetworkError);
|
||||
connect(job, &NotificationConfirmJob::jobFinished,
|
||||
this, &ActivityWidget::slotNotifyServerFinished);
|
||||
job->start();
|
||||
|
||||
// count the number of running notification requests. If this member var
|
||||
// is larger than zero, no new fetching of notifications is started
|
||||
_notificationRequestsRunning++;
|
||||
}
|
||||
} else {
|
||||
qCWarning(lcActivity) << "Notification Links: Invalid verb:" << verb;
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWidget::endNotificationRequest(int replyCode)
|
||||
{
|
||||
_notificationRequestsRunning--;
|
||||
slotNotificationRequestFinished(replyCode);
|
||||
}
|
||||
|
||||
void ActivityWidget::slotNotifyNetworkError(QNetworkReply *reply)
|
||||
{
|
||||
NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob *>(sender());
|
||||
if (!job) {
|
||||
return;
|
||||
}
|
||||
|
||||
int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
endNotificationRequest(resultCode);
|
||||
qCWarning(lcActivity) << "Server notify job failed with code " << resultCode;
|
||||
}
|
||||
|
||||
void ActivityWidget::slotNotifyServerFinished(const QString &reply, int replyCode)
|
||||
{
|
||||
NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob *>(sender());
|
||||
if (!job) {
|
||||
return;
|
||||
}
|
||||
|
||||
endNotificationRequest(replyCode);
|
||||
qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply;
|
||||
}
|
||||
|
||||
void ActivityWidget::slotStyleChanged()
|
||||
{
|
||||
// Notify the other widgets (Dark-/Light-Mode switching)
|
||||
emit styleChanged();
|
||||
}
|
||||
|
||||
/* ==================================================================== */
|
||||
|
||||
ActivitySettings::ActivitySettings(AccountState *accountState, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, _accountState(accountState)
|
||||
{
|
||||
_vbox = new QVBoxLayout(this);
|
||||
setLayout(_vbox);
|
||||
|
||||
_activityWidget = new ActivityWidget(_accountState, this);
|
||||
|
||||
_vbox->insertWidget(1, _activityWidget);
|
||||
connect(_activityWidget, &ActivityWidget::guiLog, this, &ActivitySettings::guiLog);
|
||||
connect(&_notificationCheckTimer, &QTimer::timeout,
|
||||
this, &ActivitySettings::slotRegularNotificationCheck);
|
||||
|
||||
// Add a progress indicator to spin if the acitivity list is updated.
|
||||
_progressIndicator = new QProgressIndicator(this);
|
||||
|
||||
// connect a model signal to stop the animation
|
||||
connect(_activityWidget, &ActivityWidget::rowsInserted, _progressIndicator, &QProgressIndicator::stopAnimation);
|
||||
connect(_activityWidget, &ActivityWidget::rowsInserted, this, &ActivitySettings::slotDisplayActivities);
|
||||
|
||||
// Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching)
|
||||
connect(this, &ActivitySettings::styleChanged, _activityWidget, &ActivityWidget::slotStyleChanged);
|
||||
}
|
||||
|
||||
void ActivitySettings::slotDisplayActivities(){
|
||||
_vbox->removeWidget(_progressIndicator);
|
||||
}
|
||||
|
||||
void ActivitySettings::setNotificationRefreshInterval(std::chrono::milliseconds interval)
|
||||
{
|
||||
qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval";
|
||||
_notificationCheckTimer.start(interval.count());
|
||||
}
|
||||
|
||||
void ActivitySettings::slotRemoveAccount()
|
||||
{
|
||||
_activityWidget->slotRemoveAccount();
|
||||
}
|
||||
|
||||
void ActivitySettings::slotRefresh()
|
||||
{
|
||||
// QElapsedTimer isn't actually constructed as invalid.
|
||||
if (!_timeSinceLastCheck.contains(_accountState)) {
|
||||
_timeSinceLastCheck[_accountState].invalidate();
|
||||
}
|
||||
QElapsedTimer &timer = _timeSinceLastCheck[_accountState];
|
||||
|
||||
// Fetch Activities only if visible and if last check is longer than 15 secs ago
|
||||
if (timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD) {
|
||||
qCDebug(lcActivity) << "Do not check as last check is only secs ago: " << timer.elapsed() / 1000;
|
||||
return;
|
||||
}
|
||||
if (_accountState && _accountState->isConnected()) {
|
||||
if (isVisible() || !timer.isValid()) {
|
||||
_vbox->insertWidget(0, _progressIndicator);
|
||||
_vbox->setAlignment(_progressIndicator, Qt::AlignHCenter);
|
||||
_progressIndicator->startAnimation();
|
||||
_activityWidget->slotRefreshActivities();
|
||||
}
|
||||
_activityWidget->slotRefreshNotifications();
|
||||
timer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivitySettings::slotRegularNotificationCheck()
|
||||
{
|
||||
slotRefresh();
|
||||
}
|
||||
|
||||
bool ActivitySettings::event(QEvent *e)
|
||||
{
|
||||
if (e->type() == QEvent::Show) {
|
||||
slotRefresh();
|
||||
}
|
||||
return QWidget::event(e);
|
||||
}
|
||||
|
||||
ActivitySettings::~ActivitySettings()
|
||||
{
|
||||
}
|
||||
|
||||
void ActivitySettings::slotStyleChanged()
|
||||
{
|
||||
if(_progressIndicator)
|
||||
_progressIndicator->setColor(QGuiApplication::palette().color(QPalette::Text));
|
||||
|
||||
// Notify the other widgets (Dark-/Light-Mode switching)
|
||||
emit styleChanged();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
* 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 ACTIVITYWIDGET_H
|
||||
#define ACTIVITYWIDGET_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QDateTime>
|
||||
#include <QLocale>
|
||||
#include <QAbstractListModel>
|
||||
#include <chrono>
|
||||
|
||||
#include "progressdispatcher.h"
|
||||
#include "owncloudgui.h"
|
||||
#include "account.h"
|
||||
#include "activitydata.h"
|
||||
#include "accountmanager.h"
|
||||
|
||||
#include "ui_activitywidget.h"
|
||||
|
||||
class QPushButton;
|
||||
class QProgressIndicator;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class Account;
|
||||
class AccountStatusPtr;
|
||||
class JsonApiJob;
|
||||
class ActivityListModel;
|
||||
|
||||
namespace Ui {
|
||||
class ActivityWidget;
|
||||
}
|
||||
class Application;
|
||||
|
||||
/**
|
||||
* @brief The ActivityWidget class
|
||||
* @ingroup gui
|
||||
*
|
||||
* The list widget to display the activities, contained in the
|
||||
* subsequent ActivitySettings widget.
|
||||
*/
|
||||
|
||||
class ActivityWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ActivityWidget(AccountState *accountState, QWidget *parent = nullptr);
|
||||
~ActivityWidget();
|
||||
QSize sizeHint() const override { return ownCloudGui::settingsDialogSize(); }
|
||||
void storeActivityList(QTextStream &ts);
|
||||
|
||||
/**
|
||||
* Adjusts the activity tab's and some widgets' visibility
|
||||
*
|
||||
* Based on whether activities are enabled and whether notifications are
|
||||
* available.
|
||||
*/
|
||||
void checkActivityWidgetVisibility();
|
||||
|
||||
public slots:
|
||||
void slotOpenFile(QModelIndex indx);
|
||||
void slotRefreshActivities();
|
||||
void slotRefreshNotifications();
|
||||
void slotRemoveAccount();
|
||||
void slotAccountActivityStatus(int statusCode);
|
||||
void addError(const QString &folderAlias, const QString &message, ErrorCategory category);
|
||||
void slotProgressInfo(const QString &folder, const ProgressInfo &progress);
|
||||
void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
|
||||
void slotStyleChanged();
|
||||
|
||||
signals:
|
||||
void guiLog(const QString &, const QString &);
|
||||
void rowsInserted();
|
||||
void hideActivityTab(bool);
|
||||
void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
|
||||
void styleChanged();
|
||||
|
||||
private slots:
|
||||
void slotBuildNotificationDisplay(const ActivityList &list);
|
||||
void slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
|
||||
void slotNotifyNetworkError(QNetworkReply *);
|
||||
void slotNotifyServerFinished(const QString &reply, int replyCode);
|
||||
void endNotificationRequest(int replyCode);
|
||||
void slotNotificationRequestFinished(int statusCode);
|
||||
void slotPrimaryButtonClickedOnListView(const QModelIndex &index);
|
||||
void slotSecondaryButtonClickedOnListView(const QModelIndex &index);
|
||||
|
||||
private:
|
||||
void customizeStyle();
|
||||
void showLabels();
|
||||
QString timeString(QDateTime dt, QLocale::FormatType format) const;
|
||||
Ui::ActivityWidget *_ui;
|
||||
QSet<QString> _accountsWithoutActivities;
|
||||
QElapsedTimer _guiLogTimer;
|
||||
QSet<int> _guiLoggedNotifications;
|
||||
ActivityList _blacklistedNotifications;
|
||||
|
||||
QTimer _removeTimer;
|
||||
|
||||
// number of currently running notification requests. If non zero,
|
||||
// no query for notifications is started.
|
||||
int _notificationRequestsRunning;
|
||||
|
||||
ActivityListModel *_model;
|
||||
AccountState *_accountState;
|
||||
const QString _accept;
|
||||
const QString _remote_share;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @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(AccountState *accountState, QWidget *parent = nullptr);
|
||||
|
||||
~ActivitySettings();
|
||||
QSize sizeHint() const override { return ownCloudGui::settingsDialogSize(); }
|
||||
|
||||
public slots:
|
||||
void slotRefresh();
|
||||
void slotRemoveAccount();
|
||||
void setNotificationRefreshInterval(std::chrono::milliseconds interval);
|
||||
void slotStyleChanged();
|
||||
|
||||
private slots:
|
||||
void slotRegularNotificationCheck();
|
||||
void slotDisplayActivities();
|
||||
|
||||
signals:
|
||||
void guiLog(const QString &, const QString &);
|
||||
void styleChanged();
|
||||
|
||||
private:
|
||||
bool event(QEvent *e) override;
|
||||
|
||||
ActivityWidget *_activityWidget;
|
||||
QProgressIndicator *_progressIndicator;
|
||||
QVBoxLayout *_vbox;
|
||||
QTimer _notificationCheckTimer;
|
||||
QHash<AccountState *, QElapsedTimer> _timeSinceLastCheck;
|
||||
|
||||
AccountState *_accountState;
|
||||
};
|
||||
}
|
||||
#endif // ActivityWIDGET_H
|
|
@ -1,98 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>OCC::ActivityWidget</class>
|
||||
<widget class="QWidget" name="OCC::ActivityWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>652</width>
|
||||
<height>556</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string notr="true">Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QListView" name="_activityList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QAbstractScrollArea::AdjustToContents</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::IgnoreAction</enum>
|
||||
</property>
|
||||
<property name="resizeMode">
|
||||
<enum>QListView::Adjust</enum>
|
||||
</property>
|
||||
<property name="viewMode">
|
||||
<enum>QListView::ListMode</enum>
|
||||
</property>
|
||||
<property name="modelColumn">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="_bottomLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">TextLabel</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>_activityList</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -266,6 +266,8 @@ Application::Application(int &argc, char **argv)
|
|||
|
||||
// Allow other classes to hook into isShowingSettingsDialog() signals (re-auth widgets, for example)
|
||||
connect(_gui.data(), &ownCloudGui::isShowingSettingsDialog, this, &Application::slotGuiIsShowingSettings);
|
||||
|
||||
_gui->createTray();
|
||||
}
|
||||
|
||||
Application::~Application()
|
||||
|
@ -390,7 +392,7 @@ void Application::slotownCloudWizardDone(int res)
|
|||
Utility::setLaunchOnStartup(_theme->appName(), _theme->appNameGUI(), true);
|
||||
}
|
||||
|
||||
_gui->slotShowSettings();
|
||||
Systray::instance()->showWindow();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -914,6 +914,8 @@ void Folder::slotItemCompleted(const SyncFileItemPtr &item)
|
|||
_folderWatcher->removePath(path() + item->_file);
|
||||
_folderWatcher->addPath(path() + item->destination());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>785</width>
|
||||
<width>516</width>
|
||||
<height>523</height>
|
||||
</rect>
|
||||
</property>
|
||||
|
|
|
@ -23,6 +23,7 @@ IconJob::IconJob(const QUrl &url, QObject *parent) :
|
|||
this, &IconJob::finished);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
_accessManager.get(request);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>563</width>
|
||||
<width>516</width>
|
||||
<height>444</height>
|
||||
</rect>
|
||||
</property>
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
#include "application.h"
|
||||
#include "owncloudgui.h"
|
||||
#include "ocsnavigationappsjob.h"
|
||||
#include "theme.h"
|
||||
#include "folderman.h"
|
||||
#include "progressdispatcher.h"
|
||||
|
@ -46,9 +45,11 @@
|
|||
#include <QX11Info>
|
||||
#endif
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QQmlEngine>
|
||||
#include <QQmlComponent>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQuickItem>
|
||||
#include <QQmlContext>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
|
@ -62,22 +63,32 @@ ownCloudGui::ownCloudGui(Application *parent)
|
|||
#ifdef WITH_LIBCLOUDPROVIDERS
|
||||
, _bus(QDBusConnection::sessionBus())
|
||||
#endif
|
||||
, _recentActionsMenu(nullptr)
|
||||
, _app(parent)
|
||||
{
|
||||
_tray = new Systray();
|
||||
_tray = Systray::instance();
|
||||
_tray->setParent(this);
|
||||
|
||||
// for the beginning, set the offline icon until the account was verified
|
||||
_tray->setIcon(Theme::instance()->folderOfflineIcon(/*systray?*/ true, /*currently visible?*/ false));
|
||||
_tray->setIcon(Theme::instance()->folderOfflineIcon(/*systray?*/ true));
|
||||
|
||||
_tray->show();
|
||||
|
||||
connect(_tray.data(), &QSystemTrayIcon::activated,
|
||||
this, &ownCloudGui::slotTrayClicked);
|
||||
|
||||
setupActions();
|
||||
setupContextMenu();
|
||||
connect(_tray.data(), &Systray::pauseSync,
|
||||
this, &ownCloudGui::slotPauseAllFolders);
|
||||
|
||||
_tray->show();
|
||||
connect(_tray.data(), &Systray::pauseSync,
|
||||
this, &ownCloudGui::slotUnpauseAllFolders);
|
||||
|
||||
connect(_tray.data(), &Systray::openHelp,
|
||||
this, &ownCloudGui::slotHelp);
|
||||
|
||||
connect(_tray.data(), &Systray::openSettings,
|
||||
this, &ownCloudGui::slotShowSettings);
|
||||
|
||||
connect(_tray.data(), &Systray::shutdown,
|
||||
this, &ownCloudGui::slotShutdown);
|
||||
|
||||
ProgressDispatcher *pd = ProgressDispatcher::instance();
|
||||
connect(pd, &ProgressDispatcher::progressInfo, this,
|
||||
|
@ -87,17 +98,18 @@ ownCloudGui::ownCloudGui(Application *parent)
|
|||
connect(folderMan, &FolderMan::folderSyncStateChange,
|
||||
this, &ownCloudGui::slotSyncStateChange);
|
||||
|
||||
connect(AccountManager::instance(), &AccountManager::accountAdded,
|
||||
this, &ownCloudGui::updateContextMenuNeeded);
|
||||
connect(AccountManager::instance(), &AccountManager::accountRemoved,
|
||||
this, &ownCloudGui::updateContextMenuNeeded);
|
||||
|
||||
connect(Logger::instance(), &Logger::guiLog,
|
||||
this, &ownCloudGui::slotShowTrayMessage);
|
||||
connect(Logger::instance(), &Logger::optionalGuiLog,
|
||||
this, &ownCloudGui::slotShowOptionalTrayMessage);
|
||||
connect(Logger::instance(), &Logger::guiMessage,
|
||||
this, &ownCloudGui::slotShowGuiMessage);
|
||||
|
||||
}
|
||||
|
||||
void ownCloudGui::createTray()
|
||||
{
|
||||
_tray->create();
|
||||
}
|
||||
|
||||
#ifdef WITH_LIBCLOUDPROVIDERS
|
||||
|
@ -140,13 +152,6 @@ void ownCloudGui::slotOpenSettingsDialog()
|
|||
|
||||
void ownCloudGui::slotTrayClicked(QSystemTrayIcon::ActivationReason reason)
|
||||
{
|
||||
if (_workaroundFakeDoubleClick) {
|
||||
static QElapsedTimer last_click;
|
||||
if (last_click.isValid() && last_click.elapsed() < 200) {
|
||||
return;
|
||||
}
|
||||
last_click.start();
|
||||
}
|
||||
|
||||
// Left click
|
||||
if (reason == QSystemTrayIcon::Trigger) {
|
||||
|
@ -158,17 +163,10 @@ void ownCloudGui::slotTrayClicked(QSystemTrayIcon::ActivationReason reason)
|
|||
Q_ASSERT(shareDialog.data());
|
||||
raiseDialog(shareDialog);
|
||||
}
|
||||
} else if (_tray->isOpen()) {
|
||||
_tray->hideWindow();
|
||||
} else {
|
||||
#ifdef Q_OS_MAC
|
||||
// on macOS, a left click always opens menu.
|
||||
// However if the settings dialog is already visible but hidden
|
||||
// by other applications, this will bring it to the front.
|
||||
if (!_settingsDialog.isNull() && _settingsDialog->isVisible()) {
|
||||
raiseDialog(_settingsDialog.data());
|
||||
}
|
||||
#else
|
||||
slotOpenSettingsDialog();
|
||||
#endif
|
||||
_tray->showWindow();
|
||||
}
|
||||
}
|
||||
// FIXME: Also make sure that any auto updater dialogue https://github.com/owncloud/client/issues/5613
|
||||
|
@ -178,7 +176,6 @@ void ownCloudGui::slotTrayClicked(QSystemTrayIcon::ActivationReason reason)
|
|||
void ownCloudGui::slotSyncStateChange(Folder *folder)
|
||||
{
|
||||
slotComputeOverallSyncStatus();
|
||||
updateContextMenuNeeded();
|
||||
|
||||
if (!folder) {
|
||||
return; // Valid, just a general GUI redraw was needed.
|
||||
|
@ -194,16 +191,11 @@ void ownCloudGui::slotSyncStateChange(Folder *folder)
|
|||
|| result.status() == SyncResult::Error) {
|
||||
Logger::instance()->enterNextLogFile();
|
||||
}
|
||||
|
||||
if (result.status() == SyncResult::NotYetStarted) {
|
||||
_settingsDialog->slotRefreshActivity(folder->accountState());
|
||||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::slotFoldersChanged()
|
||||
{
|
||||
slotComputeOverallSyncStatus();
|
||||
updateContextMenuNeeded();
|
||||
}
|
||||
|
||||
void ownCloudGui::slotOpenPath(const QString &path)
|
||||
|
@ -213,7 +205,6 @@ void ownCloudGui::slotOpenPath(const QString &path)
|
|||
|
||||
void ownCloudGui::slotAccountStateChanged()
|
||||
{
|
||||
updateContextMenuNeeded();
|
||||
slotComputeOverallSyncStatus();
|
||||
}
|
||||
|
||||
|
@ -239,7 +230,7 @@ void ownCloudGui::slotComputeOverallSyncStatus()
|
|||
// Don't overwrite the status if we're currently syncing
|
||||
if (FolderMan::instance()->currentSyncFolder())
|
||||
return;
|
||||
_actionStatus->setText(text);
|
||||
//_actionStatus->setText(text);
|
||||
};
|
||||
|
||||
foreach (auto a, AccountManager::instance()->accounts()) {
|
||||
|
@ -259,7 +250,7 @@ void ownCloudGui::slotComputeOverallSyncStatus()
|
|||
}
|
||||
|
||||
if (!problemAccounts.empty()) {
|
||||
_tray->setIcon(Theme::instance()->folderOfflineIcon(true, contextMenuVisible()));
|
||||
_tray->setIcon(Theme::instance()->folderOfflineIcon(true));
|
||||
if (allDisconnected) {
|
||||
setStatusText(tr("Disconnected"));
|
||||
} else {
|
||||
|
@ -289,12 +280,12 @@ void ownCloudGui::slotComputeOverallSyncStatus()
|
|||
}
|
||||
|
||||
if (allSignedOut) {
|
||||
_tray->setIcon(Theme::instance()->folderOfflineIcon(true, contextMenuVisible()));
|
||||
_tray->setIcon(Theme::instance()->folderOfflineIcon(true));
|
||||
_tray->setToolTip(tr("Please sign in"));
|
||||
setStatusText(tr("Signed out"));
|
||||
return;
|
||||
} else if (allPaused) {
|
||||
_tray->setIcon(Theme::instance()->syncStateIcon(SyncResult::Paused, true, contextMenuVisible()));
|
||||
_tray->setIcon(Theme::instance()->syncStateIcon(SyncResult::Paused, true));
|
||||
_tray->setToolTip(tr("Account synchronization is disabled"));
|
||||
setStatusText(tr("Synchronization is paused"));
|
||||
return;
|
||||
|
@ -321,7 +312,7 @@ void ownCloudGui::slotComputeOverallSyncStatus()
|
|||
iconStatus = SyncResult::Problem;
|
||||
}
|
||||
|
||||
QIcon statusIcon = Theme::instance()->syncStateIcon(iconStatus, true, contextMenuVisible());
|
||||
QIcon statusIcon = Theme::instance()->syncStateIcon(iconStatus, true);
|
||||
_tray->setIcon(statusIcon);
|
||||
|
||||
// create the tray blob message, check if we have an defined state
|
||||
|
@ -359,381 +350,6 @@ void ownCloudGui::slotComputeOverallSyncStatus()
|
|||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::addAccountContextMenu(AccountStatePtr accountState, QMenu *menu, bool separateMenu)
|
||||
{
|
||||
// Only show the name in the action if it's not part of an
|
||||
// account sub menu.
|
||||
QString browserOpen = tr("Open in browser");
|
||||
if (!separateMenu) {
|
||||
browserOpen = tr("Open %1 in browser").arg(Theme::instance()->appNameGUI());
|
||||
}
|
||||
auto actionOpenoC = menu->addAction(browserOpen);
|
||||
actionOpenoC->setProperty(propertyAccountC, QVariant::fromValue(accountState->account()));
|
||||
QObject::connect(actionOpenoC, &QAction::triggered, this, &ownCloudGui::slotOpenOwnCloud);
|
||||
|
||||
FolderMan *folderMan = FolderMan::instance();
|
||||
bool firstFolder = true;
|
||||
bool singleSyncFolder = folderMan->map().size() == 1 && Theme::instance()->singleSyncFolder();
|
||||
bool onePaused = false;
|
||||
bool allPaused = true;
|
||||
foreach (Folder *folder, folderMan->map()) {
|
||||
if (folder->accountState() != accountState.data()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (folder->syncPaused()) {
|
||||
onePaused = true;
|
||||
} else {
|
||||
allPaused = false;
|
||||
}
|
||||
|
||||
if (firstFolder && !singleSyncFolder) {
|
||||
firstFolder = false;
|
||||
menu->addSeparator();
|
||||
menu->addAction(tr("Managed Folders:"))->setDisabled(true);
|
||||
}
|
||||
|
||||
QAction *action = menu->addAction(tr("Open folder '%1'").arg(folder->shortGuiLocalPath()));
|
||||
auto alias = folder->alias();
|
||||
connect(action, &QAction::triggered, this, [this, alias] { this->slotFolderOpenAction(alias); });
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
if (separateMenu) {
|
||||
if (onePaused) {
|
||||
QAction *enable = menu->addAction(tr("Resume all folders"));
|
||||
enable->setProperty(propertyAccountC, QVariant::fromValue(accountState));
|
||||
connect(enable, &QAction::triggered, this, &ownCloudGui::slotUnpauseAllFolders);
|
||||
}
|
||||
if (!allPaused) {
|
||||
QAction *enable = menu->addAction(tr("Pause all folders"));
|
||||
enable->setProperty(propertyAccountC, QVariant::fromValue(accountState));
|
||||
connect(enable, &QAction::triggered, this, &ownCloudGui::slotPauseAllFolders);
|
||||
}
|
||||
|
||||
if (accountState->isSignedOut()) {
|
||||
QAction *signin = menu->addAction(tr("Log in …"));
|
||||
signin->setProperty(propertyAccountC, QVariant::fromValue(accountState));
|
||||
connect(signin, &QAction::triggered, this, &ownCloudGui::slotLogin);
|
||||
} else {
|
||||
QAction *signout = menu->addAction(tr("Log out"));
|
||||
signout->setProperty(propertyAccountC, QVariant::fromValue(accountState));
|
||||
connect(signout, &QAction::triggered, this, &ownCloudGui::slotLogout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::slotContextMenuAboutToShow()
|
||||
{
|
||||
_contextMenuVisibleManual = true;
|
||||
|
||||
// Update icon in sys tray, as it might change depending on the context menu state
|
||||
slotComputeOverallSyncStatus();
|
||||
|
||||
if (!_workaroundNoAboutToShowUpdate) {
|
||||
updateContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::slotContextMenuAboutToHide()
|
||||
{
|
||||
_contextMenuVisibleManual = false;
|
||||
|
||||
// Update icon in sys tray, as it might change depending on the context menu state
|
||||
slotComputeOverallSyncStatus();
|
||||
}
|
||||
|
||||
bool ownCloudGui::contextMenuVisible() const
|
||||
{
|
||||
// On some platforms isVisible doesn't work and always returns false,
|
||||
// elsewhere aboutToHide is unreliable.
|
||||
if (_workaroundManualVisibility)
|
||||
return _contextMenuVisibleManual;
|
||||
return _contextMenu->isVisible();
|
||||
}
|
||||
|
||||
static bool minimalTrayMenu()
|
||||
{
|
||||
static QByteArray var = qgetenv("OWNCLOUD_MINIMAL_TRAY_MENU");
|
||||
return !var.isEmpty();
|
||||
}
|
||||
|
||||
static bool updateWhileVisible()
|
||||
{
|
||||
static QByteArray var = qgetenv("OWNCLOUD_TRAY_UPDATE_WHILE_VISIBLE");
|
||||
if (var == "1") {
|
||||
return true;
|
||||
} else if (var == "0") {
|
||||
return false;
|
||||
} else {
|
||||
// triggers bug on OS X: https://bugreports.qt.io/browse/QTBUG-54845
|
||||
// or flickering on Xubuntu
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static QByteArray envForceQDBusTrayWorkaround()
|
||||
{
|
||||
static QByteArray var = qgetenv("OWNCLOUD_FORCE_QDBUS_TRAY_WORKAROUND");
|
||||
return var;
|
||||
}
|
||||
|
||||
static QByteArray envForceWorkaroundShowAndHideTray()
|
||||
{
|
||||
static QByteArray var = qgetenv("OWNCLOUD_FORCE_TRAY_SHOW_HIDE");
|
||||
return var;
|
||||
}
|
||||
|
||||
static QByteArray envForceWorkaroundNoAboutToShowUpdate()
|
||||
{
|
||||
static QByteArray var = qgetenv("OWNCLOUD_FORCE_TRAY_NO_ABOUT_TO_SHOW");
|
||||
return var;
|
||||
}
|
||||
|
||||
static QByteArray envForceWorkaroundFakeDoubleClick()
|
||||
{
|
||||
static QByteArray var = qgetenv("OWNCLOUD_FORCE_TRAY_FAKE_DOUBLE_CLICK");
|
||||
return var;
|
||||
}
|
||||
|
||||
static QByteArray envForceWorkaroundManualVisibility()
|
||||
{
|
||||
static QByteArray var = qgetenv("OWNCLOUD_FORCE_TRAY_MANUAL_VISIBILITY");
|
||||
return var;
|
||||
}
|
||||
|
||||
void ownCloudGui::setupContextMenu()
|
||||
{
|
||||
if (_contextMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
_contextMenu.reset(new QMenu());
|
||||
_contextMenu->setTitle(Theme::instance()->appNameGUI());
|
||||
|
||||
_recentActionsMenu = new QMenu(tr("Recent Changes"), _contextMenu.data());
|
||||
|
||||
// this must be called only once after creating the context menu, or
|
||||
// it will trigger a bug in Ubuntu's SNI bridge patch (11.10, 12.04).
|
||||
_tray->setContextMenu(_contextMenu.data());
|
||||
|
||||
// The tray menu is surprisingly problematic. Being able to switch to
|
||||
// a minimal version of it is a useful workaround and testing tool.
|
||||
if (minimalTrayMenu()) {
|
||||
_contextMenu->addAction(_actionQuit);
|
||||
return;
|
||||
}
|
||||
|
||||
auto applyEnvVariable = [](bool *sw, const QByteArray &value) {
|
||||
if (value == "1")
|
||||
*sw = true;
|
||||
if (value == "0")
|
||||
*sw = false;
|
||||
};
|
||||
|
||||
// This is an old compound flag that people might still depend on
|
||||
bool qdbusmenuWorkarounds = false;
|
||||
applyEnvVariable(&qdbusmenuWorkarounds, envForceQDBusTrayWorkaround());
|
||||
if (qdbusmenuWorkarounds) {
|
||||
_workaroundFakeDoubleClick = true;
|
||||
_workaroundNoAboutToShowUpdate = true;
|
||||
_workaroundShowAndHideTray = true;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
// https://bugreports.qt.io/browse/QTBUG-54633
|
||||
_workaroundNoAboutToShowUpdate = true;
|
||||
_workaroundManualVisibility = true;
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// For KDE sessions if the platform plugin is missing,
|
||||
// neither aboutToShow() updates nor the isVisible() call
|
||||
// work. At least aboutToHide is reliable.
|
||||
// https://github.com/owncloud/client/issues/6545
|
||||
static QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP");
|
||||
static QByteArray desktopSession = qgetenv("DESKTOP_SESSION");
|
||||
bool isKde =
|
||||
xdgCurrentDesktop.contains("KDE")
|
||||
|| desktopSession.contains("plasma")
|
||||
|| desktopSession.contains("kde");
|
||||
QObject *platformMenu = reinterpret_cast<QObject *>(_tray->contextMenu()->platformMenu());
|
||||
if (isKde && platformMenu && platformMenu->metaObject()->className() == QLatin1String("QDBusPlatformMenu")) {
|
||||
_workaroundManualVisibility = true;
|
||||
_workaroundNoAboutToShowUpdate = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
applyEnvVariable(&_workaroundNoAboutToShowUpdate, envForceWorkaroundNoAboutToShowUpdate());
|
||||
applyEnvVariable(&_workaroundFakeDoubleClick, envForceWorkaroundFakeDoubleClick());
|
||||
applyEnvVariable(&_workaroundShowAndHideTray, envForceWorkaroundShowAndHideTray());
|
||||
applyEnvVariable(&_workaroundManualVisibility, envForceWorkaroundManualVisibility());
|
||||
|
||||
qCInfo(lcApplication) << "Tray menu workarounds:"
|
||||
<< "noabouttoshow:" << _workaroundNoAboutToShowUpdate
|
||||
<< "fakedoubleclick:" << _workaroundFakeDoubleClick
|
||||
<< "showhide:" << _workaroundShowAndHideTray
|
||||
<< "manualvisibility:" << _workaroundManualVisibility;
|
||||
|
||||
|
||||
connect(&_delayedTrayUpdateTimer, &QTimer::timeout, this, &ownCloudGui::updateContextMenu);
|
||||
_delayedTrayUpdateTimer.setInterval(2 * 1000);
|
||||
_delayedTrayUpdateTimer.setSingleShot(true);
|
||||
|
||||
connect(_contextMenu.data(), SIGNAL(aboutToShow()), SLOT(slotContextMenuAboutToShow()));
|
||||
// unfortunately aboutToHide is unreliable, it seems to work on OSX though
|
||||
connect(_contextMenu.data(), SIGNAL(aboutToHide()), SLOT(slotContextMenuAboutToHide()));
|
||||
|
||||
// Populate the context menu now.
|
||||
updateContextMenu();
|
||||
}
|
||||
|
||||
void ownCloudGui::updateContextMenu()
|
||||
{
|
||||
if (minimalTrayMenu()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's visible, we can't update live, and it won't be updated lazily: reschedule
|
||||
if (contextMenuVisible() && !updateWhileVisible() && _workaroundNoAboutToShowUpdate) {
|
||||
if (!_delayedTrayUpdateTimer.isActive()) {
|
||||
_delayedTrayUpdateTimer.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_workaroundShowAndHideTray) {
|
||||
// To make tray menu updates work with these bugs (see setupContextMenu)
|
||||
// we need to hide and show the tray icon. We don't want to do that
|
||||
// while it's visible!
|
||||
if (contextMenuVisible()) {
|
||||
if (!_delayedTrayUpdateTimer.isActive()) {
|
||||
_delayedTrayUpdateTimer.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_tray->hide();
|
||||
}
|
||||
|
||||
_contextMenu->clear();
|
||||
slotRebuildRecentMenus();
|
||||
|
||||
// We must call deleteLater because we might be called from the press in one of the actions.
|
||||
foreach (auto menu, _accountMenus) {
|
||||
menu->deleteLater();
|
||||
}
|
||||
_accountMenus.clear();
|
||||
|
||||
auto accountList = AccountManager::instance()->accounts();
|
||||
|
||||
bool isConfigured = (!accountList.isEmpty());
|
||||
bool atLeastOneConnected = false;
|
||||
bool atLeastOnePaused = false;
|
||||
bool atLeastOneNotPaused = false;
|
||||
foreach (auto a, accountList) {
|
||||
if (a->isConnected()) {
|
||||
atLeastOneConnected = true;
|
||||
}
|
||||
}
|
||||
foreach (auto f, FolderMan::instance()->map()) {
|
||||
if (f->syncPaused()) {
|
||||
atLeastOnePaused = true;
|
||||
} else {
|
||||
atLeastOneNotPaused = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (accountList.count() > 1) {
|
||||
foreach (AccountStatePtr account, accountList) {
|
||||
QMenu *accountMenu = new QMenu(account->account()->displayName(), _contextMenu.data());
|
||||
_accountMenus.append(accountMenu);
|
||||
_contextMenu->addMenu(accountMenu);
|
||||
|
||||
addAccountContextMenu(account, accountMenu, true);
|
||||
fetchNavigationApps(account);
|
||||
}
|
||||
} else if (accountList.count() == 1) {
|
||||
addAccountContextMenu(accountList.first(), _contextMenu.data(), false);
|
||||
fetchNavigationApps(accountList.first());
|
||||
}
|
||||
|
||||
_contextMenu->addSeparator();
|
||||
|
||||
_contextMenu->addAction(_actionStatus);
|
||||
if (isConfigured && atLeastOneConnected) {
|
||||
_contextMenu->addMenu(_recentActionsMenu);
|
||||
}
|
||||
|
||||
_contextMenu->addSeparator();
|
||||
|
||||
if (_navLinksMenu) {
|
||||
_contextMenu->addMenu(_navLinksMenu);
|
||||
}
|
||||
|
||||
_contextMenu->addSeparator();
|
||||
|
||||
if (accountList.isEmpty()) {
|
||||
_contextMenu->addAction(_actionNewAccountWizard);
|
||||
}
|
||||
_contextMenu->addAction(_actionSettings);
|
||||
if (!Theme::instance()->helpUrl().isEmpty()) {
|
||||
_contextMenu->addAction(_actionHelp);
|
||||
}
|
||||
|
||||
if (_actionCrash) {
|
||||
_contextMenu->addAction(_actionCrash);
|
||||
}
|
||||
|
||||
_contextMenu->addSeparator();
|
||||
|
||||
if (atLeastOnePaused) {
|
||||
QString text;
|
||||
if (accountList.count() > 1) {
|
||||
text = tr("Resume all synchronization");
|
||||
} else {
|
||||
text = tr("Resume synchronization");
|
||||
}
|
||||
QAction *action = _contextMenu->addAction(text);
|
||||
connect(action, &QAction::triggered, this, &ownCloudGui::slotUnpauseAllFolders);
|
||||
}
|
||||
if (atLeastOneNotPaused) {
|
||||
QString text;
|
||||
if (accountList.count() > 1) {
|
||||
text = tr("Pause all synchronization");
|
||||
} else {
|
||||
text = tr("Pause synchronization");
|
||||
}
|
||||
QAction *action = _contextMenu->addAction(text);
|
||||
connect(action, &QAction::triggered, this, &ownCloudGui::slotPauseAllFolders);
|
||||
}
|
||||
_contextMenu->addAction(_actionQuit);
|
||||
|
||||
if (_workaroundShowAndHideTray) {
|
||||
_tray->show();
|
||||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::updateContextMenuNeeded()
|
||||
{
|
||||
// if it's visible and we can update live: update now
|
||||
if (contextMenuVisible() && updateWhileVisible()) {
|
||||
// Note: don't update while visible on OSX
|
||||
// https://bugreports.qt.io/browse/QTBUG-54845
|
||||
updateContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// if we can't lazily update: update later
|
||||
if (_workaroundNoAboutToShowUpdate) {
|
||||
// Note: don't update immediately even in the invisible case
|
||||
// as that can lead to extremely frequent menu updates
|
||||
if (!_delayedTrayUpdateTimer.isActive()) {
|
||||
_delayedTrayUpdateTimer.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::slotShowTrayMessage(const QString &title, const QString &msg)
|
||||
{
|
||||
if (_tray)
|
||||
|
@ -747,7 +363,6 @@ void ownCloudGui::slotShowOptionalTrayMessage(const QString &title, const QStrin
|
|||
slotShowTrayMessage(title, msg);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* open the folder with the given Alias
|
||||
*/
|
||||
|
@ -771,156 +386,17 @@ void ownCloudGui::slotFolderOpenAction(const QString &alias)
|
|||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::setupActions()
|
||||
{
|
||||
_actionStatus = new QAction(tr("Unknown status"), this);
|
||||
_actionStatus->setEnabled(false);
|
||||
_navLinksMenu = new QMenu(tr("Apps"));
|
||||
_navLinksMenu->setEnabled(false);
|
||||
_actionSettings = new QAction(tr("Settings …"), this);
|
||||
_actionNewAccountWizard = new QAction(tr("New account …"), this);
|
||||
_actionRecent = new QAction(tr("View more activity …"), this);
|
||||
_actionRecent->setEnabled(true);
|
||||
|
||||
QObject::connect(_actionRecent, &QAction::triggered, this, &ownCloudGui::slotShowSyncProtocol);
|
||||
QObject::connect(_actionSettings, &QAction::triggered, this, &ownCloudGui::slotShowSettings);
|
||||
QObject::connect(_actionNewAccountWizard, &QAction::triggered, this, &ownCloudGui::slotNewAccountWizard);
|
||||
_actionHelp = new QAction(tr("Help"), this);
|
||||
QObject::connect(_actionHelp, &QAction::triggered, this, &ownCloudGui::slotHelp);
|
||||
_actionQuit = new QAction(tr("Quit %1").arg(Theme::instance()->appNameGUI()), this);
|
||||
QObject::connect(_actionQuit, SIGNAL(triggered(bool)), _app, SLOT(quit()));
|
||||
|
||||
if (_app->debugMode()) {
|
||||
_actionCrash = new QAction(tr("Crash now", "Only shows in debug mode to allow testing the crash handler"), this);
|
||||
connect(_actionCrash, &QAction::triggered, _app, &Application::slotCrash);
|
||||
} else {
|
||||
_actionCrash = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){
|
||||
if(statusCode == 200){
|
||||
qCDebug(lcApplication) << "New navigation apps ETag Response Header received " << value;
|
||||
auto account = qvariant_cast<AccountStatePtr>(sender()->property(propertyAccountC));
|
||||
account->setNavigationAppsEtagResponseHeader(value);
|
||||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::fetchNavigationApps(AccountStatePtr account){
|
||||
OcsNavigationAppsJob *job = new OcsNavigationAppsJob(account->account());
|
||||
job->setProperty(propertyAccountC, QVariant::fromValue(account));
|
||||
job->addRawHeader("If-None-Match", account->navigationAppsEtagResponseHeader());
|
||||
connect(job, &OcsNavigationAppsJob::appsJobFinished, this, &ownCloudGui::slotNavigationAppsFetched);
|
||||
connect(job, &OcsNavigationAppsJob::etagResponseHeaderReceived, this, &ownCloudGui::slotEtagResponseHeaderReceived);
|
||||
connect(job, &OcsNavigationAppsJob::ocsError, this, &ownCloudGui::slotOcsError);
|
||||
job->getNavigationApps();
|
||||
}
|
||||
|
||||
void ownCloudGui::buildNavigationAppsMenu(AccountStatePtr account, QMenu *accountMenu){
|
||||
auto navLinks = _navApps.value(account);
|
||||
|
||||
_navLinksMenu->clear();
|
||||
_navLinksMenu->setEnabled(navLinks.size() > 0);
|
||||
|
||||
if(navLinks.size() > 0){
|
||||
// when there is only one account add the nav links above the settings
|
||||
QAction *actionBefore = _actionSettings;
|
||||
|
||||
// when there is more than one account add the nav links above pause/unpause folder or logout action
|
||||
if(AccountManager::instance()->accounts().size() > 1){
|
||||
foreach(QAction *action, accountMenu->actions()){
|
||||
|
||||
// pause/unpause folder and logout actions have propertyAccountC
|
||||
if(auto actionAccount = qvariant_cast<AccountStatePtr>(action->property(propertyAccountC))){
|
||||
if(actionAccount == account){
|
||||
actionBefore = action;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create submenu with links
|
||||
foreach (const QJsonValue &value, navLinks) {
|
||||
auto navLink = value.toObject();
|
||||
QAction *action = new QAction(navLink.value("name").toString(), this);
|
||||
QUrl href(navLink.value("href").toString());
|
||||
connect(action, &QAction::triggered, this, [href] { QDesktopServices::openUrl(href); });
|
||||
_navLinksMenu->addAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode)
|
||||
{
|
||||
if(auto account = qvariant_cast<AccountStatePtr>(sender()->property(propertyAccountC))){
|
||||
if (statusCode == 304) {
|
||||
qCWarning(lcApplication) << "Status code " << statusCode << " Not Modified - No new navigation apps.";
|
||||
} else {
|
||||
if(!reply.isEmpty()){
|
||||
auto element = reply.object().value("ocs").toObject().value("data");
|
||||
auto navLinks = element.toArray();
|
||||
_navApps.insert(account, navLinks);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO see pull #523
|
||||
auto accountList = AccountManager::instance()->accounts();
|
||||
if(accountList.size() > 1){
|
||||
// the list of apps will be displayed under the account that it belongs to
|
||||
foreach (QMenu *accountMenu, _accountMenus) {
|
||||
if(accountMenu->title() == account->account()->displayName()){
|
||||
buildNavigationAppsMenu(account, accountMenu);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(accountList.size() == 1){
|
||||
buildNavigationAppsMenu(account, _contextMenu.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ownCloudGui::slotOcsError(int statusCode, const QString &message)
|
||||
{
|
||||
emit serverError(statusCode, message);
|
||||
}
|
||||
|
||||
void ownCloudGui::slotRebuildRecentMenus()
|
||||
{
|
||||
_recentActionsMenu->clear();
|
||||
if (!_recentItemsActions.isEmpty()) {
|
||||
foreach (QAction *a, _recentItemsActions) {
|
||||
_recentActionsMenu->addAction(a);
|
||||
}
|
||||
_recentActionsMenu->addSeparator();
|
||||
} else {
|
||||
_recentActionsMenu->addAction(tr("No items synced recently"))->setEnabled(false);
|
||||
}
|
||||
// add a more... entry.
|
||||
_recentActionsMenu->addAction(_actionRecent);
|
||||
}
|
||||
|
||||
/// Returns true if the completion of a given item should show up in the
|
||||
/// 'Recent Activity' menu
|
||||
static bool shouldShowInRecentsMenu(const SyncFileItem &item)
|
||||
{
|
||||
return !Progress::isIgnoredKind(item._status)
|
||||
&& item._instruction != CSYNC_INSTRUCTION_EVAL
|
||||
&& item._instruction != CSYNC_INSTRUCTION_NONE;
|
||||
}
|
||||
|
||||
|
||||
void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo &progress)
|
||||
{
|
||||
Q_UNUSED(folder);
|
||||
|
||||
if (progress.status() == ProgressInfo::Discovery) {
|
||||
if (!progress._currentDiscoveredRemoteFolder.isEmpty()) {
|
||||
_actionStatus->setText(tr("Checking for changes in remote '%1'")
|
||||
.arg(progress._currentDiscoveredRemoteFolder));
|
||||
//_actionStatus->setText(tr("Checking for changes in remote '%1'")
|
||||
//.arg(progress._currentDiscoveredRemoteFolder));
|
||||
} else if (!progress._currentDiscoveredLocalFolder.isEmpty()) {
|
||||
_actionStatus->setText(tr("Checking for changes in local '%1'")
|
||||
.arg(progress._currentDiscoveredLocalFolder));
|
||||
//_actionStatus->setText(tr("Checking for changes in local '%1'")
|
||||
//.arg(progress._currentDiscoveredLocalFolder));
|
||||
}
|
||||
} else if (progress.status() == ProgressInfo::Done) {
|
||||
QTimer::singleShot(2000, this, &ownCloudGui::slotComputeOverallSyncStatus);
|
||||
|
@ -943,7 +419,7 @@ void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo &
|
|||
.arg(currentFile)
|
||||
.arg(totalFileCount);
|
||||
}
|
||||
_actionStatus->setText(msg);
|
||||
//_actionStatus->setText(msg);
|
||||
} else {
|
||||
QString totalSizeStr = Utility::octetsToString(progress.totalSize());
|
||||
QString msg;
|
||||
|
@ -954,18 +430,10 @@ void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo &
|
|||
msg = tr("Syncing %1")
|
||||
.arg(totalSizeStr);
|
||||
}
|
||||
_actionStatus->setText(msg);
|
||||
//_actionStatus->setText(msg);
|
||||
}
|
||||
|
||||
_actionRecent->setIcon(QIcon()); // Fixme: Set a "in-progress"-item eventually.
|
||||
|
||||
if (!progress._lastCompletedItem.isEmpty()
|
||||
&& shouldShowInRecentsMenu(progress._lastCompletedItem)) {
|
||||
if (Progress::isWarningKind(progress._lastCompletedItem._status)) {
|
||||
// display a warn icon if warnings happened.
|
||||
QIcon warnIcon(":/client/resources/warning");
|
||||
_actionRecent->setIcon(warnIcon);
|
||||
}
|
||||
if (!progress._lastCompletedItem.isEmpty()) {
|
||||
|
||||
QString kindStr = Progress::asResultString(progress._lastCompletedItem);
|
||||
QString timeStr = QTime::currentTime().toString("hh:mm");
|
||||
|
@ -984,12 +452,6 @@ void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo &
|
|||
_recentItemsActions.takeFirst()->deleteLater();
|
||||
}
|
||||
_recentItemsActions.append(action);
|
||||
|
||||
// Update the "Recent" menu if the context menu is being shown,
|
||||
// otherwise it'll be updated later, when the context menu is opened.
|
||||
if (updateWhileVisible() && contextMenuVisible()) {
|
||||
slotRebuildRecentMenus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1097,6 +559,7 @@ void ownCloudGui::slotShutdown()
|
|||
_settingsDialog->close();
|
||||
if (!_logBrowser.isNull())
|
||||
_logBrowser->deleteLater();
|
||||
_app->quit();
|
||||
}
|
||||
|
||||
void ownCloudGui::slotToggleLogBrowser()
|
||||
|
|
|
@ -63,9 +63,7 @@ public:
|
|||
void setupCloudProviders();
|
||||
bool cloudProviderApiAvailable();
|
||||
#endif
|
||||
|
||||
/// Whether the tray menu is visible
|
||||
bool contextMenuVisible() const;
|
||||
void createTray();
|
||||
|
||||
signals:
|
||||
void setupProxy();
|
||||
|
@ -73,16 +71,10 @@ signals:
|
|||
void isShowingSettingsDialog();
|
||||
|
||||
public slots:
|
||||
void setupContextMenu();
|
||||
void updateContextMenu();
|
||||
void updateContextMenuNeeded();
|
||||
void slotContextMenuAboutToShow();
|
||||
void slotContextMenuAboutToHide();
|
||||
void slotComputeOverallSyncStatus();
|
||||
void slotShowTrayMessage(const QString &title, const QString &msg);
|
||||
void slotShowOptionalTrayMessage(const QString &title, const QString &msg);
|
||||
void slotFolderOpenAction(const QString &alias);
|
||||
void slotRebuildRecentMenus();
|
||||
void slotUpdateProgress(const QString &folder, const ProgressInfo &progress);
|
||||
void slotShowGuiMessage(const QString &title, const QString &message);
|
||||
void slotFoldersChanged();
|
||||
|
@ -99,8 +91,6 @@ public slots:
|
|||
void slotOpenPath(const QString &path);
|
||||
void slotAccountStateChanged();
|
||||
void slotTrayMessageIfServerUnsupported(Account *account);
|
||||
void slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode);
|
||||
void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
|
||||
|
||||
|
||||
/**
|
||||
|
@ -114,9 +104,6 @@ public slots:
|
|||
|
||||
void slotRemoveDestroyedShareDialogs();
|
||||
|
||||
protected slots:
|
||||
void slotOcsError(int statusCode, const QString &message);
|
||||
|
||||
private slots:
|
||||
void slotLogin();
|
||||
void slotLogout();
|
||||
|
@ -126,47 +113,21 @@ private slots:
|
|||
|
||||
private:
|
||||
void setPauseOnAllFoldersHelper(bool pause);
|
||||
void setupActions();
|
||||
void addAccountContextMenu(AccountStatePtr accountState, QMenu *menu, bool separateMenu);
|
||||
void fetchNavigationApps(AccountStatePtr account);
|
||||
void buildNavigationAppsMenu(AccountStatePtr account, QMenu *accountMenu);
|
||||
|
||||
QPointer<Systray> _tray;
|
||||
QPointer<SettingsDialog> _settingsDialog;
|
||||
QPointer<LogBrowser> _logBrowser;
|
||||
// tray's menu
|
||||
QScopedPointer<QMenu> _contextMenu;
|
||||
|
||||
// Manually tracking whether the context menu is visible via aboutToShow
|
||||
// and aboutToHide. Unfortunately aboutToHide isn't reliable everywhere
|
||||
// so this only gets used with _workaroundManualVisibility (when the tray's
|
||||
// isVisible() is unreliable)
|
||||
bool _contextMenuVisibleManual = false;
|
||||
|
||||
#ifdef WITH_LIBCLOUDPROVIDERS
|
||||
QDBusConnection _bus;
|
||||
#endif
|
||||
|
||||
QMenu *_recentActionsMenu;
|
||||
QVector<QMenu *> _accountMenus;
|
||||
bool _workaroundShowAndHideTray = false;
|
||||
bool _workaroundNoAboutToShowUpdate = false;
|
||||
bool _workaroundFakeDoubleClick = false;
|
||||
bool _workaroundManualVisibility = false;
|
||||
QTimer _delayedTrayUpdateTimer;
|
||||
QMap<QString, QPointer<ShareDialog>> _shareDialogs;
|
||||
|
||||
QAction *_actionNewAccountWizard;
|
||||
QAction *_actionSettings;
|
||||
QAction *_actionStatus;
|
||||
QAction *_actionEstimate;
|
||||
QAction *_actionRecent;
|
||||
QAction *_actionHelp;
|
||||
QAction *_actionQuit;
|
||||
QAction *_actionCrash;
|
||||
|
||||
QMenu *_navLinksMenu;
|
||||
QMap<AccountStatePtr, QJsonArray> _navApps;
|
||||
|
||||
QList<QAction *> _recentItemsActions;
|
||||
Application *_app;
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* 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 SERVERNOTIFICATIONHANDLER_H
|
||||
#define SERVERNOTIFICATIONHANDLER_H
|
||||
|
||||
#include <QtCore>
|
||||
|
||||
#include "activitywidget.h"
|
||||
|
||||
class QJsonDocument;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class ServerNotificationHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr);
|
||||
static QMap<int, QIcon> iconCache;
|
||||
|
||||
signals:
|
||||
void newNotificationList(ActivityList);
|
||||
|
||||
public slots:
|
||||
void slotFetchNotifications();
|
||||
|
||||
private slots:
|
||||
void slotNotificationsReceived(const QJsonDocument &json, int statusCode);
|
||||
void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
|
||||
void slotIconDownloaded(QByteArray iconData);
|
||||
|
||||
private:
|
||||
QPointer<JsonApiJob> _notificationJob;
|
||||
AccountState *_accountState;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // SERVERNOTIFICATIONHANDLER_H
|
|
@ -23,7 +23,6 @@
|
|||
#include "configfile.h"
|
||||
#include "progressdispatcher.h"
|
||||
#include "owncloudgui.h"
|
||||
#include "activitywidget.h"
|
||||
#include "accountmanager.h"
|
||||
|
||||
#include <QLabel>
|
||||
|
@ -189,39 +188,13 @@ void SettingsDialog::showFirstPage()
|
|||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::showActivityPage()
|
||||
{
|
||||
if (auto account = qvariant_cast<AccountState*>(sender()->property("account"))) {
|
||||
_activitySettings[account]->show();
|
||||
_ui->stack->setCurrentWidget(_activitySettings[account]);
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::showIssuesList(AccountState *account) {
|
||||
for (auto it = _actionGroupWidgets.begin(); it != _actionGroupWidgets.end(); ++it) {
|
||||
/*for (auto it = _actionGroupWidgets.begin(); it != _actionGroupWidgets.end(); ++it) {
|
||||
if (it.value() == _activitySettings[account]) {
|
||||
it.key()->activate(QAction::ActionEvent::Trigger);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::activityAdded(AccountState *s){
|
||||
_ui->stack->addWidget(_activitySettings[s]);
|
||||
connect(_activitySettings[s], &ActivitySettings::guiLog, _gui,
|
||||
&ownCloudGui::slotShowOptionalTrayMessage);
|
||||
|
||||
ConfigFile cfg;
|
||||
_activitySettings[s]->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
|
||||
|
||||
// Note: all the actions have a '\n' because the account name is in two lines and
|
||||
// all buttons must have the same size in order to keep a good layout
|
||||
QAction *action = createColorAwareAction(QLatin1String(":/client/resources/activity.png"), tr("Activity"));
|
||||
action->setProperty("account", QVariant::fromValue(s));
|
||||
_toolBar->insertAction(_actionBefore, action);
|
||||
_actionGroup->addAction(action);
|
||||
_actionGroupWidgets.insert(action, _activitySettings[s]);
|
||||
connect(action, &QAction::triggered, this, &SettingsDialog::showActivityPage);
|
||||
}*/
|
||||
}
|
||||
|
||||
void SettingsDialog::accountAdded(AccountState *s)
|
||||
|
@ -229,14 +202,6 @@ void SettingsDialog::accountAdded(AccountState *s)
|
|||
auto height = _toolBar->sizeHint().height();
|
||||
bool brandingSingleAccount = !Theme::instance()->multiAccount();
|
||||
|
||||
_activitySettings[s] = new ActivitySettings(s, this);
|
||||
|
||||
// if this is not the first account, then before we continue to add more accounts we add a separator
|
||||
if(AccountManager::instance()->accounts().first().data() != s &&
|
||||
AccountManager::instance()->accounts().size() >= 1){
|
||||
_actionGroupWidgets.insert(_toolBar->insertSeparator(_actionBefore), _activitySettings[s]);
|
||||
}
|
||||
|
||||
QAction *accountAction;
|
||||
QImage avatar = s->account()->avatar();
|
||||
const QString actionText = brandingSingleAccount ? tr("Account") : s->account()->displayName();
|
||||
|
@ -264,19 +229,11 @@ void SettingsDialog::accountAdded(AccountState *s)
|
|||
connect(accountSettings, &AccountSettings::folderChanged, _gui, &ownCloudGui::slotFoldersChanged);
|
||||
connect(accountSettings, &AccountSettings::openFolderAlias,
|
||||
_gui, &ownCloudGui::slotFolderOpenAction);
|
||||
connect(accountSettings, &AccountSettings::showIssuesList, this, &SettingsDialog::showIssuesList);
|
||||
connect(s->account().data(), &Account::accountChangedAvatar, this, &SettingsDialog::slotAccountAvatarChanged);
|
||||
connect(s->account().data(), &Account::accountChangedDisplayName, this, &SettingsDialog::slotAccountDisplayNameChanged);
|
||||
|
||||
// Refresh immediatly when getting online
|
||||
connect(s, &AccountState::isConnectedChanged, this, &SettingsDialog::slotRefreshActivityAccountStateSender);
|
||||
|
||||
// Connect styleChanged event, to adapt (Dark-/Light-Mode switching)
|
||||
connect(this, &SettingsDialog::styleChanged, accountSettings, &AccountSettings::slotStyleChanged);
|
||||
connect(this, &SettingsDialog::styleChanged, _activitySettings[s], &ActivitySettings::slotStyleChanged);
|
||||
|
||||
activityAdded(s);
|
||||
slotRefreshActivity(s);
|
||||
}
|
||||
|
||||
void SettingsDialog::slotAccountAvatarChanged()
|
||||
|
@ -332,19 +289,6 @@ void SettingsDialog::accountRemoved(AccountState *s)
|
|||
_actionForAccount.remove(s->account().data());
|
||||
}
|
||||
|
||||
if(_activitySettings.contains(s)){
|
||||
_activitySettings[s]->slotRemoveAccount();
|
||||
_activitySettings[s]->hide();
|
||||
|
||||
// get the settings widget and the separator
|
||||
foreach(QAction *action, _actionGroupWidgets.keys(_activitySettings[s])){
|
||||
_actionGroupWidgets.remove(action);
|
||||
_toolBar->removeAction(action);
|
||||
}
|
||||
_toolBar->widgetForAction(_actionBefore)->hide();
|
||||
_activitySettings.remove(s);
|
||||
}
|
||||
|
||||
// Hide when the last account is deleted. We want to enter the same
|
||||
// state we'd be in the client was started up without an account
|
||||
// configured.
|
||||
|
@ -414,15 +358,4 @@ QAction *SettingsDialog::createColorAwareAction(const QString &iconPath, const Q
|
|||
return createActionWithIcon(coloredIcon, text, iconPath);
|
||||
}
|
||||
|
||||
void SettingsDialog::slotRefreshActivityAccountStateSender()
|
||||
{
|
||||
slotRefreshActivity(qobject_cast<AccountState*>(sender()));
|
||||
}
|
||||
|
||||
void SettingsDialog::slotRefreshActivity(AccountState *accountState)
|
||||
{
|
||||
if (accountState->isConnected())
|
||||
_activitySettings[accountState]->slotRefresh();
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
|
|
@ -37,7 +37,6 @@ class AccountSettings;
|
|||
class Application;
|
||||
class FolderMan;
|
||||
class ownCloudGui;
|
||||
class ActivitySettings;
|
||||
|
||||
/**
|
||||
* @brief The SettingsDialog class
|
||||
|
@ -55,11 +54,8 @@ public:
|
|||
|
||||
public slots:
|
||||
void showFirstPage();
|
||||
void showActivityPage();
|
||||
void showIssuesList(AccountState *account);
|
||||
void slotSwitchPage(QAction *action);
|
||||
void slotRefreshActivity(AccountState *accountState);
|
||||
void slotRefreshActivityAccountStateSender();
|
||||
void slotAccountAvatarChanged();
|
||||
void slotAccountDisplayNameChanged();
|
||||
|
||||
|
@ -78,7 +74,6 @@ private slots:
|
|||
|
||||
private:
|
||||
void customizeStyle();
|
||||
void activityAdded(AccountState *);
|
||||
|
||||
QAction *createColorAwareAction(const QString &iconName, const QString &fileName);
|
||||
QAction *createActionWithIcon(const QIcon &icon, const QString &text, const QString &iconPath = QString());
|
||||
|
@ -95,7 +90,6 @@ private:
|
|||
QHash<Account *, QAction *> _actionForAccount;
|
||||
|
||||
QToolBar *_toolBar;
|
||||
QMap<AccountState *, ActivitySettings *> _activitySettings;
|
||||
|
||||
ownCloudGui *_gui;
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>693</width>
|
||||
<width>516</width>
|
||||
<height>457</height>
|
||||
</rect>
|
||||
</property>
|
||||
|
|
|
@ -12,9 +12,17 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "systray.h"
|
||||
#include "theme.h"
|
||||
#include "config.h"
|
||||
#include "tray/UserModel.h"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlComponent>
|
||||
#include <QQmlEngine>
|
||||
#include <QScreen>
|
||||
|
||||
#ifdef USE_FDO_NOTIFICATIONS
|
||||
#include <QDBusConnection>
|
||||
|
@ -28,10 +36,76 @@
|
|||
|
||||
namespace OCC {
|
||||
|
||||
Systray *Systray::_instance = nullptr;
|
||||
|
||||
Systray *Systray::instance()
|
||||
{
|
||||
if (_instance == nullptr) {
|
||||
_instance = new Systray();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
Systray::Systray()
|
||||
: _isOpen(false)
|
||||
, _syncIsPaused(false)
|
||||
, _trayComponent(nullptr)
|
||||
, _trayContext(nullptr)
|
||||
{
|
||||
// Create QML tray engine, build component, set C++ backend context used in window.qml
|
||||
// Use pointer instead of engine() helper function until Qt 5.12 is minimum standard
|
||||
_trayEngine = new QQmlEngine;
|
||||
_trayEngine->addImageProvider("avatars", new ImageProvider);
|
||||
_trayEngine->rootContext()->setContextProperty("userModelBackend", UserModel::instance());
|
||||
_trayEngine->rootContext()->setContextProperty("appsMenuModelBackend", UserAppsModel::instance());
|
||||
_trayEngine->rootContext()->setContextProperty("systrayBackend", this);
|
||||
|
||||
_trayComponent = new QQmlComponent(_trayEngine, QUrl(QStringLiteral("qrc:/qml/src/gui/tray/Window.qml")));
|
||||
|
||||
connect(UserModel::instance(), &UserModel::newUserSelected,
|
||||
this, &Systray::slotNewUserSelected);
|
||||
|
||||
connect(AccountManager::instance(), &AccountManager::accountAdded,
|
||||
this, &Systray::showWindow);
|
||||
}
|
||||
|
||||
void Systray::create()
|
||||
{
|
||||
if (_trayContext == nullptr) {
|
||||
_trayEngine->rootContext()->setContextProperty("activityModel", UserModel::instance()->currentActivityModel());
|
||||
_trayContext = _trayEngine->contextForObject(_trayComponent->create());
|
||||
hideWindow();
|
||||
}
|
||||
}
|
||||
|
||||
void Systray::slotNewUserSelected()
|
||||
{
|
||||
// Change ActivityModel
|
||||
_trayEngine->rootContext()->setContextProperty("activityModel", UserModel::instance()->currentActivityModel());
|
||||
|
||||
// Rebuild App list
|
||||
UserAppsModel::instance()->buildAppList();
|
||||
}
|
||||
|
||||
bool Systray::isOpen()
|
||||
{
|
||||
return _isOpen;
|
||||
}
|
||||
|
||||
Q_INVOKABLE void Systray::setOpened()
|
||||
{
|
||||
_isOpen = true;
|
||||
}
|
||||
|
||||
Q_INVOKABLE void Systray::setClosed()
|
||||
{
|
||||
_isOpen = false;
|
||||
}
|
||||
|
||||
void Systray::showMessage(const QString &title, const QString &message, MessageIcon icon, int millisecondsTimeoutHint)
|
||||
{
|
||||
#ifdef USE_FDO_NOTIFICATIONS
|
||||
if(QDBusInterface(NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE).isValid()) {
|
||||
if (QDBusInterface(NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE).isValid()) {
|
||||
QList<QVariant> args = QList<QVariant>() << APPLICATION_NAME << quint32(0) << APPLICATION_ICON_NAME
|
||||
<< title << message << QStringList() << QVariantMap() << qint32(-1);
|
||||
QDBusMessage method = QDBusMessage::createMethodCall(NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE, "Notify");
|
||||
|
@ -54,4 +128,95 @@ void Systray::setToolTip(const QString &tip)
|
|||
QSystemTrayIcon::setToolTip(tr("%1: %2").arg(Theme::instance()->appNameGUI(), tip));
|
||||
}
|
||||
|
||||
int Systray::calcTrayWindowX()
|
||||
{
|
||||
#ifdef Q_OS_OSX
|
||||
// macOS handles DPI awareness differently
|
||||
// and menu bar is always at the top, icons starting from the right
|
||||
|
||||
QPoint topLeft = this->geometry().topLeft();
|
||||
QPoint topRight = this->geometry().topRight();
|
||||
int trayIconTopCenterX = (topRight - ((topRight - topLeft) * 0.5)).x();
|
||||
return trayIconTopCenterX - (400 * 0.5);
|
||||
#else
|
||||
QScreen *trayScreen = QGuiApplication::primaryScreen();
|
||||
int screenWidth = trayScreen->geometry().width();
|
||||
int screenHeight = trayScreen->geometry().height();
|
||||
int availableWidth = trayScreen->availableGeometry().width();
|
||||
int availableHeight = trayScreen->availableGeometry().height();
|
||||
QPoint topRightDpiAware = this->geometry().topRight() / trayScreen->devicePixelRatio();
|
||||
QPoint topLeftDpiAware = this->geometry().topLeft() / trayScreen->devicePixelRatio();
|
||||
|
||||
// get coordinates from top center point of tray icon
|
||||
int trayIconTopCenterX = (topRightDpiAware - ((topRightDpiAware - topLeftDpiAware) * 0.5)).x();
|
||||
int trayIconTopCenterY = (topRightDpiAware - ((topRightDpiAware - topLeftDpiAware) * 0.5)).y();
|
||||
|
||||
if (availableHeight < screenHeight) {
|
||||
// taskbar is on top or bottom
|
||||
if (trayIconTopCenterX + (400 * 0.5) > availableWidth) {
|
||||
return availableWidth - 400 - 12;
|
||||
} else {
|
||||
return trayIconTopCenterX - (400 * 0.5);
|
||||
}
|
||||
} else {
|
||||
if (trayScreen->availableGeometry().x() > trayScreen->geometry().x()) {
|
||||
// on the left
|
||||
return (screenWidth - availableWidth) + 6;
|
||||
} else {
|
||||
// on the right
|
||||
return screenWidth - 400 - (screenWidth - availableWidth) - 6;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
int Systray::calcTrayWindowY()
|
||||
{
|
||||
#ifdef Q_OS_OSX
|
||||
// macOS menu bar is always 22 (effective) pixels
|
||||
// don't use availableGeometry() here, because this also excludes the dock
|
||||
return 22+6;
|
||||
#else
|
||||
QScreen *trayScreen = QGuiApplication::primaryScreen();
|
||||
int screenWidth = trayScreen->geometry().width();
|
||||
int screenHeight = trayScreen->geometry().height();
|
||||
int availableHeight = trayScreen->availableGeometry().height();
|
||||
QPoint topRightDpiAware = this->geometry().topRight() / trayScreen->devicePixelRatio();
|
||||
QPoint topLeftDpiAware = this->geometry().topLeft() / trayScreen->devicePixelRatio();
|
||||
|
||||
// get coordinates from top center point of tray icon
|
||||
int trayIconTopCenterX = (topRightDpiAware - ((topRightDpiAware - topLeftDpiAware) * 0.5)).x();
|
||||
int trayIconTopCenterY = (topRightDpiAware - ((topRightDpiAware - topLeftDpiAware) * 0.5)).y();
|
||||
|
||||
if (availableHeight < screenHeight) {
|
||||
// taskbar is on top or bottom
|
||||
if (trayScreen->availableGeometry().y() > trayScreen->geometry().y()) {
|
||||
// on top
|
||||
return (screenHeight - availableHeight) + 6;
|
||||
} else {
|
||||
// on bottom
|
||||
return screenHeight - 510 - (screenHeight - availableHeight) - 6;
|
||||
}
|
||||
} else {
|
||||
// on the left or right
|
||||
return (trayIconTopCenterY - 510 + 12);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Systray::syncIsPaused()
|
||||
{
|
||||
return _syncIsPaused;
|
||||
}
|
||||
|
||||
void Systray::pauseResumeSync()
|
||||
{
|
||||
if (_syncIsPaused) {
|
||||
_syncIsPaused = false;
|
||||
emit resumeSync();
|
||||
} else {
|
||||
_syncIsPaused = true;
|
||||
emit pauseSync();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
#define SYSTRAY_H
|
||||
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QQmlContext>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "tray/UserModel.h"
|
||||
|
||||
class QIcon;
|
||||
|
||||
|
@ -26,16 +30,56 @@ bool canOsXSendUserNotification();
|
|||
void sendOsXUserNotification(const QString &title, const QString &message);
|
||||
#endif
|
||||
|
||||
namespace Ui {
|
||||
class Systray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The Systray class
|
||||
* @ingroup gui
|
||||
*/
|
||||
class Systray : public QSystemTrayIcon
|
||||
class Systray
|
||||
: public QSystemTrayIcon
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static Systray *instance();
|
||||
virtual ~Systray() {};
|
||||
|
||||
void create();
|
||||
void showMessage(const QString &title, const QString &message, MessageIcon icon = Information, int millisecondsTimeoutHint = 10000);
|
||||
void setToolTip(const QString &tip);
|
||||
bool isOpen();
|
||||
|
||||
Q_INVOKABLE void pauseResumeSync();
|
||||
Q_INVOKABLE int calcTrayWindowX();
|
||||
Q_INVOKABLE int calcTrayWindowY();
|
||||
Q_INVOKABLE bool syncIsPaused();
|
||||
Q_INVOKABLE void setOpened();
|
||||
Q_INVOKABLE void setClosed();
|
||||
|
||||
signals:
|
||||
void currentUserChanged();
|
||||
void openSettings();
|
||||
void openHelp();
|
||||
void shutdown();
|
||||
void pauseSync();
|
||||
void resumeSync();
|
||||
|
||||
Q_INVOKABLE void hideWindow();
|
||||
Q_INVOKABLE void showWindow();
|
||||
|
||||
public slots:
|
||||
void slotNewUserSelected();
|
||||
|
||||
private:
|
||||
static Systray *_instance;
|
||||
Systray();
|
||||
bool _isOpen;
|
||||
bool _syncIsPaused;
|
||||
QQmlEngine *_trayEngine;
|
||||
QQmlComponent *_trayComponent;
|
||||
QQmlContext *_trayContext;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
#include <QtCore>
|
||||
|
||||
#include "activitydata.h"
|
||||
#include "ActivityData.h"
|
||||
|
||||
|
||||
namespace OCC {
|
|
@ -56,6 +56,7 @@ public:
|
|||
|
||||
Type _type;
|
||||
qlonglong _id;
|
||||
QString _fileAction;
|
||||
QString _objectType;
|
||||
QString _subject;
|
||||
QString _message;
|
||||
|
@ -64,6 +65,8 @@ public:
|
|||
QUrl _link;
|
||||
QDateTime _dateTime;
|
||||
QString _accName;
|
||||
QString _icon;
|
||||
QString _iconData;
|
||||
|
||||
// Stores information about the error
|
||||
int _status;
|
|
@ -15,7 +15,6 @@
|
|||
#include <QtCore>
|
||||
#include <QAbstractListModel>
|
||||
#include <QWidget>
|
||||
#include <QIcon>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
|
||||
|
@ -23,34 +22,44 @@
|
|||
#include "accountstate.h"
|
||||
#include "accountmanager.h"
|
||||
#include "folderman.h"
|
||||
#include "iconjob.h"
|
||||
#include "accessmanager.h"
|
||||
#include "activityitemdelegate.h"
|
||||
|
||||
#include "activitydata.h"
|
||||
#include "activitylistmodel.h"
|
||||
#include "ActivityData.h"
|
||||
#include "ActivityListModel.h"
|
||||
|
||||
#include "theme.h"
|
||||
|
||||
#include "servernotificationhandler.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcActivity, "nextcloud.gui.activity", QtInfoMsg)
|
||||
|
||||
ActivityListModel::ActivityListModel(AccountState *accountState, QWidget *parent)
|
||||
: QAbstractListModel(parent)
|
||||
ActivityListModel::ActivityListModel(AccountState *accountState, QObject *parent)
|
||||
: QAbstractListModel()
|
||||
, _accountState(accountState)
|
||||
{
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ActivityListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[DisplayPathRole] = "displaypath";
|
||||
roles[PathRole] = "path";
|
||||
roles[LinkRole] = "link";
|
||||
roles[MessageRole] = "message";
|
||||
roles[ActionRole] = "type";
|
||||
roles[ActionIconRole] = "icon";
|
||||
roles[ActionTextRole] = "subject";
|
||||
roles[ActionTextColorRole] = "activityTextTitleColor";
|
||||
roles[ObjectTypeRole] = "objectType";
|
||||
roles[PointInTimeRole] = "dateTime";
|
||||
return roles;
|
||||
}
|
||||
|
||||
QVariant ActivityListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
Activity a;
|
||||
|
||||
// filter the get action here
|
||||
// send only the text of the get action
|
||||
// if there is more than one send the icon? the ...
|
||||
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
|
@ -61,25 +70,44 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
|
|||
QStringList list;
|
||||
|
||||
switch (role) {
|
||||
case ActivityItemDelegate::PathRole:
|
||||
if(!a._file.isEmpty()){
|
||||
case DisplayPathRole:
|
||||
if (!a._file.isEmpty()) {
|
||||
auto folder = FolderMan::instance()->folder(a._folder);
|
||||
QString relPath(a._file);
|
||||
if(folder) relPath.prepend(folder->remotePath());
|
||||
if (folder) {
|
||||
relPath.prepend(folder->remotePath());
|
||||
}
|
||||
list = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
|
||||
if (list.count() > 0) {
|
||||
return QVariant(list.at(0));
|
||||
if (relPath.startsWith('/') || relPath.startsWith('\\')) {
|
||||
return relPath.remove(0, 1);
|
||||
} else {
|
||||
return relPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
case PathRole:
|
||||
if (!a._file.isEmpty()) {
|
||||
auto folder = FolderMan::instance()->folder(a._folder);
|
||||
QString relPath(a._file);
|
||||
if (folder)
|
||||
relPath.prepend(folder->remotePath());
|
||||
list = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
|
||||
if (list.count() > 0) {
|
||||
QString path = "file:///" + QString(list.at(0));
|
||||
return QUrl(path);
|
||||
}
|
||||
// File does not exist anymore? Let's try to open its path
|
||||
if(QFileInfo(relPath).exists()) {
|
||||
if (QFileInfo(relPath).exists()) {
|
||||
list = FolderMan::instance()->findFileInLocalFolders(QFileInfo(relPath).path(), ast->account());
|
||||
if (list.count() > 0) {
|
||||
return QVariant(list.at(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
case ActivityItemDelegate::ActionsLinksRole:{
|
||||
return QString();
|
||||
case ActionsLinksRole: {
|
||||
QList<QVariant> customList;
|
||||
foreach (ActivityLink customItem, a._links) {
|
||||
QVariant customVariant;
|
||||
|
@ -88,59 +116,80 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
|
|||
}
|
||||
return customList;
|
||||
}
|
||||
case ActivityItemDelegate::ActionIconRole:{
|
||||
ActionIcon actionIcon;
|
||||
if(a._type == Activity::NotificationType){
|
||||
QIcon cachedIcon = ServerNotificationHandler::iconCache.value(a._id);
|
||||
if(!cachedIcon.isNull()) {
|
||||
actionIcon.iconType = ActivityIconType::iconUseCached;
|
||||
actionIcon.cachedIcon = cachedIcon;
|
||||
case ActionIconRole: {
|
||||
if (a._type == Activity::NotificationType) {
|
||||
return "qrc:///client/theme/black/bell.svg";
|
||||
} else if (a._type == Activity::SyncResultType) {
|
||||
return "qrc:///client/theme/black/state-error.svg";
|
||||
} else if (a._type == Activity::SyncFileItemType) {
|
||||
if (a._status == SyncFileItem::NormalError
|
||||
|| a._status == SyncFileItem::FatalError
|
||||
|| a._status == SyncFileItem::DetailError
|
||||
|| a._status == SyncFileItem::BlacklistedError) {
|
||||
return "qrc:///client/theme/black/state-error.svg";
|
||||
} else if (a._status == SyncFileItem::SoftError
|
||||
|| a._status == SyncFileItem::Conflict
|
||||
|| a._status == SyncFileItem::Restoration
|
||||
|| a._status == SyncFileItem::FileLocked) {
|
||||
return "qrc:///client/theme/black/state-warning.svg";
|
||||
} else if (a._status == SyncFileItem::FileIgnored) {
|
||||
return "qrc:///client/theme/black/state-info.svg";
|
||||
} else {
|
||||
actionIcon.iconType = ActivityIconType::iconBell;
|
||||
// File sync successful
|
||||
if (a._fileAction == "file_created") {
|
||||
return "qrc:///client/resources/add-color.svg";
|
||||
} else if (a._fileAction == "file_deleted") {
|
||||
return "qrc:///client/resources/delete-color.svg";
|
||||
} else {
|
||||
return "qrc:///client/resources/change.svg";
|
||||
}
|
||||
}
|
||||
} else if(a._type == Activity::SyncResultType){
|
||||
actionIcon.iconType = ActivityIconType::iconStateError;
|
||||
} else if(a._type == Activity::SyncFileItemType){
|
||||
if(a._status == SyncFileItem::NormalError
|
||||
|| a._status == SyncFileItem::FatalError
|
||||
|| a._status == SyncFileItem::DetailError
|
||||
|| a._status == SyncFileItem::BlacklistedError) {
|
||||
actionIcon.iconType = ActivityIconType::iconStateError;
|
||||
} else if(a._status == SyncFileItem::SoftError
|
||||
|| a._status == SyncFileItem::Conflict
|
||||
|| a._status == SyncFileItem::Restoration
|
||||
|| a._status == SyncFileItem::FileLocked){
|
||||
actionIcon.iconType = ActivityIconType::iconStateWarning;
|
||||
} else if(a._status == SyncFileItem::FileIgnored){
|
||||
actionIcon.iconType = ActivityIconType::iconStateInfo;
|
||||
} else {
|
||||
actionIcon.iconType = ActivityIconType::iconStateSync;
|
||||
}
|
||||
} else {
|
||||
actionIcon.iconType = ActivityIconType::iconActivity;
|
||||
// We have an activity
|
||||
if (!a._iconData.isEmpty()) {
|
||||
QString svgData = "data:image/svg+xml;utf8," + a._iconData;
|
||||
return svgData;
|
||||
}
|
||||
return "qrc:///client/theme/black/activity.svg";
|
||||
}
|
||||
QVariant icn;
|
||||
icn.setValue(actionIcon);
|
||||
return icn;
|
||||
}
|
||||
case ActivityItemDelegate::ObjectTypeRole:
|
||||
case ObjectTypeRole:
|
||||
return a._objectType;
|
||||
case ActivityItemDelegate::ActionRole:{
|
||||
QVariant type;
|
||||
type.setValue(a._type);
|
||||
return type;
|
||||
case ActionRole: {
|
||||
switch (a._type) {
|
||||
case Activity::ActivityType:
|
||||
return "Activity";
|
||||
case Activity::NotificationType:
|
||||
return "Notification";
|
||||
case Activity::SyncFileItemType:
|
||||
return "File";
|
||||
case Activity::SyncResultType:
|
||||
return "Sync";
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
case ActivityItemDelegate::ActionTextRole:
|
||||
case ActionTextRole:
|
||||
return a._subject;
|
||||
case ActivityItemDelegate::MessageRole:
|
||||
case ActionTextColorRole:
|
||||
return a._id == -1 ? QLatin1String("#808080") : QLatin1String("#222"); // FIXME: This is a temporary workaround for _showMoreActivitiesAvailableEntry
|
||||
case MessageRole:
|
||||
if (a._message.isEmpty()) {
|
||||
return QString("No description available.");
|
||||
}
|
||||
return a._message;
|
||||
case ActivityItemDelegate::LinkRole:
|
||||
return a._link;
|
||||
case ActivityItemDelegate::AccountRole:
|
||||
case LinkRole: {
|
||||
if (a._link.isEmpty()) {
|
||||
return "";
|
||||
} else {
|
||||
return a._link;
|
||||
}
|
||||
}
|
||||
case AccountRole:
|
||||
return a._accName;
|
||||
case ActivityItemDelegate::PointInTimeRole:
|
||||
return QString("%1 (%2)").arg(a._dateTime.toLocalTime().toString(Qt::DefaultLocaleShortDate), Utility::timeAgoInWords(a._dateTime.toLocalTime()));
|
||||
case ActivityItemDelegate::AccountConnectedRole:
|
||||
case PointInTimeRole:
|
||||
return a._id == -1 ? "" : QString("%1 - %2").arg(Utility::timeAgoInWords(a._dateTime.toLocalTime()), a._dateTime.toLocalTime().toString(Qt::DefaultLocaleShortDate));
|
||||
case AccountConnectedRole:
|
||||
return (ast && ast->isConnected());
|
||||
default:
|
||||
return QVariant();
|
||||
|
@ -171,13 +220,13 @@ void ActivityListModel::startFetchJob()
|
|||
if (!_accountState->isConnected()) {
|
||||
return;
|
||||
}
|
||||
JsonApiJob *job = new JsonApiJob(_accountState->account(), QLatin1String("ocs/v2.php/cloud/activity"), this);
|
||||
JsonApiJob *job = new JsonApiJob(_accountState->account(), QLatin1String("ocs/v2.php/apps/activity/api/v2/activity"), this);
|
||||
QObject::connect(job, &JsonApiJob::jsonReceived,
|
||||
this, &ActivityListModel::slotActivitiesReceived);
|
||||
|
||||
QUrlQuery params;
|
||||
params.addQueryItem(QLatin1String("start"), QString::number(_currentItem));
|
||||
params.addQueryItem(QLatin1String("count"), QString::number(100));
|
||||
params.addQueryItem(QLatin1String("since"), QString::number(_currentItem));
|
||||
params.addQueryItem(QLatin1String("limit"), QString::number(50));
|
||||
job->addQueryParams(params);
|
||||
|
||||
_currentlyFetching = true;
|
||||
|
@ -200,21 +249,43 @@ void ActivityListModel::slotActivitiesReceived(const QJsonDocument &json, int st
|
|||
}
|
||||
|
||||
_currentlyFetching = false;
|
||||
_currentItem += activities.size();
|
||||
|
||||
QDateTime oldestDate = QDateTime::currentDateTime();
|
||||
oldestDate = oldestDate.addDays(_maxActivitiesDays * -1);
|
||||
|
||||
foreach (auto activ, activities) {
|
||||
auto json = activ.toObject();
|
||||
|
||||
Activity a;
|
||||
a._type = Activity::ActivityType;
|
||||
a._objectType = json.value("object_type").toString();
|
||||
a._accName = ast->account()->displayName();
|
||||
a._id = json.value("id").toInt();
|
||||
a._id = json.value("activity_id").toInt();
|
||||
a._fileAction = json.value("type").toString();
|
||||
a._subject = json.value("subject").toString();
|
||||
a._message = json.value("message").toString();
|
||||
a._file = json.value("file").toString();
|
||||
a._file = json.value("object_name").toString();
|
||||
a._link = QUrl(json.value("link").toString());
|
||||
a._dateTime = QDateTime::fromString(json.value("date").toString(), Qt::ISODate);
|
||||
a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate);
|
||||
a._icon = json.value("icon").toString();
|
||||
|
||||
if (!a._icon.isEmpty()) {
|
||||
IconJob *iconJob = new IconJob(QUrl(a._icon));
|
||||
iconJob->setProperty("activityId", a._id);
|
||||
connect(iconJob, &IconJob::jobFinished, this, &ActivityListModel::slotIconDownloaded);
|
||||
}
|
||||
|
||||
list.append(a);
|
||||
_currentItem = list.last()._id;
|
||||
|
||||
_totalActivitiesFetched++;
|
||||
if(_totalActivitiesFetched == _maxActivities ||
|
||||
a._dateTime < oldestDate) {
|
||||
|
||||
_showMoreActivitiesAvailableEntry = true;
|
||||
_doneFetching = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_activityLists.append(list);
|
||||
|
@ -224,76 +295,95 @@ void ActivityListModel::slotActivitiesReceived(const QJsonDocument &json, int st
|
|||
combineActivityLists();
|
||||
}
|
||||
|
||||
void ActivityListModel::addErrorToActivityList(Activity activity) {
|
||||
void ActivityListModel::slotIconDownloaded(QByteArray iconData)
|
||||
{
|
||||
for (size_t i = 0; i < _activityLists.count(); i++) {
|
||||
if (_activityLists[i]._id == sender()->property("activityId").toLongLong()) {
|
||||
_activityLists[i]._iconData = iconData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityListModel::addErrorToActivityList(Activity activity)
|
||||
{
|
||||
qCInfo(lcActivity) << "Error successfully added to the notification list: " << activity._subject;
|
||||
_notificationErrorsLists.prepend(activity);
|
||||
combineActivityLists();
|
||||
}
|
||||
|
||||
void ActivityListModel::addIgnoredFileToList(Activity newActivity) {
|
||||
void ActivityListModel::addIgnoredFileToList(Activity newActivity)
|
||||
{
|
||||
qCInfo(lcActivity) << "First checking for duplicates then add file to the notification list of ignored files: " << newActivity._file;
|
||||
|
||||
bool duplicate = false;
|
||||
if(_listOfIgnoredFiles.size() == 0){
|
||||
if (_listOfIgnoredFiles.size() == 0) {
|
||||
_notificationIgnoredFiles = newActivity;
|
||||
_notificationIgnoredFiles._subject = tr("Files from the ignore list as well as symbolic links are not synced. This includes:");
|
||||
_listOfIgnoredFiles.append(newActivity);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(Activity activity, _listOfIgnoredFiles){
|
||||
if(activity._file == newActivity._file){
|
||||
foreach (Activity activity, _listOfIgnoredFiles) {
|
||||
if (activity._file == newActivity._file) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!duplicate){
|
||||
if (!duplicate) {
|
||||
_notificationIgnoredFiles._message.append(", " + newActivity._file);
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityListModel::addNotificationToActivityList(Activity activity) {
|
||||
void ActivityListModel::addNotificationToActivityList(Activity activity)
|
||||
{
|
||||
qCInfo(lcActivity) << "Notification successfully added to the notification list: " << activity._subject;
|
||||
_notificationLists.prepend(activity);
|
||||
combineActivityLists();
|
||||
}
|
||||
|
||||
void ActivityListModel::clearNotifications() {
|
||||
void ActivityListModel::clearNotifications()
|
||||
{
|
||||
qCInfo(lcActivity) << "Clear the notifications";
|
||||
_notificationLists.clear();
|
||||
combineActivityLists();
|
||||
}
|
||||
|
||||
void ActivityListModel::removeActivityFromActivityList(int row) {
|
||||
void ActivityListModel::removeActivityFromActivityList(int row)
|
||||
{
|
||||
Activity activity = _finalList.at(row);
|
||||
removeActivityFromActivityList(activity);
|
||||
combineActivityLists();
|
||||
}
|
||||
|
||||
void ActivityListModel::addSyncFileItemToActivityList(Activity activity) {
|
||||
void ActivityListModel::addSyncFileItemToActivityList(Activity activity)
|
||||
{
|
||||
qCInfo(lcActivity) << "Successfully added to the activity list: " << activity._subject;
|
||||
_syncFileItemLists.prepend(activity);
|
||||
combineActivityLists();
|
||||
}
|
||||
|
||||
void ActivityListModel::removeActivityFromActivityList(Activity activity) {
|
||||
void ActivityListModel::removeActivityFromActivityList(Activity activity)
|
||||
{
|
||||
qCInfo(lcActivity) << "Activity/Notification/Error successfully dismissed: " << activity._subject;
|
||||
qCInfo(lcActivity) << "Trying to remove Activity/Notification/Error from view... ";
|
||||
|
||||
int index = -1;
|
||||
if(activity._type == Activity::ActivityType){
|
||||
if (activity._type == Activity::ActivityType) {
|
||||
index = _activityLists.indexOf(activity);
|
||||
if(index != -1) _activityLists.removeAt(index);
|
||||
} else if(activity._type == Activity::NotificationType){
|
||||
if (index != -1)
|
||||
_activityLists.removeAt(index);
|
||||
} else if (activity._type == Activity::NotificationType) {
|
||||
index = _notificationLists.indexOf(activity);
|
||||
if(index != -1) _notificationLists.removeAt(index);
|
||||
if (index != -1)
|
||||
_notificationLists.removeAt(index);
|
||||
} else {
|
||||
index = _notificationErrorsLists.indexOf(activity);
|
||||
if(index != -1) _notificationErrorsLists.removeAt(index);
|
||||
if (index != -1)
|
||||
_notificationErrorsLists.removeAt(index);
|
||||
}
|
||||
|
||||
if(index != -1){
|
||||
if (index != -1) {
|
||||
qCInfo(lcActivity) << "Activity/Notification/Error successfully removed from the list.";
|
||||
qCInfo(lcActivity) << "Updating Activity/Notification/Error view.";
|
||||
combineActivityLists();
|
||||
|
@ -304,38 +394,57 @@ void ActivityListModel::combineActivityLists()
|
|||
{
|
||||
ActivityList resultList;
|
||||
|
||||
if(_notificationErrorsLists.count() > 0) {
|
||||
if (_notificationErrorsLists.count() > 0) {
|
||||
std::sort(_notificationErrorsLists.begin(), _notificationErrorsLists.end());
|
||||
resultList.append(_notificationErrorsLists);
|
||||
}
|
||||
if(_listOfIgnoredFiles.size() > 0)
|
||||
if (_listOfIgnoredFiles.size() > 0)
|
||||
resultList.append(_notificationIgnoredFiles);
|
||||
|
||||
if(_notificationLists.count() > 0) {
|
||||
if (_notificationLists.count() > 0) {
|
||||
std::sort(_notificationLists.begin(), _notificationLists.end());
|
||||
resultList.append(_notificationLists);
|
||||
}
|
||||
|
||||
if(_syncFileItemLists.count() > 0) {
|
||||
if (_syncFileItemLists.count() > 0) {
|
||||
std::sort(_syncFileItemLists.begin(), _syncFileItemLists.end());
|
||||
resultList.append(_syncFileItemLists);
|
||||
}
|
||||
|
||||
if(_activityLists.count() > 0) {
|
||||
if (_activityLists.count() > 0) {
|
||||
std::sort(_activityLists.begin(), _activityLists.end());
|
||||
resultList.append(_activityLists);
|
||||
|
||||
if(_showMoreActivitiesAvailableEntry) {
|
||||
Activity a;
|
||||
a._type = Activity::ActivityType;
|
||||
a._accName = _accountState->account()->displayName();
|
||||
a._id = -1;
|
||||
a._subject = tr("For more activities please open the Activity app.");
|
||||
a._dateTime = QDateTime::currentDateTime();
|
||||
|
||||
AccountApp *app = _accountState->findApp(QLatin1String("activity"));
|
||||
if(app) {
|
||||
a._link = app->url();
|
||||
}
|
||||
|
||||
resultList.append(a);
|
||||
}
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
_finalList.clear();
|
||||
endResetModel();
|
||||
|
||||
beginInsertRows(QModelIndex(), 0, resultList.count());
|
||||
_finalList = resultList;
|
||||
endInsertRows();
|
||||
if (resultList.count() > 0) {
|
||||
beginInsertRows(QModelIndex(), 0, resultList.count() - 1);
|
||||
_finalList = resultList;
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
|
||||
bool ActivityListModel::canFetchActivities() const {
|
||||
bool ActivityListModel::canFetchActivities() const
|
||||
{
|
||||
return _accountState->isConnected() && _accountState->account()->capabilities().hasActivities();
|
||||
}
|
||||
|
||||
|
@ -354,6 +463,8 @@ void ActivityListModel::slotRefreshActivity()
|
|||
_activityLists.clear();
|
||||
_doneFetching = false;
|
||||
_currentItem = 0;
|
||||
_totalActivitiesFetched = 0;
|
||||
_showMoreActivitiesAvailableEntry = false;
|
||||
|
||||
if (canFetchActivities()) {
|
||||
startFetchJob();
|
||||
|
@ -370,5 +481,7 @@ void ActivityListModel::slotRemoveAccount()
|
|||
_currentlyFetching = false;
|
||||
_doneFetching = false;
|
||||
_currentItem = 0;
|
||||
_totalActivitiesFetched = 0;
|
||||
_showMoreActivitiesAvailableEntry = false;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
#include <QtCore>
|
||||
|
||||
#include "activitydata.h"
|
||||
#include "ActivityData.h"
|
||||
|
||||
class QJsonDocument;
|
||||
|
||||
|
@ -38,21 +38,24 @@ class ActivityListModel : public QAbstractListModel
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum ActivityIconType {
|
||||
iconUseCached = 0,
|
||||
iconActivity,
|
||||
iconBell,
|
||||
iconStateError,
|
||||
iconStateWarning,
|
||||
iconStateInfo,
|
||||
iconStateSync
|
||||
};
|
||||
struct ActionIcon {
|
||||
ActivityIconType iconType;
|
||||
QIcon cachedIcon;
|
||||
};
|
||||
enum DataRole {
|
||||
ActionIconRole = Qt::UserRole + 1,
|
||||
UserIconRole,
|
||||
AccountRole,
|
||||
ObjectTypeRole,
|
||||
ActionsLinksRole,
|
||||
ActionTextRole,
|
||||
ActionTextColorRole,
|
||||
ActionRole,
|
||||
MessageRole,
|
||||
DisplayPathRole,
|
||||
PathRole,
|
||||
LinkRole,
|
||||
PointInTimeRole,
|
||||
AccountConnectedRole,
|
||||
SyncFileStatusRole};
|
||||
|
||||
explicit ActivityListModel(AccountState *accountState, QWidget *parent = nullptr);
|
||||
explicit ActivityListModel(AccountState *accountState, QObject* parent = 0);
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
@ -76,10 +79,14 @@ public slots:
|
|||
|
||||
private slots:
|
||||
void slotActivitiesReceived(const QJsonDocument &json, int statusCode);
|
||||
void slotIconDownloaded(QByteArray iconData);
|
||||
|
||||
signals:
|
||||
void activityJobStatusCode(int statusCode);
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
void startFetchJob();
|
||||
void combineActivityLists();
|
||||
|
@ -96,9 +103,12 @@ private:
|
|||
bool _currentlyFetching = false;
|
||||
bool _doneFetching = false;
|
||||
int _currentItem = 0;
|
||||
|
||||
int _totalActivitiesFetched = 0;
|
||||
int _maxActivities = 100;
|
||||
int _maxActivitiesDays = 30;
|
||||
bool _showMoreActivitiesAvailableEntry = false;
|
||||
};
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::ActivityListModel::ActionIcon)
|
||||
|
||||
#endif // ACTIVITYLISTMODEL_H
|
|
@ -1,18 +1,5 @@
|
|||
/*
|
||||
* 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 "NotificationHandler.h"
|
||||
|
||||
#include "servernotificationhandler.h"
|
||||
#include "accountstate.h"
|
||||
#include "capabilities.h"
|
||||
#include "networkjobs.h"
|
||||
|
@ -30,7 +17,7 @@ const QString notificationsPath = QLatin1String("ocs/v2.php/apps/notifications/a
|
|||
const char propertyAccountStateC[] = "oc_account_state";
|
||||
const int successStatusCode = 200;
|
||||
const int notModifiedStatusCode = 304;
|
||||
QMap<int, QIcon> ServerNotificationHandler::iconCache;
|
||||
QMap<int, QByteArray> ServerNotificationHandler::iconCache;
|
||||
|
||||
ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState, QObject *parent)
|
||||
: QObject(parent)
|
||||
|
@ -41,9 +28,7 @@ ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState,
|
|||
void ServerNotificationHandler::slotFetchNotifications()
|
||||
{
|
||||
// check connectivity and credentials
|
||||
if (!(_accountState && _accountState->isConnected() &&
|
||||
_accountState->account() && _accountState->account()->credentials() &&
|
||||
_accountState->account()->credentials()->ready())) {
|
||||
if (!(_accountState && _accountState->isConnected() && _accountState->account() && _accountState->account()->credentials() && _accountState->account()->credentials()->ready())) {
|
||||
deleteLater();
|
||||
return;
|
||||
}
|
||||
|
@ -68,18 +53,18 @@ void ServerNotificationHandler::slotFetchNotifications()
|
|||
_notificationJob->start();
|
||||
}
|
||||
|
||||
void ServerNotificationHandler::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){
|
||||
if(statusCode == successStatusCode){
|
||||
void ServerNotificationHandler::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode)
|
||||
{
|
||||
if (statusCode == successStatusCode) {
|
||||
qCWarning(lcServerNotification) << "New Notification ETag Response Header received " << value;
|
||||
AccountState *account = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
|
||||
account->setNotificationsEtagResponseHeader(value);
|
||||
}
|
||||
}
|
||||
|
||||
void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData){
|
||||
QPixmap pixmap;
|
||||
pixmap.loadFromData(iconData);
|
||||
iconCache.insert(sender()->property("activityId").toInt(), QIcon(pixmap));
|
||||
void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData)
|
||||
{
|
||||
iconCache.insert(sender()->property("activityId").toInt(),iconData);
|
||||
}
|
||||
|
||||
void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &json, int statusCode)
|
||||
|
@ -107,7 +92,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
|
|||
auto json = element.toObject();
|
||||
a._type = Activity::NotificationType;
|
||||
a._accName = ai->account()->displayName();
|
||||
a._id = json.value("notification_id").toInt();
|
||||
a._id = json.value("activity_id").toInt();
|
||||
|
||||
//need to know, specially for remote_share
|
||||
a._objectType = json.value("object_type").toString();
|
||||
|
@ -115,16 +100,17 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
|
|||
|
||||
a._subject = json.value("subject").toString();
|
||||
a._message = json.value("message").toString();
|
||||
a._icon = json.value("icon").toString();
|
||||
|
||||
if(!json.value("icon").toString().isEmpty()){
|
||||
IconJob *iconJob = new IconJob(QUrl(json.value("icon").toString()));
|
||||
if (!a._icon.isEmpty()) {
|
||||
IconJob *iconJob = new IconJob(QUrl(a._icon));
|
||||
iconJob->setProperty("activityId", a._id);
|
||||
connect(iconJob, &IconJob::jobFinished, this, &ServerNotificationHandler::slotIconDownloaded);
|
||||
}
|
||||
|
||||
QUrl link(json.value("link").toString());
|
||||
if (!link.isEmpty()) {
|
||||
if(link.host().isEmpty()){
|
||||
if (link.host().isEmpty()) {
|
||||
link.setScheme(ai->account()->url().scheme());
|
||||
link.setHost(ai->account()->url().host());
|
||||
}
|
||||
|
@ -151,8 +137,8 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
|
|||
// https://github.com/owncloud/notifications/blob/master/docs/ocs-endpoint-v1.md#deleting-a-notification-for-a-user
|
||||
ActivityLink al;
|
||||
al._label = tr("Dismiss");
|
||||
al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString();
|
||||
al._verb = "DELETE";
|
||||
al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString();
|
||||
al._verb = "DELETE";
|
||||
al._isPrimary = false;
|
||||
a._links.append(al);
|
||||
|
||||
|
@ -162,4 +148,4 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
|
|||
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
}
|
36
src/gui/tray/NotificationHandler.h
Normal file
|
@ -0,0 +1,36 @@
|
|||
#ifndef NOTIFICATIONHANDLER_H
|
||||
#define NOTIFICATIONHANDLER_H
|
||||
|
||||
#include <QtCore>
|
||||
|
||||
#include "UserModel.h"
|
||||
|
||||
class QJsonDocument;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class ServerNotificationHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr);
|
||||
static QMap<int, QByteArray> iconCache;
|
||||
|
||||
signals:
|
||||
void newNotificationList(ActivityList);
|
||||
|
||||
public slots:
|
||||
void slotFetchNotifications();
|
||||
|
||||
private slots:
|
||||
void slotNotificationsReceived(const QJsonDocument &json, int statusCode);
|
||||
void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
|
||||
void slotIconDownloaded(QByteArray iconData);
|
||||
|
||||
private:
|
||||
QPointer<JsonApiJob> _notificationJob;
|
||||
AccountState *_accountState;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // NOTIFICATIONHANDLER_H
|
156
src/gui/tray/UserLine.qml
Normal file
|
@ -0,0 +1,156 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.2
|
||||
|
||||
MenuItem {
|
||||
id: userLine
|
||||
height: 60
|
||||
|
||||
RowLayout {
|
||||
id: userLineLayout
|
||||
spacing: 0
|
||||
width: 220
|
||||
height: 60
|
||||
|
||||
Button {
|
||||
id: accountButton
|
||||
Layout.preferredWidth: (userLineLayout.width * (5/6))
|
||||
Layout.preferredHeight: (userLineLayout.height)
|
||||
display: AbstractButton.IconOnly
|
||||
flat: true
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onContainsMouseChanged: {
|
||||
accountStateIndicatorBackground.color = (containsMouse ? "#f6f6f6" : "white")
|
||||
}
|
||||
onClicked: {
|
||||
if (!isCurrentUser) {
|
||||
userModelBackend.switchCurrentUser(id)
|
||||
} else {
|
||||
accountMenu.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: accountControlRowLayout
|
||||
height: accountButton.height
|
||||
width: accountButton.width
|
||||
spacing: 0
|
||||
Image {
|
||||
id: accountAvatar
|
||||
Layout.leftMargin: 4
|
||||
verticalAlignment: Qt.AlignCenter
|
||||
cache: false
|
||||
source: ("image://avatars/" + id)
|
||||
Layout.preferredHeight: (userLineLayout.height -16)
|
||||
Layout.preferredWidth: (userLineLayout.height -16)
|
||||
Rectangle {
|
||||
id: accountStateIndicatorBackground
|
||||
width: accountStateIndicator.sourceSize.width + 2
|
||||
height: width
|
||||
anchors.bottom: accountAvatar.bottom
|
||||
anchors.right: accountAvatar.right
|
||||
color: "white"
|
||||
radius: width*0.5
|
||||
}
|
||||
Image {
|
||||
id: accountStateIndicator
|
||||
source: isConnected ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg"
|
||||
cache: false
|
||||
x: accountStateIndicatorBackground.x + 1
|
||||
y: accountStateIndicatorBackground.y + 1
|
||||
sourceSize.width: 16
|
||||
sourceSize.height: 16
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: accountLabels
|
||||
spacing: 4
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.leftMargin: 6
|
||||
Label {
|
||||
id: accountUser
|
||||
width: 128
|
||||
text: name
|
||||
elide: Text.ElideRight
|
||||
color: "black"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
Label {
|
||||
id: accountServer
|
||||
width: 128
|
||||
text: server
|
||||
elide: Text.ElideRight
|
||||
color: "black"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
} // accountButton
|
||||
|
||||
Button {
|
||||
id: userMoreButton
|
||||
Layout.preferredWidth: (userLineLayout.width * (1/6))
|
||||
Layout.preferredHeight: userLineLayout.height
|
||||
flat: true
|
||||
|
||||
icon.source: "qrc:///client/resources/more.svg"
|
||||
icon.color: "transparent"
|
||||
|
||||
MouseArea {
|
||||
id: userMoreButtonMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked:
|
||||
{
|
||||
userMoreButtonMenu.popup()
|
||||
}
|
||||
}
|
||||
background:
|
||||
Rectangle {
|
||||
color: userMoreButtonMouseArea.containsMouse ? "grey" : "transparent"
|
||||
opacity: 0.2
|
||||
height: userMoreButton.height - 2
|
||||
y: userMoreButton.y + 1
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: userMoreButtonMenu
|
||||
width: 120
|
||||
|
||||
background: Rectangle {
|
||||
border.color: "#0082c9"
|
||||
radius: 2
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: isConnected ? qsTr("Log out") : qsTr("Log in")
|
||||
font.pixelSize: 12
|
||||
onClicked: {
|
||||
isConnected ? userModelBackend.logout(index) : userModelBackend.login(index)
|
||||
accountMenu.close()
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("Remove Account")
|
||||
font.pixelSize: 12
|
||||
onClicked: {
|
||||
userModelBackend.removeAccount(index)
|
||||
accountMenu.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // MenuItem userLine
|
868
src/gui/tray/UserModel.cpp
Normal file
|
@ -0,0 +1,868 @@
|
|||
#include "NotificationHandler.h"
|
||||
#include "UserModel.h"
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "owncloudgui.h"
|
||||
#include "syncengine.h"
|
||||
#include "ocsjob.h"
|
||||
#include "configfile.h"
|
||||
#include "notificationconfirmjob.h"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QIcon>
|
||||
#include <QMessageBox>
|
||||
#include <QSvgRenderer>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
|
||||
// time span in milliseconds which has to be between two
|
||||
// refreshes of the notifications
|
||||
#define NOTIFICATION_REQUEST_FREE_PERIOD 15000
|
||||
|
||||
namespace OCC {
|
||||
|
||||
User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _account(account)
|
||||
, _isCurrentUser(isCurrent)
|
||||
, _activityModel(new ActivityListModel(_account.data()))
|
||||
, _notificationRequestsRunning(0)
|
||||
{
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
|
||||
this, &User::slotProgressInfo);
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
|
||||
this, &User::slotItemCompleted);
|
||||
connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
|
||||
this, &User::slotAddError);
|
||||
|
||||
connect(&_notificationCheckTimer, &QTimer::timeout,
|
||||
this, &User::slotRefresh);
|
||||
|
||||
connect(_account.data(), &AccountState::stateChanged,
|
||||
[=]() { if (isConnected()) {slotRefresh();} });
|
||||
connect(_account.data(), &AccountState::hasFetchedNavigationApps,
|
||||
this, &User::slotRebuildNavigationAppList);
|
||||
}
|
||||
|
||||
void User::slotBuildNotificationDisplay(const ActivityList &list)
|
||||
{
|
||||
// Whether a new notification was added to the list
|
||||
bool newNotificationShown = false;
|
||||
|
||||
_activityModel->clearNotifications();
|
||||
|
||||
foreach (auto activity, list) {
|
||||
if (_blacklistedNotifications.contains(activity)) {
|
||||
qCInfo(lcActivity) << "Activity in blacklist, skip";
|
||||
continue;
|
||||
}
|
||||
|
||||
// handle gui logs. In order to NOT annoy the user with every fetching of the
|
||||
// notifications the notification id is stored in a Set. Only if an id
|
||||
// is not in the set, it qualifies for guiLog.
|
||||
// Important: The _guiLoggedNotifications set must be wiped regularly which
|
||||
// will repeat the gui log.
|
||||
|
||||
// after one hour, clear the gui log notification store
|
||||
if (_guiLogTimer.elapsed() > 60 * 60 * 1000) {
|
||||
_guiLoggedNotifications.clear();
|
||||
}
|
||||
|
||||
if (!_guiLoggedNotifications.contains(activity._id)) {
|
||||
newNotificationShown = true;
|
||||
_guiLoggedNotifications.insert(activity._id);
|
||||
|
||||
// Assemble a tray notification for the NEW notification
|
||||
ConfigFile cfg;
|
||||
if (cfg.optionalServerNotifications()) {
|
||||
if (AccountManager::instance()->accounts().count() == 1) {
|
||||
emit guiLog(activity._subject, "");
|
||||
} else {
|
||||
emit guiLog(activity._subject, activity._accName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_activityModel->addNotificationToActivityList(activity);
|
||||
}
|
||||
|
||||
// restart the gui log timer now that we show a new notification
|
||||
if (newNotificationShown) {
|
||||
_guiLogTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void User::setNotificationRefreshInterval(std::chrono::milliseconds interval)
|
||||
{
|
||||
qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval";
|
||||
_notificationCheckTimer.start(interval.count());
|
||||
}
|
||||
|
||||
void User::slotRefreshImmediately() {
|
||||
if (_account.data() && _account.data()->isConnected()) {
|
||||
this->slotRefreshActivities();
|
||||
}
|
||||
this->slotRefreshNotifications();
|
||||
}
|
||||
|
||||
void User::slotRefresh()
|
||||
{
|
||||
// QElapsedTimer isn't actually constructed as invalid.
|
||||
if (!_timeSinceLastCheck.contains(_account.data())) {
|
||||
_timeSinceLastCheck[_account.data()].invalidate();
|
||||
}
|
||||
QElapsedTimer &timer = _timeSinceLastCheck[_account.data()];
|
||||
|
||||
// Fetch Activities only if visible and if last check is longer than 15 secs ago
|
||||
if (timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD) {
|
||||
qCDebug(lcActivity) << "Do not check as last check is only secs ago: " << timer.elapsed() / 1000;
|
||||
return;
|
||||
}
|
||||
if (_account.data() && _account.data()->isConnected()) {
|
||||
if (!timer.isValid()) {
|
||||
this->slotRefreshActivities();
|
||||
}
|
||||
this->slotRefreshNotifications();
|
||||
timer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void User::slotRefreshActivities()
|
||||
{
|
||||
_activityModel->slotRefreshActivity();
|
||||
}
|
||||
|
||||
void User::slotRefreshNotifications()
|
||||
{
|
||||
// start a server notification handler if no notification requests
|
||||
// are running
|
||||
if (_notificationRequestsRunning == 0) {
|
||||
ServerNotificationHandler *snh = new ServerNotificationHandler(_account.data());
|
||||
connect(snh, &ServerNotificationHandler::newNotificationList,
|
||||
this, &User::slotBuildNotificationDisplay);
|
||||
|
||||
snh->slotFetchNotifications();
|
||||
} else {
|
||||
qCWarning(lcActivity) << "Notification request counter not zero.";
|
||||
}
|
||||
}
|
||||
|
||||
void User::slotRebuildNavigationAppList()
|
||||
{
|
||||
// Rebuild App list
|
||||
UserAppsModel::instance()->buildAppList();
|
||||
}
|
||||
|
||||
void User::slotNotificationRequestFinished(int statusCode)
|
||||
{
|
||||
int row = sender()->property("activityRow").toInt();
|
||||
|
||||
// the ocs API returns stat code 100 or 200 inside the xml if it succeeded.
|
||||
if (statusCode != OCS_SUCCESS_STATUS_CODE && statusCode != OCS_SUCCESS_STATUS_CODE_V2) {
|
||||
qCWarning(lcActivity) << "Notification Request to Server failed, leave notification visible.";
|
||||
} else {
|
||||
// to do use the model to rebuild the list or remove the item
|
||||
qCWarning(lcActivity) << "Notification Request to Server successed, rebuilding list.";
|
||||
_activityModel->removeActivityFromActivityList(row);
|
||||
}
|
||||
}
|
||||
|
||||
void User::slotEndNotificationRequest(int replyCode)
|
||||
{
|
||||
_notificationRequestsRunning--;
|
||||
slotNotificationRequestFinished(replyCode);
|
||||
}
|
||||
|
||||
void User::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row)
|
||||
{
|
||||
qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName;
|
||||
|
||||
const QStringList validVerbs = QStringList() << "GET"
|
||||
<< "PUT"
|
||||
<< "POST"
|
||||
<< "DELETE";
|
||||
|
||||
if (validVerbs.contains(verb)) {
|
||||
AccountStatePtr acc = AccountManager::instance()->account(accountName);
|
||||
if (acc) {
|
||||
NotificationConfirmJob *job = new NotificationConfirmJob(acc->account());
|
||||
QUrl l(link);
|
||||
job->setLinkAndVerb(l, verb);
|
||||
job->setProperty("activityRow", QVariant::fromValue(row));
|
||||
connect(job, &AbstractNetworkJob::networkError,
|
||||
this, &User::slotNotifyNetworkError);
|
||||
connect(job, &NotificationConfirmJob::jobFinished,
|
||||
this, &User::slotNotifyServerFinished);
|
||||
job->start();
|
||||
|
||||
// count the number of running notification requests. If this member var
|
||||
// is larger than zero, no new fetching of notifications is started
|
||||
_notificationRequestsRunning++;
|
||||
}
|
||||
} else {
|
||||
qCWarning(lcActivity) << "Notification Links: Invalid verb:" << verb;
|
||||
}
|
||||
}
|
||||
|
||||
void User::slotNotifyNetworkError(QNetworkReply *reply)
|
||||
{
|
||||
NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob *>(sender());
|
||||
if (!job) {
|
||||
return;
|
||||
}
|
||||
|
||||
int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
slotEndNotificationRequest(resultCode);
|
||||
qCWarning(lcActivity) << "Server notify job failed with code " << resultCode;
|
||||
}
|
||||
|
||||
void User::slotNotifyServerFinished(const QString &reply, int replyCode)
|
||||
{
|
||||
NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob *>(sender());
|
||||
if (!job) {
|
||||
return;
|
||||
}
|
||||
|
||||
slotEndNotificationRequest(replyCode);
|
||||
qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply;
|
||||
}
|
||||
|
||||
void User::slotProgressInfo(const QString &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.
|
||||
auto f = FolderMan::instance()->folder(folder);
|
||||
if (!f)
|
||||
return;
|
||||
const auto &engine = f->syncEngine();
|
||||
const auto style = engine.lastLocalDiscoveryStyle();
|
||||
foreach (Activity activity, _activityModel->errorsList()) {
|
||||
if (activity._folder != folder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (style == LocalDiscoveryStyle::FilesystemOnly) {
|
||||
_activityModel->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (activity._status == SyncFileItem::Conflict && !QFileInfo(f->path() + activity._file).exists()) {
|
||||
_activityModel->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (activity._status == SyncFileItem::FileLocked && !QFileInfo(f->path() + activity._file).exists()) {
|
||||
_activityModel->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (activity._status == SyncFileItem::FileIgnored && !QFileInfo(f->path() + activity._file).exists()) {
|
||||
_activityModel->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!QFileInfo(f->path() + activity._file).exists()) {
|
||||
_activityModel->removeActivityFromActivityList(activity);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto path = QFileInfo(activity._file).dir().path().toUtf8();
|
||||
if (path == ".")
|
||||
path.clear();
|
||||
|
||||
if (engine.shouldDiscoverLocally(path))
|
||||
_activityModel->removeActivityFromActivityList(activity);
|
||||
}
|
||||
}
|
||||
|
||||
if (progress.status() == ProgressInfo::Done) {
|
||||
// We keep track very well of pending conflicts.
|
||||
// Inform other components about them.
|
||||
QStringList conflicts;
|
||||
foreach (Activity activity, _activityModel->errorsList()) {
|
||||
if (activity._folder == folder
|
||||
&& activity._status == SyncFileItem::Conflict) {
|
||||
conflicts.append(activity._file);
|
||||
}
|
||||
}
|
||||
|
||||
emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts);
|
||||
}
|
||||
}
|
||||
|
||||
void User::slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category)
|
||||
{
|
||||
auto folderInstance = FolderMan::instance()->folder(folderAlias);
|
||||
if (!folderInstance)
|
||||
return;
|
||||
|
||||
if (folderInstance->accountState() == _account.data()) {
|
||||
qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << message;
|
||||
|
||||
Activity activity;
|
||||
activity._type = Activity::SyncResultType;
|
||||
activity._status = SyncResult::Error;
|
||||
activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate);
|
||||
activity._subject = message;
|
||||
activity._message = folderInstance->shortGuiLocalPath();
|
||||
activity._link = folderInstance->shortGuiLocalPath();
|
||||
activity._accName = folderInstance->accountState()->account()->displayName();
|
||||
activity._folder = folderAlias;
|
||||
|
||||
|
||||
if (category == ErrorCategory::InsufficientRemoteStorage) {
|
||||
ActivityLink link;
|
||||
link._label = tr("Retry all uploads");
|
||||
link._link = folderInstance->path();
|
||||
link._verb = "";
|
||||
link._isPrimary = true;
|
||||
activity._links.append(link);
|
||||
}
|
||||
|
||||
// add 'other errors' to activity list
|
||||
_activityModel->addErrorToActivityList(activity);
|
||||
}
|
||||
}
|
||||
|
||||
void User::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
|
||||
{
|
||||
auto folderInstance = FolderMan::instance()->folder(folder);
|
||||
|
||||
if (!folderInstance)
|
||||
return;
|
||||
|
||||
// check if we are adding it to the right account and if it is useful information (protocol errors)
|
||||
if (folderInstance->accountState() == _account.data()) {
|
||||
qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in " << item->_errorString;
|
||||
|
||||
Activity activity;
|
||||
activity._type = Activity::SyncFileItemType; //client activity
|
||||
activity._status = item->_status;
|
||||
activity._dateTime = QDateTime::currentDateTime();
|
||||
activity._message = item->_originalFile;
|
||||
activity._link = folderInstance->accountState()->account()->url();
|
||||
activity._accName = folderInstance->accountState()->account()->displayName();
|
||||
activity._file = item->_file;
|
||||
activity._folder = folder;
|
||||
activity._fileAction = "";
|
||||
|
||||
if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
|
||||
activity._fileAction = "file_deleted";
|
||||
} else if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
|
||||
activity._fileAction = "file_created";
|
||||
} else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) {
|
||||
activity._fileAction = "file_renamed";
|
||||
} else {
|
||||
activity._fileAction = "file_changed";
|
||||
}
|
||||
|
||||
|
||||
if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) {
|
||||
qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully.";
|
||||
|
||||
if (activity._fileAction == "file_renamed") {
|
||||
activity._message.prepend(tr("You renamed") + " ");
|
||||
} else if (activity._fileAction == "file_deleted") {
|
||||
activity._message.prepend(tr("You deleted") + " ");
|
||||
} else if (activity._fileAction == "file_created") {
|
||||
activity._message.prepend(tr("You created") + " ");
|
||||
} else {
|
||||
activity._message.prepend(tr("You changed") + " ");
|
||||
}
|
||||
|
||||
_activityModel->addSyncFileItemToActivityList(activity);
|
||||
} else {
|
||||
qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString;
|
||||
activity._subject = item->_errorString;
|
||||
|
||||
if (item->_status == SyncFileItem::Status::FileIgnored) {
|
||||
_activityModel->addIgnoredFileToList(activity);
|
||||
} else {
|
||||
// add 'protocol error' to activity list
|
||||
_activityModel->addErrorToActivityList(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccountPtr User::account() const
|
||||
{
|
||||
return _account->account();
|
||||
}
|
||||
|
||||
void User::setCurrentUser(const bool &isCurrent)
|
||||
{
|
||||
_isCurrentUser = isCurrent;
|
||||
}
|
||||
|
||||
Folder *User::getFolder()
|
||||
{
|
||||
foreach (Folder *folder, FolderMan::instance()->map()) {
|
||||
if (folder->accountState() == _account.data()) {
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ActivityListModel *User::getActivityModel()
|
||||
{
|
||||
return _activityModel;
|
||||
}
|
||||
|
||||
void User::openLocalFolder()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
QString path = "file:///" + this->getFolder()->path();
|
||||
#else
|
||||
QString path = "file://" + this->getFolder()->path();
|
||||
#endif
|
||||
QDesktopServices::openUrl(path);
|
||||
}
|
||||
|
||||
void User::login() const
|
||||
{
|
||||
_account->account()->resetRejectedCertificates();
|
||||
_account->signIn();
|
||||
}
|
||||
|
||||
void User::logout() const
|
||||
{
|
||||
_account->signOutByUi();
|
||||
}
|
||||
|
||||
QString User::name() const
|
||||
{
|
||||
// If davDisplayName is empty (can be several reasons, simplest is missing login at startup), fall back to username
|
||||
QString name = _account->account()->davDisplayName();
|
||||
if (name == "") {
|
||||
name = _account->account()->credentials()->user();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
QString User::server(bool shortened) const
|
||||
{
|
||||
QString serverUrl = _account->account()->url().toString();
|
||||
if (shortened) {
|
||||
serverUrl.replace(QLatin1String("https://"), QLatin1String(""));
|
||||
serverUrl.replace(QLatin1String("http://"), QLatin1String(""));
|
||||
}
|
||||
return serverUrl;
|
||||
}
|
||||
|
||||
QImage User::avatar(bool whiteBg) const
|
||||
{
|
||||
QImage img = AvatarJob::makeCircularAvatar(_account->account()->avatar());
|
||||
if (img.isNull()) {
|
||||
QImage image(128, 128, QImage::Format_ARGB32);
|
||||
image.fill(Qt::GlobalColor::transparent);
|
||||
QPainter painter(&image);
|
||||
|
||||
QSvgRenderer renderer(QString(whiteBg ? ":/client/theme/black/user.svg" : ":/client/theme/white/user.svg"));
|
||||
renderer.render(&painter);
|
||||
|
||||
return image;
|
||||
} else {
|
||||
return img;
|
||||
}
|
||||
}
|
||||
|
||||
bool User::serverHasTalk() const
|
||||
{
|
||||
return _account->hasTalk();
|
||||
}
|
||||
|
||||
bool User::hasActivities() const
|
||||
{
|
||||
return _account->account()->capabilities().hasActivities();
|
||||
}
|
||||
|
||||
AccountAppList User::appList() const
|
||||
{
|
||||
return _account->appList();
|
||||
}
|
||||
|
||||
bool User::isCurrentUser() const
|
||||
{
|
||||
return _isCurrentUser;
|
||||
}
|
||||
|
||||
bool User::isConnected() const
|
||||
{
|
||||
return (_account->connectionStatus() == AccountState::ConnectionStatus::Connected);
|
||||
}
|
||||
|
||||
void User::removeAccount() const
|
||||
{
|
||||
AccountManager::instance()->deleteAccount(_account.data());
|
||||
AccountManager::instance()->save();
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------------------*/
|
||||
|
||||
UserModel *UserModel::_instance = nullptr;
|
||||
|
||||
UserModel *UserModel::instance()
|
||||
{
|
||||
if (_instance == nullptr) {
|
||||
_instance = new UserModel();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
UserModel::UserModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, _currentUserId()
|
||||
{
|
||||
// TODO: Remember selected user from last quit via settings file
|
||||
if (AccountManager::instance()->accounts().size() > 0) {
|
||||
buildUserList();
|
||||
}
|
||||
|
||||
connect(AccountManager::instance(), &AccountManager::accountAdded,
|
||||
this, &UserModel::buildUserList);
|
||||
}
|
||||
|
||||
void UserModel::buildUserList()
|
||||
{
|
||||
for (int i = 0; i < AccountManager::instance()->accounts().size(); i++) {
|
||||
auto user = AccountManager::instance()->accounts().at(i);
|
||||
addUser(user);
|
||||
}
|
||||
if (_init) {
|
||||
_users.first()->setCurrentUser(true);
|
||||
_init = false;
|
||||
}
|
||||
}
|
||||
|
||||
Q_INVOKABLE int UserModel::numUsers()
|
||||
{
|
||||
return _users.size();
|
||||
}
|
||||
|
||||
Q_INVOKABLE int UserModel::currentUserId()
|
||||
{
|
||||
return _currentUserId;
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool UserModel::isUserConnected(const int &id)
|
||||
{
|
||||
return _users[id]->isConnected();
|
||||
}
|
||||
|
||||
Q_INVOKABLE QImage UserModel::currentUserAvatar()
|
||||
{
|
||||
if (_users.count() >= 1) {
|
||||
return _users[_currentUserId]->avatar();
|
||||
} else {
|
||||
QImage image(128, 128, QImage::Format_ARGB32);
|
||||
image.fill(Qt::GlobalColor::transparent);
|
||||
QPainter painter(&image);
|
||||
QSvgRenderer renderer(QString(":/client/theme/white/user.svg"));
|
||||
renderer.render(&painter);
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
QImage UserModel::avatarById(const int &id)
|
||||
{
|
||||
return _users[id]->avatar(true);
|
||||
}
|
||||
|
||||
Q_INVOKABLE QString UserModel::currentUserName()
|
||||
{
|
||||
if (_users.count() >= 1) {
|
||||
return _users[_currentUserId]->name();
|
||||
} else {
|
||||
return QString("No users");
|
||||
}
|
||||
}
|
||||
|
||||
Q_INVOKABLE QString UserModel::currentUserServer()
|
||||
{
|
||||
if (_users.count() >= 1) {
|
||||
return _users[_currentUserId]->server();
|
||||
} else {
|
||||
return QString("");
|
||||
}
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool UserModel::currentServerHasTalk()
|
||||
{
|
||||
if (_users.count() >= 1) {
|
||||
return _users[_currentUserId]->serverHasTalk();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void UserModel::addUser(AccountStatePtr &user, const bool &isCurrent)
|
||||
{
|
||||
bool containsUser = false;
|
||||
for (int i = 0; i < _users.size(); i++) {
|
||||
if (_users[i]->account() == user->account()) {
|
||||
containsUser = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsUser) {
|
||||
beginInsertRows(QModelIndex(), rowCount(), rowCount());
|
||||
_users << new User(user, isCurrent);
|
||||
if (isCurrent) {
|
||||
_currentUserId = _users.indexOf(_users.last());
|
||||
}
|
||||
endInsertRows();
|
||||
ConfigFile cfg;
|
||||
_users.last()->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
|
||||
}
|
||||
}
|
||||
|
||||
int UserModel::currentUserIndex()
|
||||
{
|
||||
return _currentUserId;
|
||||
}
|
||||
|
||||
Q_INVOKABLE void UserModel::openCurrentAccountLocalFolder()
|
||||
{
|
||||
_users[_currentUserId]->openLocalFolder();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void UserModel::openCurrentAccountTalk()
|
||||
{
|
||||
QString url = _users[_currentUserId]->server(false) + "/apps/spreed";
|
||||
if (!(url.contains("http://") || url.contains("https://"))) {
|
||||
url = "https://" + _users[_currentUserId]->server(false) + "/apps/spreed";
|
||||
}
|
||||
QDesktopServices::openUrl(QUrl(url));
|
||||
}
|
||||
|
||||
Q_INVOKABLE void UserModel::openCurrentAccountServer()
|
||||
{
|
||||
// Don't open this URL when the QML appMenu pops up on click (see Window.qml)
|
||||
if(appList().count() > 0)
|
||||
return;
|
||||
|
||||
QString url = _users[_currentUserId]->server(false);
|
||||
if (!(url.contains("http://") || url.contains("https://"))) {
|
||||
url = "https://" + _users[_currentUserId]->server(false);
|
||||
}
|
||||
QDesktopServices::openUrl(QUrl(url));
|
||||
}
|
||||
|
||||
Q_INVOKABLE void UserModel::switchCurrentUser(const int &id)
|
||||
{
|
||||
_users[_currentUserId]->setCurrentUser(false);
|
||||
_users[id]->setCurrentUser(true);
|
||||
_currentUserId = id;
|
||||
emit refreshCurrentUserGui();
|
||||
emit newUserSelected();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void UserModel::login(const int &id)
|
||||
{
|
||||
_users[id]->login();
|
||||
emit refreshCurrentUserGui();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void UserModel::logout(const int &id)
|
||||
{
|
||||
_users[id]->logout();
|
||||
emit refreshCurrentUserGui();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void UserModel::removeAccount(const int &id)
|
||||
{
|
||||
QMessageBox messageBox(QMessageBox::Question,
|
||||
tr("Confirm Account Removal"),
|
||||
tr("<p>Do you really want to remove the connection to the account <i>%1</i>?</p>"
|
||||
"<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
|
||||
.arg(_users[id]->name()),
|
||||
QMessageBox::NoButton);
|
||||
QPushButton *yesButton =
|
||||
messageBox.addButton(tr("Remove connection"), QMessageBox::YesRole);
|
||||
messageBox.addButton(tr("Cancel"), QMessageBox::NoRole);
|
||||
|
||||
messageBox.exec();
|
||||
if (messageBox.clickedButton() != yesButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_users[id]->isCurrentUser() && _users.count() > 1) {
|
||||
id == 0 ? switchCurrentUser(1) : switchCurrentUser(0);
|
||||
}
|
||||
|
||||
_users[id]->logout();
|
||||
_users[id]->removeAccount();
|
||||
|
||||
beginRemoveRows(QModelIndex(), id, id);
|
||||
_users.removeAt(id);
|
||||
endRemoveRows();
|
||||
|
||||
emit refreshCurrentUserGui();
|
||||
}
|
||||
|
||||
int UserModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return _users.count();
|
||||
}
|
||||
|
||||
QVariant UserModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (index.row() < 0 || index.row() >= _users.count()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (role == NameRole) {
|
||||
return _users[index.row()]->name();
|
||||
} else if (role == ServerRole) {
|
||||
return _users[index.row()]->server();
|
||||
} else if (role == AvatarRole) {
|
||||
return _users[index.row()]->avatar();
|
||||
} else if (role == IsCurrentUserRole) {
|
||||
return _users[index.row()]->isCurrentUser();
|
||||
} else if (role == IsConnectedRole) {
|
||||
return _users[index.row()]->isConnected();
|
||||
} else if (role == IdRole) {
|
||||
return index.row();
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> UserModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[NameRole] = "name";
|
||||
roles[ServerRole] = "server";
|
||||
roles[AvatarRole] = "avatar";
|
||||
roles[IsCurrentUserRole] = "isCurrentUser";
|
||||
roles[IsConnectedRole] = "isConnected";
|
||||
roles[IdRole] = "id";
|
||||
return roles;
|
||||
}
|
||||
|
||||
ActivityListModel *UserModel::currentActivityModel()
|
||||
{
|
||||
return _users[currentUserIndex()]->getActivityModel();
|
||||
}
|
||||
|
||||
bool UserModel::currentUserHasActivities()
|
||||
{
|
||||
return _users[currentUserIndex()]->hasActivities();
|
||||
}
|
||||
|
||||
void UserModel::fetchCurrentActivityModel()
|
||||
{
|
||||
_users[currentUserId()]->slotRefresh();
|
||||
}
|
||||
|
||||
AccountAppList UserModel::appList() const
|
||||
{
|
||||
if (_users.count() >= 1) {
|
||||
return _users[_currentUserId]->appList();
|
||||
} else {
|
||||
return AccountAppList();
|
||||
}
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------------------*/
|
||||
|
||||
ImageProvider::ImageProvider()
|
||||
: QQuickImageProvider(QQuickImageProvider::Image)
|
||||
{
|
||||
}
|
||||
|
||||
QImage ImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
|
||||
{
|
||||
Q_UNUSED(size)
|
||||
Q_UNUSED(requestedSize)
|
||||
|
||||
if (id == "currentUser") {
|
||||
return UserModel::instance()->currentUserAvatar();
|
||||
} else {
|
||||
int uid = id.toInt();
|
||||
return UserModel::instance()->avatarById(uid);
|
||||
}
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------------------*/
|
||||
|
||||
UserAppsModel *UserAppsModel::_instance = nullptr;
|
||||
|
||||
UserAppsModel *UserAppsModel::instance()
|
||||
{
|
||||
if (_instance == nullptr) {
|
||||
_instance = new UserAppsModel();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
UserAppsModel::UserAppsModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void UserAppsModel::buildAppList()
|
||||
{
|
||||
if (rowCount() > 0) {
|
||||
beginRemoveRows(QModelIndex(), 0, rowCount() - 1);
|
||||
_apps.clear();
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
if(UserModel::instance()->appList().count() > 0) {
|
||||
foreach(AccountApp *app, UserModel::instance()->appList()) {
|
||||
// Filter out Talk because we have a dedicated button for it
|
||||
if(app->id() == QLatin1String("spreed"))
|
||||
continue;
|
||||
|
||||
beginInsertRows(QModelIndex(), rowCount(), rowCount());
|
||||
_apps << app;
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UserAppsModel::openAppUrl(const QUrl &url)
|
||||
{
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
int UserAppsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return _apps.count();
|
||||
}
|
||||
|
||||
QVariant UserAppsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (index.row() < 0 || index.row() >= _apps.count()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (role == NameRole) {
|
||||
return _apps[index.row()]->name();
|
||||
} else if (role == UrlRole) {
|
||||
return _apps[index.row()]->url();
|
||||
} else if (role == IconUrlRole) {
|
||||
return _apps[index.row()]->iconUrl().toString();
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> UserAppsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[NameRole] = "appName";
|
||||
roles[UrlRole] = "appUrl";
|
||||
roles[IconUrlRole] = "appIconUrl";
|
||||
return roles;
|
||||
}
|
||||
|
||||
}
|
183
src/gui/tray/UserModel.h
Normal file
|
@ -0,0 +1,183 @@
|
|||
#ifndef USERMODEL_H
|
||||
#define USERMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QImage>
|
||||
#include <QDateTime>
|
||||
#include <QStringList>
|
||||
#include <QQuickImageProvider>
|
||||
|
||||
#include "ActivityListModel.h"
|
||||
#include "accountmanager.h"
|
||||
#include "folderman.h"
|
||||
#include <chrono>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class User : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
User(AccountStatePtr &account, const bool &isCurrent = false, QObject* parent = 0);
|
||||
|
||||
AccountPtr account() const;
|
||||
|
||||
bool isConnected() const;
|
||||
bool isCurrentUser() const;
|
||||
void setCurrentUser(const bool &isCurrent);
|
||||
Folder *getFolder();
|
||||
ActivityListModel *getActivityModel();
|
||||
void openLocalFolder();
|
||||
QString name() const;
|
||||
QString server(bool shortened = true) const;
|
||||
bool serverHasTalk() const;
|
||||
bool hasActivities() const;
|
||||
AccountAppList appList() const;
|
||||
QImage avatar(bool whiteBg = false) const;
|
||||
QString id() const;
|
||||
void login() const;
|
||||
void logout() const;
|
||||
void removeAccount() const;
|
||||
|
||||
signals:
|
||||
void guiLog(const QString &, const QString &);
|
||||
|
||||
public slots:
|
||||
void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
|
||||
void slotProgressInfo(const QString &folder, const ProgressInfo &progress);
|
||||
void slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category);
|
||||
void slotNotificationRequestFinished(int statusCode);
|
||||
void slotNotifyNetworkError(QNetworkReply *reply);
|
||||
void slotEndNotificationRequest(int replyCode);
|
||||
void slotNotifyServerFinished(const QString &reply, int replyCode);
|
||||
void slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
|
||||
void slotBuildNotificationDisplay(const ActivityList &list);
|
||||
void slotRefreshNotifications();
|
||||
void slotRefreshActivities();
|
||||
void slotRefresh();
|
||||
void slotRefreshImmediately();
|
||||
void setNotificationRefreshInterval(std::chrono::milliseconds interval);
|
||||
void slotRebuildNavigationAppList();
|
||||
|
||||
private:
|
||||
AccountStatePtr _account;
|
||||
bool _isCurrentUser;
|
||||
ActivityListModel *_activityModel;
|
||||
ActivityList _blacklistedNotifications;
|
||||
|
||||
QTimer _notificationCheckTimer;
|
||||
QHash<AccountState *, QElapsedTimer> _timeSinceLastCheck;
|
||||
|
||||
QElapsedTimer _guiLogTimer;
|
||||
QSet<int> _guiLoggedNotifications;
|
||||
|
||||
// number of currently running notification requests. If non zero,
|
||||
// no query for notifications is started.
|
||||
int _notificationRequestsRunning;
|
||||
};
|
||||
|
||||
class UserModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static UserModel *instance();
|
||||
virtual ~UserModel() {};
|
||||
|
||||
void addUser(AccountStatePtr &user, const bool &isCurrent = false);
|
||||
int currentUserIndex();
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||
|
||||
QImage avatarById(const int &id);
|
||||
|
||||
Q_INVOKABLE void fetchCurrentActivityModel();
|
||||
Q_INVOKABLE void openCurrentAccountLocalFolder();
|
||||
Q_INVOKABLE void openCurrentAccountTalk();
|
||||
Q_INVOKABLE void openCurrentAccountServer();
|
||||
Q_INVOKABLE QImage currentUserAvatar();
|
||||
Q_INVOKABLE int numUsers();
|
||||
Q_INVOKABLE QString currentUserName();
|
||||
Q_INVOKABLE QString currentUserServer();
|
||||
Q_INVOKABLE bool currentUserHasActivities();
|
||||
Q_INVOKABLE bool currentServerHasTalk();
|
||||
Q_INVOKABLE int currentUserId();
|
||||
Q_INVOKABLE bool isUserConnected(const int &id);
|
||||
Q_INVOKABLE void switchCurrentUser(const int &id);
|
||||
Q_INVOKABLE void login(const int &id);
|
||||
Q_INVOKABLE void logout(const int &id);
|
||||
Q_INVOKABLE void removeAccount(const int &id);
|
||||
|
||||
ActivityListModel *currentActivityModel();
|
||||
|
||||
enum UserRoles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
ServerRole,
|
||||
AvatarRole,
|
||||
IsCurrentUserRole,
|
||||
IsConnectedRole,
|
||||
IdRole
|
||||
};
|
||||
|
||||
AccountAppList appList() const;
|
||||
|
||||
signals:
|
||||
Q_INVOKABLE void addAccount();
|
||||
Q_INVOKABLE void refreshCurrentUserGui();
|
||||
Q_INVOKABLE void newUserSelected();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
static UserModel *_instance;
|
||||
UserModel(QObject *parent = 0);
|
||||
QList<User*> _users;
|
||||
int _currentUserId;
|
||||
bool _init = true;
|
||||
|
||||
void buildUserList();
|
||||
};
|
||||
|
||||
class ImageProvider : public QQuickImageProvider
|
||||
{
|
||||
public:
|
||||
ImageProvider();
|
||||
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
|
||||
};
|
||||
|
||||
class UserAppsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static UserAppsModel *instance();
|
||||
virtual ~UserAppsModel() {};
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||
|
||||
enum UserAppsRoles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
UrlRole,
|
||||
IconUrlRole
|
||||
};
|
||||
|
||||
void buildAppList();
|
||||
|
||||
public slots:
|
||||
void openAppUrl(const QUrl &url);
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
static UserAppsModel *_instance;
|
||||
UserAppsModel(QObject *parent = 0);
|
||||
|
||||
AccountAppList _apps;
|
||||
};
|
||||
|
||||
}
|
||||
#endif // USERMODEL_H
|
612
src/gui/tray/Window.qml
Normal file
|
@ -0,0 +1,612 @@
|
|||
import QtQml 2.1
|
||||
import QtQml.Models 2.1
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
Window {
|
||||
|
||||
id: trayWindow
|
||||
visible: true
|
||||
width: 400
|
||||
height: 510
|
||||
color: "transparent"
|
||||
flags: Qt.FramelessWindowHint
|
||||
|
||||
onActiveChanged: {
|
||||
if(!active) {
|
||||
trayWindow.hide();
|
||||
systrayBackend.setClosed();
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
currentAccountAvatar.source = ""
|
||||
currentAccountAvatar.source = "image://avatars/currentUser"
|
||||
currentAccountUser.text = userModelBackend.currentUserName();
|
||||
currentAccountServer.text = userModelBackend.currentUserServer();
|
||||
trayWindowTalkButton.visible = userModelBackend.currentServerHasTalk() ? true : false;
|
||||
currentAccountStateIndicator.source = ""
|
||||
currentAccountStateIndicator.source = userModelBackend.isUserConnected(userModelBackend.currentUserId()) ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg"
|
||||
|
||||
userLineInstantiator.active = false;
|
||||
userLineInstantiator.active = true;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: userModelBackend
|
||||
onRefreshCurrentUserGui: {
|
||||
currentAccountAvatar.source = ""
|
||||
currentAccountAvatar.source = "image://avatars/currentUser"
|
||||
currentAccountUser.text = userModelBackend.currentUserName();
|
||||
currentAccountServer.text = userModelBackend.currentUserServer();
|
||||
currentAccountStateIndicator.source = ""
|
||||
currentAccountStateIndicator.source = userModelBackend.isUserConnected(userModelBackend.currentUserId()) ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg"
|
||||
}
|
||||
onNewUserSelected: {
|
||||
accountMenu.close();
|
||||
trayWindowTalkButton.visible = userModelBackend.currentServerHasTalk() ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: systrayBackend
|
||||
onShowWindow: {
|
||||
accountMenu.close();
|
||||
trayWindow.show();
|
||||
trayWindow.raise();
|
||||
trayWindow.requestActivate();
|
||||
trayWindow.setX( systrayBackend.calcTrayWindowX());
|
||||
trayWindow.setY( systrayBackend.calcTrayWindowY());
|
||||
systrayBackend.setOpened();
|
||||
userModelBackend.fetchCurrentActivityModel();
|
||||
}
|
||||
onHideWindow: {
|
||||
trayWindow.hide();
|
||||
systrayBackend.setClosed();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: trayWindowBackground
|
||||
anchors.fill: parent
|
||||
radius: 10
|
||||
border.color: "#0082c9"
|
||||
|
||||
Rectangle {
|
||||
id: trayWindowHeaderBackground
|
||||
anchors.left: trayWindowBackground.left
|
||||
anchors.top: trayWindowBackground.top
|
||||
height: 60
|
||||
width: parent.width
|
||||
radius: 9
|
||||
color: "#0082c9"
|
||||
|
||||
Rectangle {
|
||||
anchors.left: trayWindowHeaderBackground.left
|
||||
anchors.bottom: trayWindowHeaderBackground.bottom
|
||||
height: 30
|
||||
width: parent.width
|
||||
color: "#0082c9"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: trayWindowHeaderLayout
|
||||
spacing: 0
|
||||
anchors.fill: parent
|
||||
|
||||
Button {
|
||||
id: currentAccountButton
|
||||
Layout.preferredWidth: 220
|
||||
Layout.preferredHeight: (trayWindowHeaderBackground.height)
|
||||
display: AbstractButton.IconOnly
|
||||
flat: true
|
||||
|
||||
MouseArea {
|
||||
id: accountBtnMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onContainsMouseChanged: {
|
||||
currentAccountStateIndicatorBackground.color = (containsMouse ? "#009dd9" : "#0082c9")
|
||||
}
|
||||
onClicked:
|
||||
{
|
||||
syncPauseButton.text = systrayBackend.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all")
|
||||
accountMenu.open()
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: accountMenu
|
||||
x: (currentAccountButton.x + 2)
|
||||
y: (currentAccountButton.y + currentAccountButton.height + 2)
|
||||
width: (currentAccountButton.width - 2)
|
||||
closePolicy: "CloseOnPressOutside"
|
||||
|
||||
background: Rectangle {
|
||||
border.color: "#0082c9"
|
||||
radius: 2
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
userLineInstantiator.active = false;
|
||||
userLineInstantiator.active = true;
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
id: userLineInstantiator
|
||||
model: userModelBackend
|
||||
delegate: UserLine {}
|
||||
onObjectAdded: accountMenu.insertItem(index, object)
|
||||
onObjectRemoved: accountMenu.removeItem(object)
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: addAccountButton
|
||||
height: 50
|
||||
|
||||
RowLayout {
|
||||
width: addAccountButton.width
|
||||
height: addAccountButton.height
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
Layout.leftMargin: 14
|
||||
verticalAlignment: Qt.AlignCenter
|
||||
source: "qrc:///client/theme/black/add.svg"
|
||||
sourceSize.width: openLocalFolderButton.icon.width
|
||||
sourceSize.height: openLocalFolderButton.icon.height
|
||||
}
|
||||
Label {
|
||||
Layout.leftMargin: 14
|
||||
text: qsTr("Add account")
|
||||
color: "black"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
onClicked: userModelBackend.addAccount()
|
||||
}
|
||||
|
||||
MenuSeparator { id: accountMenuSeparator }
|
||||
|
||||
MenuItem {
|
||||
id: syncPauseButton
|
||||
font.pixelSize: 12
|
||||
onClicked: systrayBackend.pauseResumeSync()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("Open settings")
|
||||
font.pixelSize: 12
|
||||
onClicked: systrayBackend.openSettings()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("Help")
|
||||
font.pixelSize: 12
|
||||
onClicked: systrayBackend.openHelp()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("Quit Nextcloud")
|
||||
font.pixelSize: 12
|
||||
onClicked: systrayBackend.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background:
|
||||
Item {
|
||||
id: leftHoverContainer
|
||||
height: currentAccountButton.height
|
||||
width: currentAccountButton.width
|
||||
Rectangle {
|
||||
width: currentAccountButton.width / 2
|
||||
height: currentAccountButton.height / 2
|
||||
color: "transparent"
|
||||
clip: true
|
||||
Rectangle {
|
||||
width: currentAccountButton.width
|
||||
height: currentAccountButton.height
|
||||
radius: 10
|
||||
color: "white"
|
||||
opacity: 0.2
|
||||
visible: accountBtnMouseArea.containsMouse
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: currentAccountButton.width / 2
|
||||
height: currentAccountButton.height / 2
|
||||
anchors.bottom: leftHoverContainer.bottom
|
||||
color: "white"
|
||||
opacity: 0.2
|
||||
visible: accountBtnMouseArea.containsMouse
|
||||
}
|
||||
Rectangle {
|
||||
width: currentAccountButton.width / 2
|
||||
height: currentAccountButton.height / 2
|
||||
anchors.right: leftHoverContainer.right
|
||||
color: "white"
|
||||
opacity: 0.2
|
||||
visible: accountBtnMouseArea.containsMouse
|
||||
}
|
||||
Rectangle {
|
||||
width: currentAccountButton.width / 2
|
||||
height: currentAccountButton.height / 2
|
||||
anchors.right: leftHoverContainer.right
|
||||
anchors.bottom: leftHoverContainer.bottom
|
||||
color: "white"
|
||||
opacity: 0.2
|
||||
visible: accountBtnMouseArea.containsMouse
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: accountControlRowLayout
|
||||
height: currentAccountButton.height
|
||||
width: currentAccountButton.width
|
||||
spacing: 0
|
||||
Image {
|
||||
id: currentAccountAvatar
|
||||
Layout.leftMargin: 8
|
||||
verticalAlignment: Qt.AlignCenter
|
||||
cache: false
|
||||
source: "image://avatars/currentUser"
|
||||
Layout.preferredHeight: (trayWindowHeaderBackground.height -16)
|
||||
Layout.preferredWidth: (trayWindowHeaderBackground.height -16)
|
||||
Rectangle {
|
||||
id: currentAccountStateIndicatorBackground
|
||||
width: currentAccountStateIndicator.sourceSize.width + 2
|
||||
height: width
|
||||
anchors.bottom: currentAccountAvatar.bottom
|
||||
anchors.right: currentAccountAvatar.right
|
||||
color: "#0082c9"
|
||||
radius: width*0.5
|
||||
}
|
||||
Image {
|
||||
id: currentAccountStateIndicator
|
||||
source: userModelBackend.isUserConnected(userModelBackend.currentUserId()) ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg"
|
||||
cache: false
|
||||
x: currentAccountStateIndicatorBackground.x + 1
|
||||
y: currentAccountStateIndicatorBackground.y + 1
|
||||
sourceSize.width: 16
|
||||
sourceSize.height: 16
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: accountLabels
|
||||
spacing: 4
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.leftMargin: 6
|
||||
Label {
|
||||
id: currentAccountUser
|
||||
width: 128
|
||||
text: userModelBackend.currentUserName()
|
||||
elide: Text.ElideRight
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
Label {
|
||||
id: currentAccountServer
|
||||
width: 128
|
||||
text: userModelBackend.currentUserServer()
|
||||
elide: Text.ElideRight
|
||||
color: "white"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
verticalAlignment: Qt.AlignCenter
|
||||
Layout.margins: 8
|
||||
source: "qrc:///client/theme/white/caret-down.svg"
|
||||
sourceSize.width: 20
|
||||
sourceSize.height: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: trayWindowHeaderSpacer
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
id: openLocalFolderButton
|
||||
Layout.alignment: Qt.AlignRight
|
||||
display: AbstractButton.IconOnly
|
||||
Layout.preferredWidth: (trayWindowHeaderBackground.height)
|
||||
Layout.preferredHeight: (trayWindowHeaderBackground.height)
|
||||
flat: true
|
||||
|
||||
icon.source: "qrc:///client/theme/white/folder.svg"
|
||||
icon.color: "transparent"
|
||||
|
||||
MouseArea {
|
||||
id: folderBtnMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked:
|
||||
{
|
||||
userModelBackend.openCurrentAccountLocalFolder();
|
||||
}
|
||||
}
|
||||
|
||||
background:
|
||||
Rectangle {
|
||||
color: folderBtnMouseArea.containsMouse ? "white" : "transparent"
|
||||
opacity: 0.2
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: trayWindowTalkButton
|
||||
Layout.alignment: Qt.AlignRight
|
||||
display: AbstractButton.IconOnly
|
||||
Layout.preferredWidth: (trayWindowHeaderBackground.height)
|
||||
Layout.preferredHeight: (trayWindowHeaderBackground.height)
|
||||
flat: true
|
||||
visible: userModelBackend.currentServerHasTalk() ? true : false
|
||||
|
||||
icon.source: "qrc:///client/theme/white/talk-app.svg"
|
||||
icon.color: "transparent"
|
||||
|
||||
MouseArea {
|
||||
id: talkBtnMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked:
|
||||
{
|
||||
userModelBackend.openCurrentAccountTalk();
|
||||
}
|
||||
}
|
||||
|
||||
background:
|
||||
Rectangle {
|
||||
color: talkBtnMouseArea.containsMouse ? "white" : "transparent"
|
||||
opacity: 0.2
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: trayWindowAppsButton
|
||||
Layout.alignment: Qt.AlignRight
|
||||
display: AbstractButton.IconOnly
|
||||
Layout.preferredWidth: (trayWindowHeaderBackground.height)
|
||||
Layout.preferredHeight: (trayWindowHeaderBackground.height)
|
||||
flat: true
|
||||
|
||||
icon.source: "qrc:///client/theme/white/more-apps.svg"
|
||||
icon.color: "transparent"
|
||||
|
||||
MouseArea {
|
||||
id: appsBtnMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked:
|
||||
{
|
||||
/*
|
||||
// The count() property was introduced in QtQuick.Controls 2.3 (Qt 5.10)
|
||||
// so we handle this with userModelBackend.openCurrentAccountServer()
|
||||
//
|
||||
// See UserModel::openCurrentAccountServer() to disable this workaround
|
||||
// in the future for Qt >= 5.10
|
||||
|
||||
if(appsMenu.count() > 0) {
|
||||
appsMenu.popup();
|
||||
} else {
|
||||
userModelBackend.openCurrentAccountServer();
|
||||
}
|
||||
*/
|
||||
|
||||
appsMenu.open();
|
||||
userModelBackend.openCurrentAccountServer();
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: appsMenu
|
||||
y: (trayWindowAppsButton.y + trayWindowAppsButton.height + 2)
|
||||
width: (trayWindowAppsButton.width * 3)
|
||||
closePolicy: "CloseOnPressOutside"
|
||||
|
||||
background: Rectangle {
|
||||
border.color: "#0082c9"
|
||||
radius: 2
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
id: appsMenuInstantiator
|
||||
model: appsMenuModelBackend
|
||||
onObjectAdded: appsMenu.insertItem(index, object)
|
||||
onObjectRemoved: appsMenu.removeItem(object)
|
||||
delegate: MenuItem {
|
||||
text: appName
|
||||
font.pixelSize: 12
|
||||
icon.source: appIconUrl
|
||||
onTriggered: appsMenuModelBackend.openAppUrl(appUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background:
|
||||
Item {
|
||||
id: rightHoverContainer
|
||||
height: trayWindowAppsButton.height
|
||||
width: trayWindowAppsButton.width
|
||||
Rectangle {
|
||||
width: trayWindowAppsButton.width / 2
|
||||
height: trayWindowAppsButton.height / 2
|
||||
color: "white"
|
||||
opacity: 0.2
|
||||
visible: appsBtnMouseArea.containsMouse
|
||||
}
|
||||
Rectangle {
|
||||
width: trayWindowAppsButton.width / 2
|
||||
height: trayWindowAppsButton.height / 2
|
||||
anchors.bottom: rightHoverContainer.bottom
|
||||
color: "white"
|
||||
opacity: 0.2
|
||||
visible: appsBtnMouseArea.containsMouse
|
||||
}
|
||||
Rectangle {
|
||||
width: trayWindowAppsButton.width / 2
|
||||
height: trayWindowAppsButton.height / 2
|
||||
anchors.bottom: rightHoverContainer.bottom
|
||||
anchors.right: rightHoverContainer.right
|
||||
color: "white"
|
||||
opacity: 0.2
|
||||
visible: appsBtnMouseArea.containsMouse
|
||||
}
|
||||
Rectangle {
|
||||
id: rightHoverContainerClipper
|
||||
anchors.right: rightHoverContainer.right
|
||||
width: trayWindowAppsButton.width / 2
|
||||
height: trayWindowAppsButton.height / 2
|
||||
color: "transparent"
|
||||
clip: true
|
||||
Rectangle {
|
||||
width: trayWindowAppsButton.width
|
||||
height: trayWindowAppsButton.height
|
||||
anchors.right: rightHoverContainerClipper.right
|
||||
radius: 10
|
||||
color: "white"
|
||||
opacity: 0.2
|
||||
visible: appsBtnMouseArea.containsMouse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // Rectangle trayWindowHeaderBackground
|
||||
|
||||
ListView {
|
||||
id: activityListView
|
||||
anchors.top: trayWindowHeaderBackground.bottom
|
||||
width: trayWindowBackground.width
|
||||
height: trayWindowBackground.height - trayWindowHeaderBackground.height
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: listViewScrollbar
|
||||
}
|
||||
|
||||
model: activityModel
|
||||
|
||||
delegate: RowLayout {
|
||||
id: activityItem
|
||||
width: activityListView.width
|
||||
height: trayWindowHeaderLayout.height
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
id: activityIcon
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 8
|
||||
Layout.preferredWidth: activityButton1.icon.width
|
||||
Layout.preferredHeight: activityButton1.icon.height
|
||||
verticalAlignment: Qt.AlignCenter
|
||||
cache: true
|
||||
source: icon
|
||||
sourceSize.height: 64
|
||||
sourceSize.width: 64
|
||||
}
|
||||
Column {
|
||||
id: activityTextColumn
|
||||
spacing: 4
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Text {
|
||||
id: activityTextTitle
|
||||
text: (type === "Activity" || type === "Notification") ? subject : message
|
||||
width: 240 + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 12
|
||||
color: activityTextTitleColor
|
||||
}
|
||||
|
||||
Text {
|
||||
id: activityTextInfo
|
||||
text: (type === "Activity" || type === "File" || type === "Sync") ? displaypath : message
|
||||
height: (text === "") ? 0 : activityTextTitle.height
|
||||
width: 240 + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 10
|
||||
}
|
||||
|
||||
Text {
|
||||
id: activityTextDateTime
|
||||
text: dateTime
|
||||
height: (text === "") ? 0 : activityTextTitle.height
|
||||
width: 240 + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 10
|
||||
color: "#808080"
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: activityItemFiller
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
id: activityButton1
|
||||
Layout.preferredWidth: (path === "") ? 0 : activityItem.height
|
||||
Layout.preferredHeight: activityItem.height
|
||||
Layout.alignment: Qt.AlignRight
|
||||
flat: true
|
||||
hoverEnabled: false
|
||||
visible: (path === "") ? false : true
|
||||
display: AbstractButton.IconOnly
|
||||
icon.source: "qrc:///client/resources/files.svg"
|
||||
icon.color: "transparent"
|
||||
|
||||
onClicked: {
|
||||
Qt.openUrlExternally(path)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: activityButton2
|
||||
Layout.preferredWidth: (link === "") ? 0 : activityItem.height
|
||||
Layout.preferredHeight: activityItem.height
|
||||
Layout.alignment: Qt.AlignRight
|
||||
flat: true
|
||||
hoverEnabled: false
|
||||
visible: (link === "") ? false : true
|
||||
display: AbstractButton.IconOnly
|
||||
icon.source: "qrc:///client/resources/public.svg"
|
||||
icon.color: "transparent"
|
||||
|
||||
onClicked: {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*add: Transition {
|
||||
NumberAnimation { properties: "y"; from: -60; duration: 100; easing.type: Easing.Linear }
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
NumberAnimation { property: "opacity"; from: 1.0; to: 0; duration: 100 }
|
||||
}
|
||||
|
||||
removeDisplaced: Transition {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 100}
|
||||
NumberAnimation { properties: "y"; duration: 100; easing.type: Easing.Linear }
|
||||
}
|
||||
}
|
||||
|
||||
displaced: Transition {
|
||||
NumberAnimation { properties: "y"; duration: 100; easing.type: Easing.Linear }
|
||||
}*/
|
||||
}
|
||||
|
||||
} // Rectangle trayWindowBackground
|
||||
}
|
|
@ -103,7 +103,8 @@ bool Capabilities::isValid() const
|
|||
return !_capabilities.isEmpty();
|
||||
}
|
||||
|
||||
bool Capabilities::hasActivities() const {
|
||||
bool Capabilities::hasActivities() const
|
||||
{
|
||||
return _capabilities.contains("activity");
|
||||
}
|
||||
|
||||
|
|
|
@ -376,14 +376,14 @@ void OwncloudPropagator::start(const SyncFileItemVector &items,
|
|||
Q_ASSERT(std::is_sorted(items.begin(), items.end()));
|
||||
} else if (hasChange) {
|
||||
Q_ASSERT(std::is_sorted(items.begin(), items.end(),
|
||||
[](const SyncFileItemVector::const_reference &a, const SyncFileItemVector::const_reference &b) -> bool {
|
||||
[](SyncFileItemVector::const_reference &a, SyncFileItemVector::const_reference &b) -> bool {
|
||||
return ((a->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) && (b->_instruction != CSYNC_INSTRUCTION_TYPE_CHANGE));
|
||||
}));
|
||||
Q_ASSERT(std::is_sorted(items.begin(), items.begin() + lastChangeInstruction));
|
||||
|
||||
if (hasDelete) {
|
||||
Q_ASSERT(std::is_sorted(items.begin() + (lastChangeInstruction + 1), items.end(),
|
||||
[](const SyncFileItemVector::const_reference &a, const SyncFileItemVector::const_reference &b) -> bool {
|
||||
[](SyncFileItemVector::const_reference &a, SyncFileItemVector::const_reference &b) -> bool {
|
||||
return ((a->_instruction == CSYNC_INSTRUCTION_REMOVE) && (b->_instruction != CSYNC_INSTRUCTION_REMOVE));
|
||||
}));
|
||||
Q_ASSERT(std::is_sorted(items.begin() + (lastChangeInstruction + 1), items.begin() + lastDeleteInstruction));
|
||||
|
|
|
@ -1078,7 +1078,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
|
|||
// Get CHANGE instructions to the top first
|
||||
if (syncItems.count() > 0) {
|
||||
std::sort(syncItems.begin(), syncItems.end(),
|
||||
[](const SyncFileItemVector::const_reference &a, const SyncFileItemVector::const_reference &b) -> bool {
|
||||
[](SyncFileItemVector::const_reference &a, SyncFileItemVector::const_reference &b) -> bool {
|
||||
return ((a->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) && (b->_instruction != CSYNC_INSTRUCTION_TYPE_CHANGE));
|
||||
});
|
||||
if (syncItems.at(0)->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) {
|
||||
|
@ -1089,7 +1089,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
|
|||
std::sort(syncItems.begin(), syncItems.begin() + lastChangeInstruction);
|
||||
if (syncItems.count() > lastChangeInstruction) {
|
||||
std::sort(syncItems.begin() + (lastChangeInstruction + 1), syncItems.end(),
|
||||
[](const SyncFileItemVector::const_reference &a, const SyncFileItemVector::const_reference &b) -> bool {
|
||||
[](SyncFileItemVector::const_reference &a, SyncFileItemVector::const_reference &b) -> bool {
|
||||
return ((a->_instruction == CSYNC_INSTRUCTION_REMOVE) && (b->_instruction != CSYNC_INSTRUCTION_REMOVE));
|
||||
});
|
||||
if (syncItems.at(lastChangeInstruction + 1)->_instruction == CSYNC_INSTRUCTION_REMOVE) {
|
||||
|
|
|
@ -123,11 +123,11 @@ QIcon Theme::applicationIcon() const
|
|||
* helper to load a icon from either the icon theme the desktop provides or from
|
||||
* the apps Qt resources.
|
||||
*/
|
||||
QIcon Theme::themeIcon(const QString &name, bool sysTray, bool sysTrayMenuVisible) const
|
||||
QIcon Theme::themeIcon(const QString &name, bool sysTray) const
|
||||
{
|
||||
QString flavor;
|
||||
if (sysTray) {
|
||||
flavor = systrayIconFlavor(_mono, sysTrayMenuVisible);
|
||||
flavor = systrayIconFlavor(_mono);
|
||||
} else {
|
||||
flavor = QLatin1String("colored");
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ QIcon Theme::themeIcon(const QString &name, bool sysTray, bool sysTrayMenuVisibl
|
|||
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
||||
// This defines the icon as a template and enables automatic macOS color handling
|
||||
// See https://bugreports.qt.io/browse/QTBUG-42109
|
||||
cached.setIsMask(_mono && sysTray && !sysTrayMenuVisible);
|
||||
cached.setIsMask(_mono && sysTray);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
@ -270,18 +270,11 @@ QString Theme::defaultClientFolder() const
|
|||
return appName();
|
||||
}
|
||||
|
||||
QString Theme::systrayIconFlavor(bool mono, bool sysTrayMenuVisible) const
|
||||
QString Theme::systrayIconFlavor(bool mono) const
|
||||
{
|
||||
Q_UNUSED(sysTrayMenuVisible)
|
||||
QString flavor;
|
||||
if (mono) {
|
||||
flavor = Utility::hasDarkSystray() ? QLatin1String("white") : QLatin1String("black");
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
if (sysTrayMenuVisible) {
|
||||
flavor = QLatin1String("white");
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
flavor = QLatin1String("colored");
|
||||
}
|
||||
|
@ -395,7 +388,7 @@ QVariant Theme::customMedia(CustomMediaType type)
|
|||
return re;
|
||||
}
|
||||
|
||||
QIcon Theme::syncStateIcon(SyncResult::Status status, bool sysTray, bool sysTrayMenuVisible) const
|
||||
QIcon Theme::syncStateIcon(SyncResult::Status status, bool sysTray) const
|
||||
{
|
||||
// FIXME: Mind the size!
|
||||
QString statusIcon;
|
||||
|
@ -427,7 +420,7 @@ QIcon Theme::syncStateIcon(SyncResult::Status status, bool sysTray, bool sysTray
|
|||
statusIcon = QLatin1String("state-error");
|
||||
}
|
||||
|
||||
return themeIcon(statusIcon, sysTray, sysTrayMenuVisible);
|
||||
return themeIcon(statusIcon, sysTray);
|
||||
}
|
||||
|
||||
QIcon Theme::folderDisabledIcon() const
|
||||
|
@ -435,9 +428,9 @@ QIcon Theme::folderDisabledIcon() const
|
|||
return themeIcon(QLatin1String("state-pause"));
|
||||
}
|
||||
|
||||
QIcon Theme::folderOfflineIcon(bool sysTray, bool sysTrayMenuVisible) const
|
||||
QIcon Theme::folderOfflineIcon(bool sysTray) const
|
||||
{
|
||||
return themeIcon(QLatin1String("state-offline"), sysTray, sysTrayMenuVisible);
|
||||
return themeIcon(QLatin1String("state-offline"), sysTray);
|
||||
}
|
||||
|
||||
QColor Theme::wizardHeaderTitleColor() const
|
||||
|
|
|
@ -93,10 +93,10 @@ public:
|
|||
/**
|
||||
* get an sync state icon
|
||||
*/
|
||||
virtual QIcon syncStateIcon(SyncResult::Status, bool sysTray = false, bool sysTrayMenuVisible = false) const;
|
||||
virtual QIcon syncStateIcon(SyncResult::Status, bool sysTray = false) const;
|
||||
|
||||
virtual QIcon folderDisabledIcon() const;
|
||||
virtual QIcon folderOfflineIcon(bool sysTray = false, bool sysTrayMenuVisible = false) const;
|
||||
virtual QIcon folderOfflineIcon(bool sysTray = false) const;
|
||||
virtual QIcon applicationIcon() const;
|
||||
#endif
|
||||
|
||||
|
@ -166,7 +166,7 @@ public:
|
|||
virtual QString enforcedLocale() const { return QString(); }
|
||||
|
||||
/** colored, white or black */
|
||||
QString systrayIconFlavor(bool mono, bool sysTrayMenuVisible = false) const;
|
||||
QString systrayIconFlavor(bool mono) const;
|
||||
|
||||
#ifndef TOKEN_AUTH_ONLY
|
||||
/**
|
||||
|
@ -449,7 +449,7 @@ public:
|
|||
|
||||
protected:
|
||||
#ifndef TOKEN_AUTH_ONLY
|
||||
QIcon themeIcon(const QString &name, bool sysTray = false, bool sysTrayMenuVisible = false) const;
|
||||
QIcon themeIcon(const QString &name, bool sysTray = false) const;
|
||||
#endif
|
||||
Theme();
|
||||
|
||||
|
|
|
@ -66,6 +66,8 @@ list(APPEND FolderMan_SRC ../src/gui/guiutility.cpp )
|
|||
list(APPEND FolderMan_SRC ../src/gui/navigationpanehelper.cpp )
|
||||
list(APPEND FolderMan_SRC ../src/gui/connectionvalidator.cpp )
|
||||
list(APPEND FolderMan_SRC ../src/gui/clientproxy.cpp )
|
||||
list(APPEND FolderMan_SRC ../src/gui/ocsjob.cpp )
|
||||
list(APPEND FolderMan_SRC ../src/gui/ocsnavigationappsjob.cpp )
|
||||
list(APPEND FolderMan_SRC ../src/gui/accountstate.cpp )
|
||||
list(APPEND FolderMan_SRC ../src/gui/remotewipe.cpp )
|
||||
list(APPEND FolderMan_SRC ${FolderWatcher_SRC})
|
||||
|
@ -76,11 +78,13 @@ SET(RemoteWipe_SRC ../src/gui/remotewipe.cpp)
|
|||
list(APPEND RemoteWipe_SRC ../src/gui/clientproxy.cpp )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/guiutility.cpp )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/connectionvalidator.cpp )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/ocsjob.cpp )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/ocsnavigationappsjob.cpp )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/accountstate.cpp )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/socketapi.cpp )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/folder.cpp )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/syncrunfilelog.cpp )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/folderwatcher_linux.cpp )
|
||||
list(APPEND RemoteWipe_SRC ${FolderWatcher_SRC} )
|
||||
list(APPEND RemoteWipe_SRC ../src/gui/folderwatcher.cpp )
|
||||
list(APPEND RemoteWipe_SRC ${RemoteWipe_SRC})
|
||||
list(APPEND RemoteWipe_SRC stubremotewipe.cpp )
|
||||
|
|
20
theme.qrc
|
@ -43,7 +43,7 @@
|
|||
<file>theme/white/state-sync-64.png</file>
|
||||
<file>theme/white/state-sync-128.png</file>
|
||||
<file>theme/white/state-sync-256.png</file>
|
||||
<file>theme/black/state-error-32.png</file>
|
||||
<file>theme/black/state-error-32.png</file>
|
||||
<file>theme/black/state-error-64.png</file>
|
||||
<file>theme/black/state-error-128.png</file>
|
||||
<file>theme/black/state-error-256.png</file>
|
||||
|
@ -80,7 +80,7 @@
|
|||
<file>theme/colored/state-warning-128.png</file>
|
||||
<file>theme/colored/state-warning-256.png</file>
|
||||
<file>theme/black/control-next.svg</file>
|
||||
<file>theme/black/control-prev.svg</file>
|
||||
<file>theme/black/control-prev.svg</file>
|
||||
<file>theme/black/state-error.svg</file>
|
||||
<file>theme/black/state-error-16.png</file>
|
||||
<file>theme/black/state-offline.svg</file>
|
||||
|
@ -101,8 +101,8 @@
|
|||
<file>theme/black/state-warning-64.png</file>
|
||||
<file>theme/black/state-warning-128.png</file>
|
||||
<file>theme/black/state-warning-256.png</file>
|
||||
<file>theme/white/control-next.svg</file>
|
||||
<file>theme/white/control-prev.svg</file>
|
||||
<file>theme/white/control-next.svg</file>
|
||||
<file>theme/white/control-prev.svg</file>
|
||||
<file>theme/white/state-error.svg</file>
|
||||
<file>theme/white/state-error-16.png</file>
|
||||
<file>theme/white/state-offline.svg</file>
|
||||
|
@ -131,5 +131,17 @@
|
|||
<file>theme/colored/wizard-nextcloud@2x.png</file>
|
||||
<file>theme/colored/wizard-talk.png</file>
|
||||
<file>theme/colored/wizard-talk@2x.png</file>
|
||||
<file>theme/white/folder.svg</file>
|
||||
<file>theme/white/more-apps.svg</file>
|
||||
<file>theme/white/talk-app.svg</file>
|
||||
<file>theme/white/caret-down.svg</file>
|
||||
<file>theme/black/caret-down.svg</file>
|
||||
<file>theme/white/user.svg</file>
|
||||
<file>theme/black/user.svg</file>
|
||||
<file>theme/white/add.svg</file>
|
||||
<file>theme/black/add.svg</file>
|
||||
<file>theme/black/activity.svg</file>
|
||||
<file>theme/black/bell.svg</file>
|
||||
<file>theme/black/state-info.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
2
theme/black/activity.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1"><path fill="#000" d="m32 2-20 36h22l-2 24 20-36h-22z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 140 B |
1
theme/black/add.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path fill="#000" d="M9.02 13.98h-2v-5h-5v-2h5v-5h2v5l5-.028V8.98h-5z"/></svg>
|
After Width: | Height: | Size: 175 B |
3
theme/black/bell.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16">
|
||||
<path d="m8 2c-0.5523 0-1 0.4477-1 1 0 0.0472 0.021 0.0873 0.0273 0.1328-1.7366 0.4362-3.0273 1.9953-3.0273 3.8672v2l-1 1v1h10v-1l-1-1v-2c0-1.8719-1.291-3.431-3.0273-3.8672 0.0063-0.0455 0.0273-0.0856 0.0273-0.1328 0-0.5523-0.4477-1-1-1zm-2 10c0 1.1046 0.8954 2 2 2s2-0.8954 2-2z" fill="#000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 400 B |
1
theme/black/caret-down.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path d="M4 6l4 4 4-3.994z" fill="#000"/></svg>
|
After Width: | Height: | Size: 145 B |
1
theme/black/state-info.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4.2333 4.2333" version="1.1" height="16" width="16"><g id="g3830" transform="matrix(.87498 0 0 .87498 .26458 -255.9)"><circle id="circle3818" stroke-width=".25066" fill="#000" r="2.1167" cy="294.88" cx="2.1167" /><g id="g3828" stroke-linejoin="round" stroke-linecap="round" fill="none" /></g><path style="fill:#ffffff;stroke-width:0.17859235" d="m 1.6076619,2.1122981 c 0.027682,0.068222 0.058043,0.1232286 0.115014,0.043934 0.072686,-0.047862 0.314322,-0.2548509 0.29682,-0.061078 C 1.953774,2.4553739 1.8705497,2.8125586 1.8105428,3.1738508 1.7403561,3.3728027 1.9237704,3.5430012 2.1028984,3.4078068 2.295421,3.3181535 2.4582973,3.1779584 2.6256382,3.0488362 2.599921,2.9911507 2.5809903,2.9077482 2.5191973,2.9868644 2.4356161,3.0297263 2.2566665,3.2222491 2.2163047,3.07116 2.2725613,2.681829 2.3904322,2.3041062 2.4600833,1.9170966 2.5309844,1.7376113 2.3950755,1.5200858 2.210054,1.6736753 1.985742,1.7836882 1.8010774,1.9562083 1.6076619,2.1122981 Z M 2.4041839,0.77839186 C 2.1702279,0.77446305 2.0636081,1.1609366 2.2889917,1.2561264 2.4716917,1.3236342 2.659928,1.1286114 2.6086721,0.94358974 2.5911701,0.8467927 2.5018738,0.77035521 2.4038266,0.77749894 Z" /></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
theme/black/user.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 16" height="16" width="16" version="1.1"><path d="m5 3.8c0 1.4 0.1 2.4 0.8 3.5 0.2 0.286 0.5 0.35 0.7 0.6 0.135 0.5 0.24 0.98 0.1 1.5-1.275 0.45-2.49 1-3.6 1.6-0.85 0.6-0.785 0.31-1 2.3-0.16 1.59 3.5 1.7 6 1.7s6.163-0.1 6-1.7c-0.215-2-0.23-1.71-1-2.3-1.1-0.654-2.45-1.167-3.6-1.6-0.15-0.56-0.04-0.973 0.1-1.5 0.235-0.25 0.5-0.363 0.7-0.6 0.69-0.885 0.8-2.425 0.8-3.5 0-1.59-1.43-2.8-3-2.8-1.75 0-3 1.43-3 2.8z" fill="#000"/></svg>
|
After Width: | Height: | Size: 486 B |
1
theme/white/add.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path fill="#fff" d="M9.02 13.98h-2v-5h-5v-2h5v-5h2v5l5-.028V8.98h-5z"/></svg>
|
After Width: | Height: | Size: 175 B |
1
theme/white/caret-down.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path d="M4 6l4 4 4-3.994z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 145 B |
1
theme/white/folder.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" version="1.1" height="16"><path fill="#fff" d="m1.5 2c-0.25 0-0.5 0.25-0.5 0.5v11c0 0.26 0.24 0.5 0.5 0.5h13c0.26 0 0.5-0.241 0.5-0.5v-9c0-0.25-0.25-0.5-0.5-0.5h-6.5l-2-2z"/></svg>
|
After Width: | Height: | Size: 252 B |
1
theme/white/more-apps.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="32" height="32" enable-background="new 0 0 595.275 311.111" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.44643 0 0 .44643 260.1 -.096653)" stroke-width="2"><path d="m-572.71 3.5765c-1.108 0-2 0.892-2 2v4c0 1.108 0.892 2 2 2s2-0.892 2-2v-4c0-1.108-0.892-2-2-2zm16 0c-1.108 0-2 0.892-2 2v4c0 1.108 0.892 2 2 2s2-0.892 2-2v-4c0-1.108-0.892-2-2-2zm-13 4v2c0 1.662-1.338 3-3 3s-3-1.338-3-3v-1.875c-1.728 0.44254-3 2.0052-3 3.875v16c0 2.216 1.784 4 4 4h20c2.216 0 4-1.784 4-4v-16c0-1.8698-1.272-3.4325-3-3.875v1.875c0 1.662-1.338 3-3 3s-3-1.338-3-3v-2zm-5.9062 9h21.812c0.0554 0 0.0937 0.03835 0.0937 0.09375v11.812c0 0.0554-0.0384 0.09375-0.0937 0.09375h-21.812c-0.0554 0-0.0937-0.03835-0.0937-0.09375v-11.812c0-0.0554 0.0384-0.09375 0.0937-0.09375z" fill="#fff" stroke-width="4"/></g><g fill="#fff"><path d="m20.551 3.0408c-0.96 0-1.7744 0.70205-1.7744 1.608 0.0068 0.28636 0.03242 0.6395 0.20332 1.3862v0.0185l0.0185 0.01849c0.05485 0.15714 0.13469 0.24703 0.24028 0.36967s0.23147 0.26699 0.35117 0.38815c0.0141 0.01427 0.02311 0.02307 0.03695 0.03699 0.02376 0.10331 0.0525 0.21449 0.07393 0.31422 0.05703 0.26534 0.05118 0.45324 0.03695 0.5175-0.4125 0.14484-0.9257 0.31734-1.3862 0.5175-0.25853 0.1124-0.49247 0.21277-0.68385 0.33271-0.19138 0.11994-0.38172 0.21055-0.44358 0.48057-8.5e-4 0.01228-8.5e-4 0.02466 0 0.03699-0.06045 0.55505-0.1519 1.3713-0.2218 1.9223-0.01509 0.11598 0.04603 0.23822 0.14786 0.29574 0.8361 0.45164 2.1205 0.6334 3.4008 0.62845 1.2804-5e-3 2.5545-0.19746 3.3638-0.62845 0.10182-0.05751 0.16295-0.17976 0.14786-0.29574-0.02233-0.17222-0.04973-0.56055-0.07393-0.94265-0.02419-0.3821-0.04521-0.758-0.07393-0.97965-0.01-0.05495-0.036-0.10688-0.07393-0.14786-0.25712-0.30704-0.64125-0.49471-1.0905-0.6839-0.41012-0.1727-0.89095-0.35204-1.3677-0.5545-0.02669-0.05945-0.05319-0.23241 0-0.49906 0.01427-0.0716 0.03665-0.14828 0.05545-0.2218 0.04481-0.05018 0.07973-0.0912 0.12938-0.14787 0.10589-0.12086 0.21967-0.24765 0.3142-0.36967 0.09454-0.12202 0.17188-0.2267 0.2218-0.36967l0.01849-0.0185c0.19319-0.77975 0.1933-1.1051 0.20332-1.3862v-0.0185c0-0.906-0.81435-1.608-1.7744-1.608zm5.0755-1.4756c-1.3996 0-2.5869 1.0236-2.5869 2.3444 0.0099 0.41749 0.04727 0.93235 0.29642 2.021v0.02695l0.02694 0.02695c0.07998 0.22909 0.19637 0.36014 0.35031 0.53895 0.15394 0.1788 0.33747 0.38924 0.512 0.5659 0.02052 0.02078 0.03367 0.03367 0.05389 0.05391 0.03463 0.15061 0.07654 0.3127 0.10779 0.4581 0.08314 0.38683 0.07461 0.6608 0.0539 0.75455-0.6014 0.21117-1.3496 0.46264-2.021 0.75455-0.37693 0.16386-0.718 0.31019-0.99705 0.48505-0.27904 0.17486-0.55655 0.30697-0.64675 0.70065-0.0013 0.01794-0.0013 0.03596 0 0.05391-0.08814 0.80925-0.22146 1.9993-0.32336 2.8025-0.02199 0.16908 0.06712 0.34731 0.21558 0.43115 1.219 0.65845 3.0916 0.9235 4.9584 0.9162 1.8666-0.0073 3.7243-0.28787 4.9044-0.9162 0.14846-0.08384 0.23758-0.26207 0.21558-0.43115-0.03255-0.25107-0.0725-0.8172-0.10779-1.3743-0.03527-0.55705-0.06592-1.105-0.10778-1.4282-0.01461-0.0801-0.05248-0.15582-0.10779-0.21558-0.37486-0.44763-0.93495-0.72125-1.5899-0.99705-0.59795-0.25178-1.299-0.51325-1.994-0.8084-0.03889-0.08667-0.07756-0.33883 0-0.7276 0.02082-0.10438 0.05344-0.21619 0.08084-0.32336 0.06533-0.07317 0.11624-0.13296 0.18863-0.21558 0.15438-0.1762 0.32027-0.36105 0.4581-0.53895 0.13783-0.17789 0.2506-0.3305 0.32336-0.53895l0.02695-0.02694c0.2825-1.1368 0.2825-1.6111 0.297-2.021v-0.02695c0-1.3208-1.1874-2.3444-2.5869-2.3444z" color="#000000" style="text-indent:0;text-transform:none"/><path d="m1.7778 19.75c-0.4309 0-0.7778 0.3472-0.7778 0.77805v7.195c0 0.43094 0.34689 0.777 0.7778 0.777h12.444c0.431 0 0.778-0.346 0.778-0.777v-7.195c0-0.43105-0.347-0.77825-0.778-0.77825zm0.65625 0.8996 5.323 5.323h0.46183l5.347-5.323 0.5347 0.5347-3.184 3.2326 2.4062 2.4548-0.5347 0.5347-2.4548-2.4548-1.7744 1.7986h-1.1175l-1.774-1.798-2.455 2.479-0.53475-0.5595 2.4308-2.4545-3.2085-3.2326z"/><path d="m18.5 22.25a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5zm5.5 0a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5zm5.5 0a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 1 1 0-3.5z"/></g></svg>
|
After Width: | Height: | Size: 4 KiB |
1
theme/white/talk-app.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16"><path d="m7.9992 0.999a6.9993 6.9994 0 0 0 -6.9992 6.9996 6.9993 6.9994 0 0 0 6.9992 6.9994 6.9993 6.9994 0 0 0 3.6308 -1.024c0.86024 0.34184 2.7871 1.356 3.2457 0.91794 0.47922-0.45765-0.56261-2.6116-0.81238-3.412a6.9993 6.9994 0 0 0 0.935 -3.4814 6.9993 6.9994 0 0 0 -6.9991 -6.9993zm0.0008 2.6611a4.34 4.3401 0 0 1 4.34 4.3401 4.34 4.3401 0 0 1 -4.34 4.3398 4.34 4.3401 0 0 1 -4.34 -4.3398 4.34 4.3401 0 0 1 4.34 -4.3401z" stroke-width="0.14" fill="#fff"/></svg>
|
After Width: | Height: | Size: 563 B |
1
theme/white/user.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 16" height="16" width="16" version="1.1"><path d="m5 3.8c0 1.4 0.1 2.4 0.8 3.5 0.2 0.286 0.5 0.35 0.7 0.6 0.135 0.5 0.24 0.98 0.1 1.5-1.275 0.45-2.49 1-3.6 1.6-0.85 0.6-0.785 0.31-1 2.3-0.16 1.59 3.5 1.7 6 1.7s6.163-0.1 6-1.7c-0.215-2-0.23-1.71-1-2.3-1.1-0.654-2.45-1.167-3.6-1.6-0.15-0.56-0.04-0.973 0.1-1.5 0.235-0.25 0.5-0.363 0.7-0.6 0.69-0.885 0.8-2.425 0.8-3.5 0-1.59-1.43-2.8-3-2.8-1.75 0-3 1.43-3 2.8z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 486 B |