mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
Merge pull request #6386
Mick Sayson (3): fix(filetransfer): Fix UI inconsistencies with pause/resume refactor(filetransfer): Move file transfer progress into ToxFile feat(filesform): Add in progress transfers to files form
This commit is contained in:
commit
cea54c17c9
@ -227,8 +227,8 @@ set(${PROJECT_NAME}_SOURCES
|
||||
src/chatlog/documentcache.h
|
||||
src/chatlog/pixmapcache.cpp
|
||||
src/chatlog/pixmapcache.h
|
||||
src/chatlog/toxfileprogress.cpp
|
||||
src/chatlog/toxfileprogress.h
|
||||
src/core/toxfileprogress.cpp
|
||||
src/core/toxfileprogress.h
|
||||
src/chatlog/textformatter.cpp
|
||||
src/chatlog/textformatter.h
|
||||
src/core/coreav.cpp
|
||||
|
@ -43,6 +43,7 @@ auto_test(core core "${${PROJECT_NAME}_RESOURCES}")
|
||||
auto_test(core contactid "")
|
||||
auto_test(core toxid "")
|
||||
auto_test(core toxstring "")
|
||||
auto_test(core fileprogress "")
|
||||
auto_test(chatlog textformatter "")
|
||||
auto_test(net bsu "${${PROJECT_NAME}_RESOURCES}") # needs nodes list
|
||||
auto_test(chatlog chatlinestorage "")
|
||||
@ -56,6 +57,7 @@ auto_test(model messageprocessor "")
|
||||
auto_test(model sessionchatlog "")
|
||||
auto_test(model exiftransform "")
|
||||
auto_test(model notificationgenerator "${MOCK_SOURCES}")
|
||||
auto_test(widget filesform "")
|
||||
|
||||
if (UNIX)
|
||||
auto_test(platform posixsignalnotifier "")
|
||||
|
6
res.qrc
6
res.qrc
@ -100,10 +100,13 @@
|
||||
<file>themes/dark/chatArea/typing.svg</file>
|
||||
<file>themes/dark/chatArea/error.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/no.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/no_dark.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/pause.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/pause_dark.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/yes.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/dir.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/arrow_white.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/arrow_black.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/browse.svg</file>
|
||||
<file>themes/dark/fileTransferInstance/filetransferWidget.css</file>
|
||||
<file>themes/dark/genericChatForm/genericChatForm.css</file>
|
||||
@ -164,10 +167,13 @@
|
||||
<file>themes/default/chatArea/typing.svg</file>
|
||||
<file>themes/default/chatArea/error.svg</file>
|
||||
<file>themes/default/fileTransferInstance/no.svg</file>
|
||||
<file>themes/default/fileTransferInstance/no_dark.svg</file>
|
||||
<file>themes/default/fileTransferInstance/pause.svg</file>
|
||||
<file>themes/default/fileTransferInstance/pause_dark.svg</file>
|
||||
<file>themes/default/fileTransferInstance/yes.svg</file>
|
||||
<file>themes/default/fileTransferInstance/dir.svg</file>
|
||||
<file>themes/default/fileTransferInstance/arrow_white.svg</file>
|
||||
<file>themes/default/fileTransferInstance/arrow_black.svg</file>
|
||||
<file>themes/default/fileTransferInstance/browse.svg</file>
|
||||
<file>themes/default/fileTransferInstance/filetransferWidget.css</file>
|
||||
<file>themes/default/genericChatForm/genericChatForm.css</file>
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "src/widget/style.h"
|
||||
#include "src/widget/widget.h"
|
||||
#include "src/model/exiftransform.h"
|
||||
#include "util/display.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDebug>
|
||||
@ -65,7 +66,7 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, CoreFile& _coreFile, Tox
|
||||
ui->previewButton->hide();
|
||||
ui->filenameLabel->setText(file.fileName);
|
||||
ui->progressBar->setValue(0);
|
||||
ui->fileSizeLabel->setText(getHumanReadableSize(file.filesize));
|
||||
ui->fileSizeLabel->setText(getHumanReadableSize(file.progress.getFileSize()));
|
||||
ui->etaLabel->setText("");
|
||||
|
||||
backgroundColorAnimation = new QVariantAnimation(this);
|
||||
@ -234,18 +235,6 @@ void FileTransferWidget::reloadTheme()
|
||||
updateBackgroundColor(lastStatus);
|
||||
}
|
||||
|
||||
QString FileTransferWidget::getHumanReadableSize(qint64 size)
|
||||
{
|
||||
static const char* suffix[] = {"B", "KiB", "MiB", "GiB", "TiB"};
|
||||
int exp = 0;
|
||||
|
||||
if (size > 0) {
|
||||
exp = std::min(static_cast<int>(log(size) / log(1024)), static_cast<int>(sizeof(suffix) / sizeof(suffix[0]) - 1));
|
||||
}
|
||||
|
||||
return QString().setNum(size / pow(1024, exp), 'f', exp > 1 ? 2 : 0).append(suffix[exp]);
|
||||
}
|
||||
|
||||
void FileTransferWidget::updateWidgetColor(ToxFile const& file)
|
||||
{
|
||||
if (lastStatus == file.status) {
|
||||
@ -321,19 +310,12 @@ void FileTransferWidget::updateFileProgress(ToxFile const& file)
|
||||
{
|
||||
switch (file.status) {
|
||||
case ToxFile::INITIALIZING:
|
||||
break;
|
||||
case ToxFile::PAUSED:
|
||||
fileProgress.resetSpeed();
|
||||
break;
|
||||
case ToxFile::TRANSMITTING: {
|
||||
if (!fileProgress.needsUpdate()) {
|
||||
break;
|
||||
}
|
||||
|
||||
fileProgress.addSample(file);
|
||||
auto speed = fileProgress.getSpeed();
|
||||
auto progress = fileProgress.getProgress();
|
||||
auto remainingTime = fileProgress.getTimeLeftSeconds();
|
||||
auto speed = file.progress.getSpeed();
|
||||
auto progress = file.progress.getProgress();
|
||||
auto remainingTime = file.progress.getTimeLeftSeconds();
|
||||
|
||||
ui->progressBar->setValue(static_cast<int>(progress * 100.0));
|
||||
|
||||
@ -525,11 +507,12 @@ void FileTransferWidget::updateWidget(ToxFile const& file)
|
||||
|
||||
fileInfo = file;
|
||||
|
||||
// If we repainted on every packet our gui would be *very* slow
|
||||
bool bTransmitNeedsUpdate = fileProgress.needsUpdate();
|
||||
bool shouldUpdateFileProgress = file.status != ToxFile::TRANSMITTING || lastTransmissionUpdate ==
|
||||
QTime() || lastTransmissionUpdate.msecsTo(file.progress.lastSampleTime()) > 1000;
|
||||
|
||||
updatePreview(file);
|
||||
updateFileProgress(file);
|
||||
if (shouldUpdateFileProgress)
|
||||
updateFileProgress(file);
|
||||
updateWidgetText(file);
|
||||
updateWidgetColor(file);
|
||||
setupButtons(file);
|
||||
@ -537,14 +520,8 @@ void FileTransferWidget::updateWidget(ToxFile const& file)
|
||||
|
||||
lastStatus = file.status;
|
||||
|
||||
// trigger repaint
|
||||
switch (file.status) {
|
||||
case ToxFile::TRANSMITTING:
|
||||
if (!bTransmitNeedsUpdate) {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
default:
|
||||
if (shouldUpdateFileProgress) {
|
||||
lastTransmissionUpdate = QTime::currentTime();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,6 @@
|
||||
#include <QWidget>
|
||||
|
||||
#include "src/chatlog/chatlinecontent.h"
|
||||
#include "src/chatlog/toxfileprogress.h"
|
||||
#include "src/core/toxfile.h"
|
||||
|
||||
class CoreFile;
|
||||
@ -43,8 +42,6 @@ public:
|
||||
explicit FileTransferWidget(QWidget* parent, CoreFile& _coreFile, ToxFile file);
|
||||
virtual ~FileTransferWidget();
|
||||
bool isActive() const;
|
||||
static QString getHumanReadableSize(qint64 size);
|
||||
|
||||
void onFileTransferUpdate(ToxFile file);
|
||||
|
||||
protected:
|
||||
@ -81,7 +78,6 @@ private:
|
||||
private:
|
||||
CoreFile& coreFile;
|
||||
Ui::FileTransferWidget* ui;
|
||||
ToxFileProgress fileProgress;
|
||||
ToxFile fileInfo;
|
||||
QVariantAnimation* backgroundColorAnimation = nullptr;
|
||||
QVariantAnimation* buttonColorAnimation = nullptr;
|
||||
@ -90,6 +86,7 @@ private:
|
||||
QColor buttonBackgroundColor;
|
||||
|
||||
bool active;
|
||||
QTime lastTransmissionUpdate;
|
||||
ToxFile::FileStatus lastStatus = ToxFile::INITIALIZING;
|
||||
|
||||
};
|
||||
|
@ -1,93 +0,0 @@
|
||||
/*
|
||||
Copyright © 2018-2019 by The qTox Project Contributors
|
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox.
|
||||
|
||||
qTox is libre 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 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
qTox is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "toxfileprogress.h"
|
||||
|
||||
#include "src/core/toxfile.h"
|
||||
|
||||
bool ToxFileProgress::needsUpdate() const
|
||||
{
|
||||
QTime now = QTime::currentTime();
|
||||
qint64 dt = lastTick.msecsTo(now); // ms
|
||||
|
||||
if (dt < 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ToxFileProgress::addSample(ToxFile const& file)
|
||||
{
|
||||
QTime now = QTime::currentTime();
|
||||
qint64 dt = lastTick.msecsTo(now); // ms
|
||||
|
||||
if (dt < 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ETA, speed
|
||||
qreal deltaSecs = dt / 1000.0;
|
||||
|
||||
// (can't use ::abs or ::max on unsigned types substraction, they'd just overflow)
|
||||
quint64 deltaBytes = file.bytesSent > lastBytesSent ? file.bytesSent - lastBytesSent
|
||||
: lastBytesSent - file.bytesSent;
|
||||
qreal bytesPerSec = static_cast<int>(static_cast<qreal>(deltaBytes) / deltaSecs);
|
||||
|
||||
// Update member variables
|
||||
meanIndex = meanIndex % TRANSFER_ROLLING_AVG_COUNT;
|
||||
meanData[meanIndex++] = bytesPerSec;
|
||||
|
||||
double meanBytesPerSec = 0.0;
|
||||
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i) {
|
||||
meanBytesPerSec += meanData[i];
|
||||
}
|
||||
meanBytesPerSec /= static_cast<qreal>(TRANSFER_ROLLING_AVG_COUNT);
|
||||
|
||||
lastTick = now;
|
||||
|
||||
progress = static_cast<double>(file.bytesSent) / static_cast<double>(file.filesize);
|
||||
speedBytesPerSecond = meanBytesPerSec;
|
||||
timeLeftSeconds = (file.filesize - file.bytesSent) / getSpeed();
|
||||
|
||||
lastBytesSent = file.bytesSent;
|
||||
}
|
||||
|
||||
void ToxFileProgress::resetSpeed()
|
||||
{
|
||||
meanIndex = 0;
|
||||
for (auto& item : meanData) {
|
||||
item = 0;
|
||||
}
|
||||
}
|
||||
|
||||
double ToxFileProgress::getProgress() const
|
||||
{
|
||||
return progress;
|
||||
}
|
||||
|
||||
double ToxFileProgress::getSpeed() const
|
||||
{
|
||||
return speedBytesPerSecond;
|
||||
}
|
||||
|
||||
double ToxFileProgress::getTimeLeftSeconds() const
|
||||
{
|
||||
return timeLeftSeconds;
|
||||
}
|
@ -132,8 +132,7 @@ void CoreFile::sendAvatarFile(uint32_t friendId, const QByteArray& data)
|
||||
return;
|
||||
}
|
||||
|
||||
ToxFile file{fileNum, friendId, "", "", ToxFile::SENDING};
|
||||
file.filesize = filesize;
|
||||
ToxFile file{fileNum, friendId, "", "", filesize, ToxFile::SENDING};
|
||||
file.fileKind = TOX_FILE_KIND_AVATAR;
|
||||
file.avatarData = data;
|
||||
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
|
||||
@ -158,8 +157,7 @@ void CoreFile::sendFile(uint32_t friendId, QString filename, QString filePath,
|
||||
}
|
||||
qDebug() << QString("sendFile: Created file sender %1 with friend %2").arg(fileNum).arg(friendId);
|
||||
|
||||
ToxFile file{fileNum, friendId, fileName.getQString(), filePath, ToxFile::SENDING};
|
||||
file.filesize = filesize;
|
||||
ToxFile file{fileNum, friendId, fileName.getQString(), filePath, static_cast<uint64_t>(filesize), ToxFile::SENDING};
|
||||
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
|
||||
tox_file_get_file_id(tox, friendId, fileNum, reinterpret_cast<uint8_t*>(file.resumeFileId.data()),
|
||||
nullptr);
|
||||
@ -191,6 +189,7 @@ void CoreFile::pauseResumeFile(uint32_t friendId, uint32_t fileId)
|
||||
|
||||
if (file->pauseStatus.paused()) {
|
||||
file->status = ToxFile::PAUSED;
|
||||
file->progress.resetSpeed();
|
||||
emit fileTransferPaused(*file);
|
||||
} else {
|
||||
file->status = ToxFile::TRANSMITTING;
|
||||
@ -360,8 +359,7 @@ void CoreFile::onFileReceiveCallback(Tox* tox, uint32_t friendId, uint32_t fileI
|
||||
qDebug() << QString("Received file request %1:%2 kind %3").arg(friendId).arg(fileId).arg(kind);
|
||||
}
|
||||
|
||||
ToxFile file{fileId, friendId, filename.getBytes(), "", ToxFile::RECEIVING};
|
||||
file.filesize = filesize;
|
||||
ToxFile file{fileId, friendId, filename.getBytes(), "", filesize, ToxFile::RECEIVING};
|
||||
file.fileKind = kind;
|
||||
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
|
||||
tox_file_get_file_id(tox, friendId, fileId, reinterpret_cast<uint8_t*>(file.resumeFileId.data()),
|
||||
@ -390,8 +388,7 @@ void CoreFile::handleAvatarOffer(uint32_t friendId, uint32_t fileId, bool accept
|
||||
.arg(fileId);
|
||||
tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_RESUME, nullptr);
|
||||
|
||||
ToxFile file{fileId, friendId, "<avatar>", "", ToxFile::RECEIVING};
|
||||
file.filesize = 0;
|
||||
ToxFile file{fileId, friendId, "<avatar>", "", 0, ToxFile::RECEIVING};
|
||||
file.fileKind = TOX_FILE_KIND_AVATAR;
|
||||
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
|
||||
tox_file_get_file_id(tox, friendId, fileId, reinterpret_cast<uint8_t*>(file.resumeFileId.data()),
|
||||
@ -451,7 +448,6 @@ void CoreFile::onFileDataCallback(Tox* tox, uint32_t friendId, uint32_t fileId,
|
||||
file->status = ToxFile::FINISHED;
|
||||
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
|
||||
emit coreFile->fileTransferFinished(*file);
|
||||
emit coreFile->fileUploadFinished(file->filePath);
|
||||
}
|
||||
coreFile->removeFile(friendId, fileId);
|
||||
return;
|
||||
@ -475,7 +471,7 @@ void CoreFile::onFileDataCallback(Tox* tox, uint32_t friendId, uint32_t fileId,
|
||||
coreFile->removeFile(friendId, fileId);
|
||||
return;
|
||||
}
|
||||
file->bytesSent += length;
|
||||
file->progress.addSample(file->progress.getBytesSent() + length);
|
||||
file->hashGenerator->addData(reinterpret_cast<const char*>(data.get()), length);
|
||||
}
|
||||
|
||||
@ -500,7 +496,7 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil
|
||||
return;
|
||||
}
|
||||
|
||||
if (file->bytesSent != position) {
|
||||
if (file->progress.getBytesSent() != position) {
|
||||
qWarning("onFileRecvChunkCallback: Received a chunk out-of-order, aborting transfer");
|
||||
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
|
||||
file->status = ToxFile::CANCELED;
|
||||
@ -522,7 +518,6 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil
|
||||
}
|
||||
} else {
|
||||
emit coreFile->fileTransferFinished(*file);
|
||||
emit coreFile->fileDownloadFinished(file->filePath);
|
||||
}
|
||||
coreFile->removeFile(friendId, fileId);
|
||||
return;
|
||||
@ -533,7 +528,7 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil
|
||||
} else {
|
||||
file->file->write(reinterpret_cast<const char*>(data), length);
|
||||
}
|
||||
file->bytesSent += length;
|
||||
file->progress.addSample(file->progress.getBytesSent() + length);
|
||||
file->hashGenerator->addData(reinterpret_cast<const char*>(data), length);
|
||||
|
||||
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
|
||||
|
@ -69,8 +69,6 @@ signals:
|
||||
void fileTransferAccepted(ToxFile file);
|
||||
void fileTransferCancelled(ToxFile file);
|
||||
void fileTransferFinished(ToxFile file);
|
||||
void fileUploadFinished(const QString& path);
|
||||
void fileDownloadFinished(const QString& path);
|
||||
void fileTransferPaused(ToxFile file);
|
||||
void fileTransferInfo(ToxFile file);
|
||||
void fileTransferRemotePausedUnpaused(ToxFile file, bool paused);
|
||||
|
@ -34,21 +34,29 @@
|
||||
* @brief Data file (default) or avatar
|
||||
*/
|
||||
|
||||
ToxFile::ToxFile()
|
||||
: fileKind(0)
|
||||
, fileNum(0)
|
||||
, friendId(0)
|
||||
, status(INITIALIZING)
|
||||
, direction(SENDING)
|
||||
, progress(0)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @brief ToxFile constructor
|
||||
*/
|
||||
ToxFile::ToxFile(uint32_t fileNum, uint32_t friendId, QString filename, QString filePath,
|
||||
FileDirection Direction)
|
||||
uint64_t filesize, FileDirection Direction)
|
||||
: fileKind{TOX_FILE_KIND_DATA}
|
||||
, fileNum(fileNum)
|
||||
, friendId(friendId)
|
||||
, fileName{filename}
|
||||
, filePath{filePath}
|
||||
, file{new QFile(filePath)}
|
||||
, bytesSent{0}
|
||||
, filesize{0}
|
||||
, status{INITIALIZING}
|
||||
, direction{Direction}
|
||||
, progress(filesize)
|
||||
{}
|
||||
|
||||
bool ToxFile::operator==(const ToxFile& other) const
|
||||
|
@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "src/core/toxfilepause.h"
|
||||
#include "src/core/toxfileprogress.h"
|
||||
|
||||
#include <QString>
|
||||
#include <memory>
|
||||
@ -50,9 +51,9 @@ struct ToxFile
|
||||
RECEIVING = 1,
|
||||
};
|
||||
|
||||
ToxFile() = default;
|
||||
ToxFile();
|
||||
ToxFile(uint32_t FileNum, uint32_t FriendId, QString FileName, QString filePath,
|
||||
FileDirection Direction);
|
||||
uint64_t filesize, FileDirection Direction);
|
||||
|
||||
bool operator==(const ToxFile& other) const;
|
||||
bool operator!=(const ToxFile& other) const;
|
||||
@ -66,12 +67,11 @@ struct ToxFile
|
||||
QString fileName;
|
||||
QString filePath;
|
||||
std::shared_ptr<QFile> file;
|
||||
quint64 bytesSent;
|
||||
quint64 filesize;
|
||||
FileStatus status;
|
||||
FileDirection direction;
|
||||
QByteArray avatarData;
|
||||
QByteArray resumeFileId;
|
||||
std::shared_ptr<QCryptographicHash> hashGenerator = std::make_shared<QCryptographicHash>(QCryptographicHash::Sha256);
|
||||
ToxFilePause pauseStatus;
|
||||
ToxFileProgress progress;
|
||||
};
|
||||
|
137
src/core/toxfileprogress.cpp
Normal file
137
src/core/toxfileprogress.cpp
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright © 2021 by The qTox Project Contributors
|
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox.
|
||||
|
||||
qTox is libre 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 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
qTox is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "toxfileprogress.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
ToxFileProgress::ToxFileProgress(uint64_t filesize, int samplePeriodMs)
|
||||
: filesize(filesize)
|
||||
, samplePeriodMs(samplePeriodMs)
|
||||
{
|
||||
if (samplePeriodMs < 0) {
|
||||
qWarning("Invalid sample rate, healing to 1000ms");
|
||||
this->samplePeriodMs = 1000;
|
||||
}
|
||||
}
|
||||
|
||||
QTime ToxFileProgress::lastSampleTime() const
|
||||
{
|
||||
return samples[activeSample].timestamp;
|
||||
}
|
||||
|
||||
bool ToxFileProgress::addSample(uint64_t bytesSent, QTime now)
|
||||
{
|
||||
if (bytesSent > filesize) {
|
||||
qWarning("Bytes sent exceeds file size, ignoring sample");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* active = &samples[activeSample];
|
||||
auto* inactive = &samples[!activeSample];
|
||||
|
||||
if (bytesSent < active->bytesSent || bytesSent < inactive->bytesSent) {
|
||||
qWarning("Bytes sent has decreased since last sample, ignoring sample");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (now < active->timestamp || now < inactive->timestamp) {
|
||||
qWarning("Sample time has gone backwards, clearing progress buffer");
|
||||
resetSpeed();
|
||||
}
|
||||
|
||||
// Ensure both samples are initialized
|
||||
if (inactive->timestamp == QTime()) {
|
||||
inactive->bytesSent = bytesSent;
|
||||
inactive->timestamp = now;
|
||||
}
|
||||
|
||||
if (active->timestamp == QTime()) {
|
||||
active->bytesSent = bytesSent;
|
||||
active->timestamp = now;
|
||||
}
|
||||
|
||||
if (active->timestamp.msecsTo(now) >= samplePeriodMs) {
|
||||
// Swap samples and set the newly active sample
|
||||
activeSample = !activeSample;
|
||||
std::swap(active, inactive);
|
||||
}
|
||||
|
||||
active->bytesSent = bytesSent;
|
||||
active->timestamp = now;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ToxFileProgress::resetSpeed()
|
||||
{
|
||||
for (auto& sample : samples) {
|
||||
sample.timestamp = QTime();
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t ToxFileProgress::getBytesSent() const
|
||||
{
|
||||
return samples[activeSample].bytesSent;
|
||||
}
|
||||
|
||||
double ToxFileProgress::getProgress() const
|
||||
{
|
||||
return double(samples[activeSample].bytesSent) / filesize;
|
||||
}
|
||||
|
||||
double ToxFileProgress::getSpeed() const
|
||||
{
|
||||
if (samples.size() > 0
|
||||
&& samples[activeSample].bytesSent == filesize) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const auto sampleTimeInvalid = [](const Sample& sample) {
|
||||
return sample.timestamp == QTime();
|
||||
};
|
||||
|
||||
if (std::any_of(samples.cbegin(), samples.cend(), sampleTimeInvalid)) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
if (samples[0].timestamp == samples[1].timestamp) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const auto& active = samples[activeSample];
|
||||
const auto& inactive = samples[!activeSample];
|
||||
|
||||
return (active.bytesSent - inactive.bytesSent) / double(inactive.timestamp.msecsTo(active.timestamp)) * 1000.0;
|
||||
}
|
||||
|
||||
double ToxFileProgress::getTimeLeftSeconds() const
|
||||
{
|
||||
if (samples.size() > 0
|
||||
&& samples[activeSample].bytesSent == filesize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto speed = getSpeed();
|
||||
if (speed == 0.0f) {
|
||||
return std::numeric_limits<float>::infinity();
|
||||
}
|
||||
|
||||
return double(filesize - samples[activeSample].bytesSent) / getSpeed();
|
||||
}
|
@ -21,29 +21,35 @@
|
||||
|
||||
#include <QTime>
|
||||
|
||||
struct ToxFile;
|
||||
#include <array>
|
||||
|
||||
class ToxFileProgress
|
||||
{
|
||||
public:
|
||||
bool needsUpdate() const;
|
||||
void addSample(ToxFile const& file);
|
||||
ToxFileProgress(uint64_t filesize, int samplePeriodMs = 4000);
|
||||
|
||||
QTime lastSampleTime() const;
|
||||
bool addSample(uint64_t bytesSent, QTime now = QTime::currentTime());
|
||||
void resetSpeed();
|
||||
|
||||
uint64_t getBytesSent() const;
|
||||
uint64_t getFileSize() const { return filesize; }
|
||||
double getProgress() const;
|
||||
double getSpeed() const;
|
||||
double getTimeLeftSeconds() const;
|
||||
|
||||
private:
|
||||
uint64_t lastBytesSent = 0;
|
||||
// Should never be modified, but do not want to lose assignment operators
|
||||
uint64_t filesize;
|
||||
size_t speedSampleCount;
|
||||
int samplePeriodMs;
|
||||
|
||||
static const uint8_t TRANSFER_ROLLING_AVG_COUNT = 4;
|
||||
uint8_t meanIndex = 0;
|
||||
double meanData[TRANSFER_ROLLING_AVG_COUNT] = {0.0};
|
||||
struct Sample
|
||||
{
|
||||
uint64_t bytesSent = 0;
|
||||
QTime timestamp;
|
||||
};
|
||||
|
||||
QTime lastTick = QTime::currentTime();
|
||||
|
||||
double speedBytesPerSecond;
|
||||
double timeLeftSeconds;
|
||||
double progress;
|
||||
std::array<Sample, 2> samples;
|
||||
uint8_t activeSample = 0;
|
||||
};
|
@ -229,7 +229,7 @@ void ChatHistory::onFileUpdated(const ToxPk& sender, const ToxFile& file)
|
||||
// initializing. If this is changed in the session chat log we'll end up
|
||||
// with a different order when loading from history
|
||||
history->addNewFileMessage(f.getPublicKey(), file.resumeFileId, file.fileName,
|
||||
file.filePath, file.filesize, sender,
|
||||
file.filePath, file.progress.getFileSize(), sender,
|
||||
QDateTime::currentDateTime(), username);
|
||||
break;
|
||||
}
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
#include "notificationgenerator.h"
|
||||
#include "src/chatlog/content/filetransferwidget.h"
|
||||
#include "util/display.h"
|
||||
|
||||
#include <QCollator>
|
||||
|
||||
@ -223,7 +224,7 @@ NotificationData NotificationGenerator::fileTransferNotification(const Friend* f
|
||||
{
|
||||
//: e.g. Bob - file transfer
|
||||
ret.title = tr("%1 - file transfer").arg(f->getDisplayedName());
|
||||
ret.message = filename + " (" + FileTransferWidget::getHumanReadableSize(fileSize) + ")";
|
||||
ret.message = filename + " (" + getHumanReadableSize(fileSize) + ")";
|
||||
}
|
||||
|
||||
ret.pixmap = getSenderAvatar(profile, f->getPublicKey());
|
||||
|
@ -1009,14 +1009,19 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
|
||||
case 'F': {
|
||||
it = std::next(row.begin(), fileOffset);
|
||||
assert(!it->isNull());
|
||||
ToxFile file;
|
||||
file.fileKind = TOX_FILE_KIND_DATA;
|
||||
file.resumeFileId = (*it++).toString().toUtf8();
|
||||
file.fileName = (*it++).toString();
|
||||
file.filePath = (*it++).toString();
|
||||
file.filesize = (*it++).toLongLong();
|
||||
file.direction = static_cast<ToxFile::FileDirection>((*it++).toLongLong());
|
||||
file.status = static_cast<ToxFile::FileStatus>((*it++).toLongLong());
|
||||
const auto fileKind = TOX_FILE_KIND_DATA;
|
||||
const auto resumeFileId = (*it++).toString().toUtf8();
|
||||
const auto fileName = (*it++).toString();
|
||||
const auto filePath = (*it++).toString();
|
||||
const auto filesize = (*it++).toLongLong();
|
||||
const auto direction = static_cast<ToxFile::FileDirection>((*it++).toLongLong());
|
||||
const auto status = static_cast<ToxFile::FileStatus>((*it++).toLongLong());
|
||||
|
||||
ToxFile file(0, 0, fileName, filePath, filesize, direction);
|
||||
file.fileKind = fileKind;
|
||||
file.resumeFileId = resumeFileId;
|
||||
file.status = status;
|
||||
|
||||
it = std::next(row.begin(), senderOffset);
|
||||
const auto senderKey = (*it++).toString();
|
||||
const auto senderName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
|
||||
|
@ -18,16 +18,410 @@
|
||||
*/
|
||||
|
||||
#include "filesform.h"
|
||||
#include "src/core/toxfile.h"
|
||||
#include "src/widget/contentlayout.h"
|
||||
#include "src/widget/translator.h"
|
||||
#include "src/widget/style.h"
|
||||
#include "src/widget/widget.h"
|
||||
#include "src/friendlist.h"
|
||||
#include "util/display.h"
|
||||
#include <QFileInfo>
|
||||
#include <QWindow>
|
||||
#include <QTableView>
|
||||
#include <QHeaderView>
|
||||
#include <QPushButton>
|
||||
#include <QPainter>
|
||||
#include <QMouseEvent>
|
||||
#include <cmath>
|
||||
|
||||
FilesForm::FilesForm()
|
||||
namespace {
|
||||
QRect pauseRect(const QStyleOptionViewItem& option)
|
||||
{
|
||||
float controlSize = option.rect.height() * 0.8f;
|
||||
float rectWidth = option.rect.width();
|
||||
float buttonHorizontalArea = rectWidth / 2;
|
||||
|
||||
// To center the button, we find the horizontal center and subtract half
|
||||
// our width from it
|
||||
int buttonXPos = std::round(option.rect.x() + buttonHorizontalArea / 2 - controlSize / 2);
|
||||
int buttonYPos = std::round(option.rect.y() + option.rect.height() * 0.1f);
|
||||
return QRect(buttonXPos, buttonYPos, controlSize, controlSize);
|
||||
}
|
||||
|
||||
QRect stopRect(const QStyleOptionViewItem& option)
|
||||
{
|
||||
float controlSize = option.rect.height() * 0.8;
|
||||
float rectWidth = option.rect.width();
|
||||
float buttonHorizontalArea = rectWidth / 2;
|
||||
|
||||
// To center the button, we find the horizontal center and subtract half
|
||||
// our width from it
|
||||
int buttonXPos = std::round(option.rect.x() + buttonHorizontalArea + buttonHorizontalArea / 2 - controlSize / 2);
|
||||
int buttonYPos = std::round(option.rect.y() + option.rect.height() * 0.1f);
|
||||
return QRect(buttonXPos, buttonYPos, controlSize, controlSize);
|
||||
}
|
||||
|
||||
QString fileStatusString(ToxFile file)
|
||||
{
|
||||
switch (file.status)
|
||||
{
|
||||
case ToxFile::INITIALIZING:
|
||||
return QObject::tr("Initializing");
|
||||
case ToxFile::TRANSMITTING:
|
||||
return QObject::tr("Transmitting");
|
||||
case ToxFile::FINISHED:
|
||||
return QObject::tr("Finished");
|
||||
case ToxFile::BROKEN:
|
||||
return QObject::tr("Broken");
|
||||
case ToxFile::CANCELED:
|
||||
return QObject::tr("Canceled");
|
||||
case ToxFile::PAUSED:
|
||||
if (file.pauseStatus.localPaused()) {
|
||||
return QObject::tr("Paused");
|
||||
} else {
|
||||
return QObject::tr("Remote paused");
|
||||
}
|
||||
}
|
||||
|
||||
qWarning("Corrupt file status %d", file.status);
|
||||
return "";
|
||||
}
|
||||
|
||||
bool fileTransferFailed(const ToxFile::FileStatus& status) {
|
||||
switch (status)
|
||||
{
|
||||
case ToxFile::INITIALIZING:
|
||||
case ToxFile::PAUSED:
|
||||
case ToxFile::TRANSMITTING:
|
||||
case ToxFile::FINISHED:
|
||||
return false;
|
||||
case ToxFile::BROKEN:
|
||||
case ToxFile::CANCELED:
|
||||
return true;
|
||||
}
|
||||
|
||||
qWarning("Invalid file status: %d", status);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool shouldProcessFileKind(uint8_t inKind)
|
||||
{
|
||||
auto kind = static_cast<TOX_FILE_KIND>(inKind);
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case TOX_FILE_KIND_DATA: return true;
|
||||
// Avatar sharing should be seamless, the user does not need to see
|
||||
// these in their file transfer list.
|
||||
case TOX_FILE_KIND_AVATAR: return false;
|
||||
}
|
||||
|
||||
qWarning("Unexpected file kind %d", kind);
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace FileTransferList
|
||||
{
|
||||
Column toFileTransferListColumn(int in) {
|
||||
if (in >= 0 && in < static_cast<int>(Column::invalid)) {
|
||||
return static_cast<Column>(in);
|
||||
}
|
||||
|
||||
qWarning("Invalid file transfer list column %d", in);
|
||||
return Column::invalid;
|
||||
}
|
||||
|
||||
QString toQString(Column column) {
|
||||
switch (column)
|
||||
{
|
||||
case Column::fileName:
|
||||
return QObject::tr("File Name");
|
||||
case Column::contact:
|
||||
return QObject::tr("Contact");
|
||||
case Column::progress:
|
||||
return QObject::tr("Progress");
|
||||
case Column::size:
|
||||
return QObject::tr("Size");
|
||||
case Column::speed:
|
||||
return QObject::tr("Speed");
|
||||
case Column::status:
|
||||
return QObject::tr("Status");
|
||||
case Column::control:
|
||||
return QObject::tr("Control");
|
||||
case Column::invalid:
|
||||
break;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
EditorAction toEditorAction(int in) {
|
||||
if (in < 0 || in >= static_cast<int>(EditorAction::invalid)) {
|
||||
qWarning("Unexpected editor action %d", in);
|
||||
return EditorAction::invalid;
|
||||
}
|
||||
|
||||
return static_cast<EditorAction>(in);
|
||||
}
|
||||
|
||||
Model::Model(QObject* parent)
|
||||
: QAbstractTableModel(parent)
|
||||
{}
|
||||
|
||||
QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (role != Qt::DisplayRole) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (orientation != Qt::Orientation::Horizontal) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const auto column = toFileTransferListColumn(section);
|
||||
return toQString(column);
|
||||
}
|
||||
|
||||
void Model::onFileUpdated(const ToxFile& file)
|
||||
{
|
||||
if (!shouldProcessFileKind(file.fileKind)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto idxIt = idToRow.find(file.resumeFileId);
|
||||
int rowIdx = 0;
|
||||
|
||||
if (idxIt == idToRow.end()) {
|
||||
if (files.size() >= std::numeric_limits<int>::max()) {
|
||||
// Bug waiting to happen, but also what can we do if qt just doesn't
|
||||
// support this many items in a list
|
||||
qWarning("Too many file transfers rendered, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
auto insertedIdx = files.size();
|
||||
|
||||
emit rowsAboutToBeInserted(QModelIndex(), insertedIdx, insertedIdx, {});
|
||||
|
||||
files.push_back(file);
|
||||
idToRow.insert(file.resumeFileId, insertedIdx);
|
||||
|
||||
emit rowsInserted(QModelIndex(), insertedIdx, insertedIdx, {});
|
||||
} else {
|
||||
rowIdx = idxIt.value();
|
||||
files[rowIdx] = file;
|
||||
if (fileTransferFailed(file.status)) {
|
||||
emit rowsAboutToBeRemoved(QModelIndex(), rowIdx, rowIdx, {});
|
||||
|
||||
for (auto it = idToRow.begin(); it != idToRow.end(); ++it) {
|
||||
if (it.value() > rowIdx) {
|
||||
it.value() -= 1;
|
||||
}
|
||||
}
|
||||
idToRow.remove(file.resumeFileId);
|
||||
files.erase(files.begin() + rowIdx);
|
||||
|
||||
emit rowsRemoved(QModelIndex(), rowIdx, rowIdx, {});
|
||||
}
|
||||
else {
|
||||
emit dataChanged(index(rowIdx, 0), index(rowIdx, columnCount()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int Model::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
return files.size();
|
||||
}
|
||||
|
||||
int Model::columnCount(const QModelIndex& parent) const
|
||||
{
|
||||
return static_cast<int>(Column::invalid);
|
||||
}
|
||||
|
||||
QVariant Model::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
const auto row = index.row();
|
||||
if (row < 0 || static_cast<size_t>(row) > files.size()) {
|
||||
qWarning("Invalid file transfer row %d (files: %lu)", row, files.size());
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (role == Qt::UserRole) {
|
||||
return files[row].filePath;
|
||||
}
|
||||
|
||||
if (role != Qt::DisplayRole) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const auto column = toFileTransferListColumn(index.column());
|
||||
|
||||
switch (column)
|
||||
{
|
||||
case Column::fileName:
|
||||
return files[row].fileName;
|
||||
case Column::contact:
|
||||
{
|
||||
auto f = FriendList::findFriend(FriendList::id2Key(files[row].friendId));
|
||||
if (f == nullptr) {
|
||||
qWarning("Invalid friend for file transfer");
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
return f->getDisplayedName();
|
||||
}
|
||||
case Column::progress:
|
||||
return files[row].progress.getProgress() * 100.0;
|
||||
case Column::size:
|
||||
return getHumanReadableSize(files[row].progress.getFileSize());
|
||||
case Column::speed:
|
||||
return getHumanReadableSize(files[row].progress.getSpeed()) + "/s";
|
||||
case Column::status:
|
||||
return fileStatusString(files[row]);
|
||||
case Column::control:
|
||||
return files[row].pauseStatus.localPaused();
|
||||
case Column::invalid:
|
||||
break;
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
bool Model::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
const auto column = toFileTransferListColumn(index.column());
|
||||
|
||||
if (column != Column::control) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!value.canConvert<int>()) {
|
||||
qWarning("Unexpected model data");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto action = toEditorAction(value.toInt());
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case EditorAction::cancel:
|
||||
emit cancel(files[index.row()]);
|
||||
break;
|
||||
case EditorAction::pause:
|
||||
emit togglePause(files[index.row()]);
|
||||
break;
|
||||
case EditorAction::invalid:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Delegate::Delegate(QWidget* parent)
|
||||
: QStyledItemDelegate(parent)
|
||||
{}
|
||||
|
||||
void Delegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
|
||||
{
|
||||
const auto column = toFileTransferListColumn(index.column());
|
||||
switch (column)
|
||||
{
|
||||
case Column::progress:
|
||||
{
|
||||
int progress = index.data().toInt();
|
||||
|
||||
QStyleOptionProgressBar progressBarOption;
|
||||
progressBarOption.rect = option.rect;
|
||||
progressBarOption.minimum = 0;
|
||||
progressBarOption.maximum = 100;
|
||||
progressBarOption.progress = progress;
|
||||
progressBarOption.text = QString::number(progress) + "%";
|
||||
progressBarOption.textVisible = true;
|
||||
|
||||
QApplication::style()->drawControl(QStyle::CE_ProgressBar,
|
||||
&progressBarOption, painter);
|
||||
return;
|
||||
}
|
||||
case Column::control:
|
||||
{
|
||||
const auto data = index.data();
|
||||
if (!data.canConvert<bool>()) {
|
||||
qWarning("Unexpected control type, not rendering controls");
|
||||
return;
|
||||
}
|
||||
const auto localPaused = data.toBool();
|
||||
QPixmap pausePixmap = localPaused
|
||||
? QPixmap(Style::getImagePath("fileTransferInstance/arrow_black.svg"))
|
||||
: QPixmap(Style::getImagePath("fileTransferInstance/pause_dark.svg"));
|
||||
QApplication::style()->drawItemPixmap(painter, pauseRect(option), Qt::AlignCenter, pausePixmap);
|
||||
|
||||
QPixmap stopPixmap(Style::getImagePath("fileTransferInstance/no_dark.svg"));
|
||||
QApplication::style()->drawItemPixmap(painter, stopRect(option), Qt::AlignCenter, stopPixmap);
|
||||
return;
|
||||
}
|
||||
case Column::fileName:
|
||||
case Column::contact:
|
||||
case Column::size:
|
||||
case Column::speed:
|
||||
case Column::status:
|
||||
case Column::invalid:
|
||||
break;
|
||||
}
|
||||
|
||||
QStyledItemDelegate::paint(painter, option, index);
|
||||
}
|
||||
|
||||
bool Delegate::editorEvent(QEvent* event, QAbstractItemModel* model,
|
||||
const QStyleOptionViewItem& option, const QModelIndex& index)
|
||||
{
|
||||
if (toFileTransferListColumn(index.column()) == Column::control)
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonPress) {
|
||||
auto mouseEvent = reinterpret_cast<QMouseEvent*>(event);
|
||||
const auto pos = mouseEvent->pos();
|
||||
const auto posRect = pauseRect(option);
|
||||
const auto stRect = stopRect(option);
|
||||
|
||||
if (posRect.contains(pos)) {
|
||||
model->setData(index, static_cast<int>(EditorAction::pause));
|
||||
} else if (stRect.contains(pos)) {
|
||||
model->setData(index, static_cast<int>(EditorAction::cancel));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
View::View(QAbstractItemModel* model, QWidget* parent)
|
||||
: QTableView(parent)
|
||||
{
|
||||
setModel(model);
|
||||
|
||||
// Resize to contents but stretch the file name to fill the full view
|
||||
horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
// Visually tuned until it looked ok
|
||||
horizontalHeader()->setMinimumSectionSize(75);
|
||||
horizontalHeader()->setStretchLastSection(false);
|
||||
verticalHeader()->hide();
|
||||
setShowGrid(false);
|
||||
setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
setItemDelegate(new Delegate(this));
|
||||
}
|
||||
|
||||
View::~View() = default;
|
||||
|
||||
} // namespace FileTransferList
|
||||
|
||||
FilesForm::FilesForm(CoreFile& coreFile)
|
||||
: QObject()
|
||||
, doneIcon(Style::getImagePath("fileTransferWidget/fileDone.svg"))
|
||||
{
|
||||
head = new QWidget();
|
||||
QFont bold;
|
||||
@ -36,14 +430,34 @@ FilesForm::FilesForm()
|
||||
head->setLayout(&headLayout);
|
||||
headLayout.addWidget(&headLabel);
|
||||
|
||||
recvd = new QListWidget;
|
||||
sent = new QListWidget;
|
||||
recvdModel = new FileTransferList::Model(this);
|
||||
sentModel = new FileTransferList::Model(this);
|
||||
|
||||
auto pauseFile = [&coreFile] (ToxFile file) {
|
||||
coreFile.pauseResumeFile(file.friendId, file.fileNum);
|
||||
};
|
||||
|
||||
auto cancelFileRecv = [&coreFile] (ToxFile file) {
|
||||
coreFile.cancelFileRecv(file.friendId, file.fileNum);
|
||||
};
|
||||
|
||||
auto cancelFileSend = [&coreFile] (ToxFile file) {
|
||||
coreFile.cancelFileSend(file.friendId, file.fileNum);
|
||||
};
|
||||
|
||||
connect(recvdModel, &FileTransferList::Model::togglePause, pauseFile);
|
||||
connect(recvdModel, &FileTransferList::Model::cancel, cancelFileRecv);
|
||||
connect(sentModel, &FileTransferList::Model::togglePause, pauseFile);
|
||||
connect(sentModel, &FileTransferList::Model::cancel, cancelFileSend);
|
||||
|
||||
recvd = new FileTransferList::View(recvdModel);
|
||||
sent = new FileTransferList::View(sentModel);
|
||||
|
||||
main.addTab(recvd, QString());
|
||||
main.addTab(sent, QString());
|
||||
|
||||
connect(sent, &QListWidget::itemActivated, this, &FilesForm::onFileActivated);
|
||||
connect(recvd, &QListWidget::itemActivated, this, &FilesForm::onFileActivated);
|
||||
connect(sent, &QTableView::activated, this, &FilesForm::onSentFileActivated);
|
||||
connect(recvd, &QTableView::activated, this, &FilesForm::onReceivedFileActivated);
|
||||
|
||||
retranslateUi();
|
||||
Translator::registerHandler(std::bind(&FilesForm::retranslateUi, this), this);
|
||||
@ -75,28 +489,33 @@ void FilesForm::show(ContentLayout* contentLayout)
|
||||
head->show();
|
||||
}
|
||||
|
||||
void FilesForm::onFileDownloadComplete(const QString& path)
|
||||
void FilesForm::onFileUpdated(const ToxFile& inFile)
|
||||
{
|
||||
QListWidgetItem* tmp = new QListWidgetItem(doneIcon, QFileInfo(path).fileName());
|
||||
tmp->setData(Qt::UserRole, path);
|
||||
recvd->addItem(tmp);
|
||||
if (!shouldProcessFileKind(inFile.fileKind)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inFile.direction == ToxFile::SENDING) {
|
||||
sentModel->onFileUpdated(inFile);
|
||||
}
|
||||
else if (inFile.direction == ToxFile::RECEIVING) {
|
||||
recvdModel->onFileUpdated(inFile);
|
||||
}
|
||||
else {
|
||||
qWarning("Unexpected file direction");
|
||||
}
|
||||
}
|
||||
|
||||
void FilesForm::onFileUploadComplete(const QString& path)
|
||||
void FilesForm::onSentFileActivated(const QModelIndex& index)
|
||||
{
|
||||
QListWidgetItem* tmp = new QListWidgetItem(doneIcon, QFileInfo(path).fileName());
|
||||
tmp->setData(Qt::UserRole, path);
|
||||
sent->addItem(tmp);
|
||||
const auto& filePath = sentModel->data(index, Qt::UserRole).toString();
|
||||
Widget::confirmExecutableOpen(filePath);
|
||||
}
|
||||
|
||||
// sadly, the ToxFile struct in core only has the file name, not the file path...
|
||||
// so currently, these don't work as intended (though for now, downloads might work
|
||||
// whenever they're not saved anywhere custom, thanks to the hack)
|
||||
// I could do some digging around, but for now I'm tired and others already
|
||||
// might know it without me needing to dig, so...
|
||||
void FilesForm::onFileActivated(QListWidgetItem* item)
|
||||
void FilesForm::onReceivedFileActivated(const QModelIndex& index)
|
||||
{
|
||||
Widget::confirmExecutableOpen(QFileInfo(item->data(Qt::UserRole).toString()));
|
||||
const auto& filePath = recvdModel->data(index, Qt::UserRole).toString();
|
||||
Widget::confirmExecutableOpen(filePath);
|
||||
}
|
||||
|
||||
void FilesForm::retranslateUi()
|
||||
|
@ -19,41 +19,121 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "src/core/toxfile.h"
|
||||
#include "src/core/corefile.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QListWidgetItem>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QAbstractTableModel>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTableView>
|
||||
|
||||
class ContentLayout;
|
||||
class QListWidget;
|
||||
class QTableView;
|
||||
|
||||
namespace FileTransferList
|
||||
{
|
||||
|
||||
enum class Column : int {
|
||||
// NOTE: Order defines order in UI
|
||||
fileName,
|
||||
contact,
|
||||
progress,
|
||||
size,
|
||||
speed,
|
||||
status,
|
||||
control,
|
||||
invalid
|
||||
};
|
||||
|
||||
Column toFileTransferListColumn(int in);
|
||||
QString toQString(Column column);
|
||||
|
||||
enum class EditorAction : int {
|
||||
pause,
|
||||
cancel,
|
||||
invalid,
|
||||
};
|
||||
|
||||
EditorAction toEditorAction(int in);
|
||||
|
||||
class Model : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Model(QObject* parent = nullptr);
|
||||
~Model() = default;
|
||||
|
||||
void onFileUpdated(const ToxFile& file);
|
||||
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
||||
|
||||
signals:
|
||||
void togglePause(ToxFile file);
|
||||
void cancel(ToxFile file);
|
||||
|
||||
private:
|
||||
QHash<QByteArray /*file id*/, int /*row index*/> idToRow;
|
||||
std::vector<ToxFile> files;
|
||||
};
|
||||
|
||||
class Delegate : public QStyledItemDelegate
|
||||
{
|
||||
public:
|
||||
Delegate(QWidget* parent = nullptr);
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
|
||||
|
||||
bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) override;
|
||||
};
|
||||
|
||||
class View : public QTableView
|
||||
{
|
||||
public:
|
||||
View(QAbstractItemModel* model, QWidget* parent = nullptr);
|
||||
~View();
|
||||
|
||||
};
|
||||
} // namespace FileTransferList
|
||||
|
||||
class FilesForm : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FilesForm();
|
||||
FilesForm(CoreFile& coreFile);
|
||||
~FilesForm();
|
||||
|
||||
bool isShown() const;
|
||||
void show(ContentLayout* contentLayout);
|
||||
|
||||
public slots:
|
||||
void onFileDownloadComplete(const QString& path);
|
||||
void onFileUploadComplete(const QString& path);
|
||||
void onFileUpdated(const ToxFile& file);
|
||||
|
||||
private slots:
|
||||
void onFileActivated(QListWidgetItem* item);
|
||||
void onSentFileActivated(const QModelIndex& item);
|
||||
void onReceivedFileActivated(const QModelIndex& item);
|
||||
|
||||
private:
|
||||
struct FileInfo
|
||||
{
|
||||
QListWidgetItem* item = nullptr;
|
||||
ToxFile file;
|
||||
};
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
private:
|
||||
QWidget* head;
|
||||
QIcon doneIcon;
|
||||
QLabel headLabel;
|
||||
QVBoxLayout headLayout;
|
||||
QTabWidget main;
|
||||
QListWidget *sent, *recvd;
|
||||
QTableView *sent, *recvd;
|
||||
FileTransferList::Model *sentModel, *recvdModel;
|
||||
};
|
||||
|
@ -279,7 +279,8 @@ void Widget::init()
|
||||
|
||||
Style::setThemeColor(settings.getThemeColor());
|
||||
|
||||
filesForm = new FilesForm();
|
||||
CoreFile* coreFile = core->getCoreFile();
|
||||
filesForm = new FilesForm(*coreFile);
|
||||
addFriendForm = new AddFriendForm(core->getSelfId());
|
||||
groupInviteForm = new GroupInviteForm;
|
||||
|
||||
@ -292,7 +293,6 @@ void Widget::init()
|
||||
updateCheck->checkForUpdate();
|
||||
#endif
|
||||
|
||||
CoreFile* coreFile = core->getCoreFile();
|
||||
profileInfo = new ProfileInfo(core, &profile);
|
||||
profileForm = new ProfileForm(profileInfo);
|
||||
|
||||
@ -307,8 +307,6 @@ void Widget::init()
|
||||
connect(&profile, &Profile::selfAvatarChanged, profileForm, &ProfileForm::onSelfAvatarLoaded);
|
||||
|
||||
connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::onFileReceiveRequested);
|
||||
connect(coreFile, &CoreFile::fileDownloadFinished, filesForm, &FilesForm::onFileDownloadComplete);
|
||||
connect(coreFile, &CoreFile::fileUploadFinished, filesForm, &FilesForm::onFileUploadComplete);
|
||||
connect(ui->addButton, &QPushButton::clicked, this, &Widget::onAddClicked);
|
||||
connect(ui->groupButton, &QPushButton::clicked, this, &Widget::onGroupClicked);
|
||||
connect(ui->transferButton, &QPushButton::clicked, this, &Widget::onTransferClicked);
|
||||
@ -327,16 +325,37 @@ void Widget::init()
|
||||
connect(filterDisplayGroup, &QActionGroup::triggered, this, &Widget::changeDisplayMode);
|
||||
connect(ui->friendList, &QWidget::customContextMenuRequested, this, &Widget::friendListContextMenu);
|
||||
|
||||
connect(coreFile, &CoreFile::fileSendStarted, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferAccepted, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferCancelled, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferFinished, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferPaused, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferInfo, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferRemotePausedUnpaused, this, &Widget::dispatchFileWithBool);
|
||||
connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this, &Widget::dispatchFileWithBool);
|
||||
connect(coreFile, &CoreFile::fileSendFailed, this, &Widget::dispatchFileSendFailed);
|
||||
// NOTE: Order of these signals as well as the use of QueuedConnection is important!
|
||||
// Qt::AutoConnection, signals emitted from the same thread as Widget will
|
||||
// be serviced before other signals. This is a problem when we have tight
|
||||
// calls between file control and file info callbacks.
|
||||
//
|
||||
// File info callbacks are called from the core thread and will use
|
||||
// QueuedConnection by default, our control path can easily end up on the
|
||||
// same thread as widget. This can result in the following behavior if we
|
||||
// are not careful
|
||||
//
|
||||
// * File data is received
|
||||
// * User presses pause at the same time
|
||||
// * Pause waits for data receive callback to complete (and emit fileTransferInfo)
|
||||
// * Pause is executed and emits fileTransferPaused
|
||||
// * Pause signal is handled by Qt::DirectConnection
|
||||
// * fileTransferInfo signal is handled after by Qt::QueuedConnection
|
||||
//
|
||||
// This results in stale file state! In these conditions if we are not
|
||||
// careful toxcore will think we are paused but our UI will think we are
|
||||
// resumed, because the last signal they got was a transmitting file info
|
||||
// signal!
|
||||
connect(coreFile, &CoreFile::fileTransferInfo, this, &Widget::dispatchFile, Qt::QueuedConnection);
|
||||
connect(coreFile, &CoreFile::fileSendStarted, this, &Widget::dispatchFile, Qt::QueuedConnection);
|
||||
connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::dispatchFile, Qt::QueuedConnection);
|
||||
connect(coreFile, &CoreFile::fileTransferAccepted, this, &Widget::dispatchFile, Qt::QueuedConnection);
|
||||
connect(coreFile, &CoreFile::fileTransferCancelled, this, &Widget::dispatchFile, Qt::QueuedConnection);
|
||||
connect(coreFile, &CoreFile::fileTransferFinished, this, &Widget::dispatchFile, Qt::QueuedConnection);
|
||||
connect(coreFile, &CoreFile::fileTransferPaused, this, &Widget::dispatchFile, Qt::QueuedConnection);
|
||||
connect(coreFile, &CoreFile::fileTransferRemotePausedUnpaused, this, &Widget::dispatchFileWithBool, Qt::QueuedConnection);
|
||||
connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this, &Widget::dispatchFileWithBool, Qt::QueuedConnection);
|
||||
connect(coreFile, &CoreFile::fileSendFailed, this, &Widget::dispatchFileSendFailed, Qt::QueuedConnection);
|
||||
// NOTE: We intentionally do not connect the fileUploadFinished and fileDownloadFinished signals
|
||||
// because they are duplicates of fileTransferFinished NOTE: We don't hook up the
|
||||
// fileNameChanged signal since it is only emitted before a fileReceiveRequest. We get the
|
||||
@ -1105,7 +1124,8 @@ void Widget::dispatchFile(ToxFile file)
|
||||
}
|
||||
|
||||
auto maxAutoAcceptSize = settings.getMaxAutoAcceptSize();
|
||||
bool autoAcceptSizeCheckPassed = maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.filesize;
|
||||
bool autoAcceptSizeCheckPassed =
|
||||
maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.progress.getFileSize();
|
||||
|
||||
if (!autoAcceptDir.isEmpty() && autoAcceptSizeCheckPassed) {
|
||||
acceptFileTransfer(file, autoAcceptDir);
|
||||
@ -1114,6 +1134,8 @@ void Widget::dispatchFile(ToxFile file)
|
||||
|
||||
const auto senderPk = (file.direction == ToxFile::SENDING) ? core->getSelfPublicKey() : pk;
|
||||
friendChatLogs[pk]->onFileUpdated(senderPk, file);
|
||||
|
||||
filesForm->onFileUpdated(file);
|
||||
}
|
||||
|
||||
void Widget::dispatchFileWithBool(ToxFile file, bool)
|
||||
@ -1707,9 +1729,7 @@ void Widget::onFriendRequestReceived(const ToxPk& friendPk, const QString& messa
|
||||
void Widget::onFileReceiveRequested(const ToxFile& file)
|
||||
{
|
||||
const ToxPk& friendPk = FriendList::id2Key(file.friendId);
|
||||
newFriendMessageAlert(friendPk,
|
||||
{},
|
||||
true, file.fileName, file.filesize);
|
||||
newFriendMessageAlert(friendPk, {}, true, file.fileName, file.progress.getFileSize());
|
||||
}
|
||||
|
||||
void Widget::updateFriendActivity(const Friend& frnd)
|
||||
|
341
test/core/fileprogress_test.cpp
Normal file
341
test/core/fileprogress_test.cpp
Normal file
@ -0,0 +1,341 @@
|
||||
/*
|
||||
Copyright © 2021 by The qTox Project Contributors
|
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox.
|
||||
|
||||
qTox is libre 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 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
qTox is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "src/core/toxfileprogress.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <limits>
|
||||
|
||||
class TestFileProgress : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void testSpeed();
|
||||
void testSpeedReset();
|
||||
void testDiscardedSample();
|
||||
void testProgress();
|
||||
void testRemainingTime();
|
||||
void testBytesSentPersistence();
|
||||
void testFileSizePersistence();
|
||||
void testNoSamples();
|
||||
void testSpeedUnevenIntervals();
|
||||
void testDefaultTimeLessThanNow();
|
||||
void testTimeChange();
|
||||
void testFinishedSpeed();
|
||||
void testSamplePeriod();
|
||||
void testInvalidSamplePeriod();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test that our speeds are sane while we're on our first few samples
|
||||
*/
|
||||
void TestFileProgress::testSpeed()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 1000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(0, nextSampleTime));
|
||||
// 1 sample has no speed
|
||||
QCOMPARE(progress.getSpeed(), 0.0);
|
||||
|
||||
// Swap buffers. Time should be valid
|
||||
nextSampleTime = nextSampleTime.addMSecs(500);
|
||||
// 10 bytes over 0.5s
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
QCOMPARE(progress.getSpeed(), 20.0);
|
||||
|
||||
// This should evict the first sample, so our time should be relative to the
|
||||
// first 10 bytes
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(20, nextSampleTime));
|
||||
// 10 bytes over 1s
|
||||
QCOMPARE(progress.getSpeed(), 10.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that resetting our speed puts us back into a sane default state
|
||||
*/
|
||||
void TestFileProgress::testSpeedReset()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 1000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(0, nextSampleTime));
|
||||
|
||||
// Push enough samples that all samples are initialized
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(20, nextSampleTime));
|
||||
|
||||
QCOMPARE(progress.getSpeed(), 10.0);
|
||||
|
||||
progress.resetSpeed();
|
||||
QCOMPARE(progress.getSpeed(), 0.0);
|
||||
QCOMPARE(progress.lastSampleTime(), QTime());
|
||||
QCOMPARE(progress.getBytesSent(), uint64_t(20));
|
||||
QCOMPARE(progress.getProgress(), 0.2);
|
||||
|
||||
// Ensure that pushing new samples after reset works correectly
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(30, nextSampleTime));
|
||||
|
||||
// 1 sample has no speed
|
||||
QCOMPARE(progress.getSpeed(), 0.0);
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(40, nextSampleTime));
|
||||
QCOMPARE(progress.getSpeed(), 10.0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Test that invalid samples are discarded
|
||||
*/
|
||||
void TestFileProgress::testDiscardedSample()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 1000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(0, nextSampleTime));
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(20, nextSampleTime));
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
|
||||
// Sample should be discarded because it's too large
|
||||
QVERIFY(!progress.addSample(300, nextSampleTime));
|
||||
QCOMPARE(progress.lastSampleTime(), QTime(1, 0, 1));
|
||||
|
||||
// Sample should be discarded because we're going backwards
|
||||
QVERIFY(!progress.addSample(10, nextSampleTime));
|
||||
QCOMPARE(progress.lastSampleTime(), QTime(1, 0, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that progress is reported correctly
|
||||
*/
|
||||
void TestFileProgress::testProgress()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 4000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(0, nextSampleTime));
|
||||
QCOMPARE(progress.getProgress(), 0.0);
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
QCOMPARE(progress.getProgress(), 0.1);
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(100, nextSampleTime));
|
||||
QCOMPARE(progress.getProgress(), 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that remaining time is predicted reasonably
|
||||
*/
|
||||
void TestFileProgress::testRemainingTime()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 2000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(0, nextSampleTime));
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
|
||||
// 10% over 1s, 90% should take 9 more seconds
|
||||
QCOMPARE(progress.getTimeLeftSeconds(), 9.0);
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(10000);
|
||||
QVERIFY(progress.addSample(100, nextSampleTime));
|
||||
// Even with a slow final sample, we should have 0 seconds remaining when we
|
||||
// are complete
|
||||
QCOMPARE(progress.getTimeLeftSeconds(), 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that the sent bytes keeps the last sample
|
||||
*/
|
||||
void TestFileProgress::testBytesSentPersistence()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 1000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
|
||||
// First sample
|
||||
QCOMPARE(progress.getBytesSent(), uint64_t(10));
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(20, nextSampleTime));
|
||||
// Second sample
|
||||
QCOMPARE(progress.getBytesSent(), uint64_t(20));
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(30, nextSampleTime));
|
||||
// After rollover
|
||||
QCOMPARE(progress.getBytesSent(), uint64_t(30));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check that the reported file size matches what was given
|
||||
*/
|
||||
void TestFileProgress::testFileSizePersistence()
|
||||
{
|
||||
auto progress = ToxFileProgress(33, 1000);
|
||||
QCOMPARE(progress.getFileSize(), uint64_t(33));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that we have sane stats when no samples have been added
|
||||
*/
|
||||
void TestFileProgress::testNoSamples()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 1000);
|
||||
QCOMPARE(progress.getSpeed(), 0.0);
|
||||
QVERIFY(progress.getTimeLeftSeconds() == std::numeric_limits<double>::infinity());
|
||||
QCOMPARE(progress.getProgress(), 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that statistics are being average over the entire range of time
|
||||
* no matter the sample frequency
|
||||
*/
|
||||
void TestFileProgress::testSpeedUnevenIntervals()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 4000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(20, nextSampleTime));
|
||||
nextSampleTime = nextSampleTime.addMSecs(3000);
|
||||
QVERIFY(progress.addSample(50, nextSampleTime));
|
||||
|
||||
// 10->50 over 4 seconds
|
||||
QCOMPARE(progress.getSpeed(), 10.0);
|
||||
}
|
||||
|
||||
void TestFileProgress::testDefaultTimeLessThanNow()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 1000);
|
||||
QVERIFY(progress.lastSampleTime() < QTime::currentTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that changing the time resets the speed count. Note that it would
|
||||
* be better to use the monotonic clock, but it's not trivial to get the
|
||||
* monotonic clock from Qt's time API
|
||||
*/
|
||||
void TestFileProgress::testTimeChange()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 1000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
|
||||
nextSampleTime = QTime(0, 0, 0);
|
||||
QVERIFY(progress.addSample(20, nextSampleTime));
|
||||
|
||||
QCOMPARE(progress.getSpeed(), 0.0);
|
||||
QCOMPARE(progress.getProgress(), 0.2);
|
||||
|
||||
nextSampleTime = QTime(0, 0, 1);
|
||||
QVERIFY(progress.addSample(30, nextSampleTime));
|
||||
QCOMPARE(progress.getSpeed(), 10.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that when a file is complete it's speed is set to 0
|
||||
*/
|
||||
|
||||
void TestFileProgress::testFinishedSpeed()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, 1000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
|
||||
nextSampleTime = nextSampleTime.addMSecs(1000);
|
||||
QVERIFY(progress.addSample(100, nextSampleTime));
|
||||
QCOMPARE(progress.getSpeed(), 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that we are averaged over the past period * samples time, and
|
||||
* when we roll we lose one sample period of data
|
||||
*/
|
||||
void TestFileProgress::testSamplePeriod()
|
||||
{
|
||||
// No matter the number of samples, we should always be averaging over 2s
|
||||
auto progress = ToxFileProgress(100, 2000);
|
||||
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(0, nextSampleTime));
|
||||
|
||||
nextSampleTime = QTime(1, 0, 0, 500);
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
|
||||
// Even with less than a sample period our speed and size should be updated
|
||||
QCOMPARE(progress.getSpeed(), 20.0);
|
||||
QCOMPARE(progress.getBytesSent(), uint64_t(10));
|
||||
|
||||
// Add a new sample at 1s, this should replace the previous sample
|
||||
nextSampleTime = QTime(1, 0, 1);
|
||||
QVERIFY(progress.addSample(30, nextSampleTime));
|
||||
QCOMPARE(progress.getSpeed(), 30.0);
|
||||
QCOMPARE(progress.getBytesSent(), uint64_t(30));
|
||||
|
||||
// Add a new sample at 2s, our time should still be relative to 0
|
||||
nextSampleTime = QTime(1, 0, 2);
|
||||
QVERIFY(progress.addSample(50, nextSampleTime));
|
||||
// 50 - 0 over 2s
|
||||
QCOMPARE(progress.getSpeed(), 25.0);
|
||||
QCOMPARE(progress.getBytesSent(), uint64_t(50));
|
||||
}
|
||||
|
||||
void TestFileProgress::testInvalidSamplePeriod()
|
||||
{
|
||||
auto progress = ToxFileProgress(100, -1);
|
||||
|
||||
// Sample period should be healed to 1000
|
||||
auto nextSampleTime = QTime(1, 0, 0);
|
||||
QVERIFY(progress.addSample(0, nextSampleTime));
|
||||
|
||||
nextSampleTime = QTime(1, 0, 0, 500);
|
||||
QVERIFY(progress.addSample(10, nextSampleTime));
|
||||
QCOMPARE(progress.getSpeed(), 20.0);
|
||||
|
||||
// Second sample should be removed and we should average over the full
|
||||
// second
|
||||
nextSampleTime = QTime(1, 0, 1);
|
||||
QVERIFY(progress.addSample(30, nextSampleTime));
|
||||
QCOMPARE(progress.getSpeed(), 30.0);
|
||||
|
||||
// First sample should be evicted and we should have an updated speed
|
||||
nextSampleTime = QTime(1, 0, 2);
|
||||
QVERIFY(progress.addSample(90, nextSampleTime));
|
||||
QCOMPARE(progress.getSpeed(), 60.0);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestFileProgress)
|
||||
#include "fileprogress_test.moc"
|
282
test/widget/filesform_test.cpp
Normal file
282
test/widget/filesform_test.cpp
Normal file
@ -0,0 +1,282 @@
|
||||
/*
|
||||
Copyright © 2021 by The qTox Project Contributors
|
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox.
|
||||
|
||||
qTox is libre 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 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
qTox is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "src/widget/form/filesform.h"
|
||||
#include "src/friendlist.h"
|
||||
#include "src/model/friend.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <limits>
|
||||
|
||||
class TestFileTransferList : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
|
||||
void testFileTransferListConversion();
|
||||
void testEditorActionConversion();
|
||||
|
||||
void testFileName();
|
||||
// NOTE: Testing contact return requires a lookup in FriendList which goes
|
||||
// down a large dependency chain that is not linked to this test
|
||||
// void testContact();
|
||||
void testProgress();
|
||||
void testSize();
|
||||
void testSpeed();
|
||||
void testStatus();
|
||||
void testControl();
|
||||
void testAvatarIgnored();
|
||||
void testMultipleFiles();
|
||||
void testFileRemoval();
|
||||
};
|
||||
|
||||
using namespace FileTransferList;
|
||||
|
||||
void TestFileTransferList::testFileTransferListConversion()
|
||||
{
|
||||
Model model;
|
||||
for (int i = 0; i < model.columnCount(); ++i) {
|
||||
QVERIFY(toFileTransferListColumn(i) != Column::invalid);
|
||||
}
|
||||
QCOMPARE(toFileTransferListColumn(100), Column::invalid);
|
||||
}
|
||||
|
||||
void TestFileTransferList::testEditorActionConversion()
|
||||
{
|
||||
QCOMPARE(toEditorAction(static_cast<int>(EditorAction::pause)), EditorAction::pause);
|
||||
QCOMPARE(toEditorAction(static_cast<int>(EditorAction::cancel)), EditorAction::cancel);
|
||||
QCOMPARE(toEditorAction(55), EditorAction::invalid);
|
||||
}
|
||||
|
||||
void TestFileTransferList::testFileName()
|
||||
{
|
||||
Model model;
|
||||
|
||||
ToxFile file;
|
||||
file.fileKind = TOX_FILE_KIND_DATA;
|
||||
file.fileName = "Test";
|
||||
model.onFileUpdated(file);
|
||||
|
||||
const auto idx = model.index(0, static_cast<int>(Column::fileName));
|
||||
const auto fileName = idx.data();
|
||||
|
||||
QCOMPARE(fileName.toString(), QString("Test"));
|
||||
}
|
||||
|
||||
void TestFileTransferList::testProgress()
|
||||
{
|
||||
Model model;
|
||||
|
||||
ToxFile file(0, 0, "", "", 1000, ToxFile::FileDirection::SENDING);
|
||||
file.progress.addSample(100, QTime(1, 0, 0));
|
||||
model.onFileUpdated(file);
|
||||
|
||||
const auto idx = model.index(0, static_cast<int>(Column::progress));
|
||||
const auto progress = idx.data();
|
||||
|
||||
// Progress should be in percent units, 100/1000 == 10
|
||||
QCOMPARE(progress.toFloat(), 10.0f);
|
||||
}
|
||||
|
||||
void TestFileTransferList::testSize()
|
||||
{
|
||||
Model model;
|
||||
|
||||
ToxFile file(0, 0, "", "", 1000, ToxFile::FileDirection::SENDING);
|
||||
model.onFileUpdated(file);
|
||||
|
||||
const auto idx = model.index(0, static_cast<int>(Column::size));
|
||||
auto size = idx.data();
|
||||
|
||||
// Size should be a human readable string
|
||||
QCOMPARE(size.toString(), QString("1000B"));
|
||||
|
||||
// 1GB + a little to avoid floating point inaccuracy
|
||||
file = ToxFile(0, 0, "", "", 1024 * 1024 * 1024 + 2, ToxFile::FileDirection::SENDING);
|
||||
model.onFileUpdated(file);
|
||||
size = idx.data();
|
||||
QCOMPARE(size.toString(), QString("1.00GiB"));
|
||||
}
|
||||
|
||||
void TestFileTransferList::testSpeed()
|
||||
{
|
||||
Model model;
|
||||
|
||||
ToxFile file(0, 0, "", "", 1024 * 1024, ToxFile::FileDirection::SENDING);
|
||||
file.progress.addSample(100 * 1024, QTime(1, 0, 0));
|
||||
file.progress.addSample(200 * 1024, QTime(1, 0, 1));
|
||||
model.onFileUpdated(file);
|
||||
|
||||
const auto idx = model.index(0, static_cast<int>(Column::speed));
|
||||
const auto speed = idx.data();
|
||||
|
||||
// Speed should be a human readable string
|
||||
QCOMPARE(speed.toString(), QString("100KiB/s"));
|
||||
}
|
||||
|
||||
void TestFileTransferList::testStatus()
|
||||
{
|
||||
Model model;
|
||||
|
||||
ToxFile file(0, 0, "", "", 1024 * 1024, ToxFile::FileDirection::SENDING);
|
||||
file.status = ToxFile::TRANSMITTING;
|
||||
model.onFileUpdated(file);
|
||||
|
||||
const auto idx = model.index(0, static_cast<int>(Column::status));
|
||||
auto status = idx.data();
|
||||
|
||||
QCOMPARE(status.toString(), QString("Transmitting"));
|
||||
|
||||
file.status = ToxFile::PAUSED;
|
||||
file.pauseStatus.remotePause();
|
||||
model.onFileUpdated(file);
|
||||
status = idx.data();
|
||||
|
||||
QCOMPARE(status.toString(), QString("Remote paused"));
|
||||
|
||||
file.status = ToxFile::PAUSED;
|
||||
file.pauseStatus.localPause();
|
||||
file.pauseStatus.remoteResume();
|
||||
model.onFileUpdated(file);
|
||||
status = idx.data();
|
||||
|
||||
QCOMPARE(status.toString(), QString("Paused"));
|
||||
}
|
||||
|
||||
void TestFileTransferList::testControl()
|
||||
{
|
||||
Model model;
|
||||
bool cancelCalled = false;
|
||||
bool pauseCalled = false;
|
||||
|
||||
QObject::connect(&model, &Model::cancel, [&] (ToxFile file) {
|
||||
cancelCalled = true;
|
||||
});
|
||||
|
||||
QObject::connect(&model, &Model::togglePause, [&] (ToxFile file) {
|
||||
pauseCalled = true;
|
||||
});
|
||||
|
||||
ToxFile file(0, 0, "", "", 1024 * 1024, ToxFile::FileDirection::SENDING);
|
||||
file.status = ToxFile::TRANSMITTING;
|
||||
model.onFileUpdated(file);
|
||||
|
||||
const auto idx = model.index(0, static_cast<int>(Column::control));
|
||||
model.setData(idx, static_cast<int>(EditorAction::pause));
|
||||
|
||||
QVERIFY(pauseCalled);
|
||||
QVERIFY(!cancelCalled);
|
||||
|
||||
pauseCalled = false;
|
||||
model.setData(idx, static_cast<int>(EditorAction::cancel));
|
||||
QVERIFY(!pauseCalled);
|
||||
QVERIFY(cancelCalled);
|
||||
|
||||
file.status = ToxFile::TRANSMITTING;
|
||||
model.onFileUpdated(file);
|
||||
// True if paused
|
||||
QCOMPARE(idx.data().toBool(), false);
|
||||
|
||||
file.status = ToxFile::PAUSED;
|
||||
file.pauseStatus.localPause();
|
||||
model.onFileUpdated(file);
|
||||
// True if _local_ paused
|
||||
QCOMPARE(idx.data().toBool(), true);
|
||||
}
|
||||
|
||||
void TestFileTransferList::testAvatarIgnored()
|
||||
{
|
||||
Model model;
|
||||
|
||||
ToxFile file;
|
||||
file.fileKind = TOX_FILE_KIND_AVATAR;
|
||||
model.onFileUpdated(file);
|
||||
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
}
|
||||
|
||||
void TestFileTransferList::testMultipleFiles()
|
||||
{
|
||||
Model model;
|
||||
|
||||
ToxFile file;
|
||||
file.resumeFileId = QByteArray();
|
||||
file.fileKind = TOX_FILE_KIND_DATA;
|
||||
file.status = ToxFile::TRANSMITTING;
|
||||
file.fileName = "a";
|
||||
model.onFileUpdated(file);
|
||||
|
||||
// File map keys off resume file ID
|
||||
file.resumeFileId = QByteArray("asdfasdf");
|
||||
file.fileName = "b";
|
||||
model.onFileUpdated(file);
|
||||
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
|
||||
auto idx = model.index(0, static_cast<int>(Column::fileName));
|
||||
QCOMPARE(idx.data().toString(), QString("a"));
|
||||
|
||||
idx = model.index(1, static_cast<int>(Column::fileName));
|
||||
QCOMPARE(idx.data().toString(), QString("b"));
|
||||
|
||||
// File name should be updated instead of inserting a new file since the
|
||||
// resume file ID is the same
|
||||
file.fileName = "c";
|
||||
model.onFileUpdated(file);
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
QCOMPARE(idx.data().toString(), QString("c"));
|
||||
}
|
||||
|
||||
void TestFileTransferList::testFileRemoval()
|
||||
{
|
||||
// Model should keep files in the list if they are finished, but not if they
|
||||
// were broken or canceled
|
||||
|
||||
Model model;
|
||||
|
||||
ToxFile file;
|
||||
file.fileKind = TOX_FILE_KIND_DATA;
|
||||
file.status = ToxFile::TRANSMITTING;
|
||||
model.onFileUpdated(file);
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
|
||||
file.status = ToxFile::BROKEN;
|
||||
model.onFileUpdated(file);
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
|
||||
file.status = ToxFile::TRANSMITTING;
|
||||
model.onFileUpdated(file);
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
|
||||
file.status = ToxFile::CANCELED;
|
||||
model.onFileUpdated(file);
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
|
||||
file.status = ToxFile::TRANSMITTING;
|
||||
model.onFileUpdated(file);
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
|
||||
file.status = ToxFile::FINISHED;
|
||||
model.onFileUpdated(file);
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestFileTransferList)
|
||||
#include "filesform_test.moc"
|
44
themes/dark/fileTransferInstance/arrow_black.svg
Normal file
44
themes/dark/fileTransferInstance/arrow_black.svg
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
id="Layer_1"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="arrow_black.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="69.416667"
|
||||
inkscape:cx="6"
|
||||
inkscape:cy="5.9927971"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><metadata
|
||||
id="metadata9"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs7" /><polygon
|
||||
points="3.572,6.187 0,0 7.145,0 "
|
||||
transform="matrix(0,-1.3815919,1.3815919,0,1.7260455,10.935737)"
|
||||
id="polygon3"
|
||||
style="fill:#000000" /></svg>
|
After Width: | Height: | Size: 1.6 KiB |
47
themes/dark/fileTransferInstance/no_dark.svg
Normal file
47
themes/dark/fileTransferInstance/no_dark.svg
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 11.999999 12"
|
||||
id="Layer_1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
sodipodi:docname="no_dark.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="27.812867"
|
||||
inkscape:cx="7.1369845"
|
||||
inkscape:cy="6.2201426"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:pagecheckerboard="0" /><metadata
|
||||
id="metadata9"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs7" /><polygon
|
||||
points="1.693,0.001 0,1.693 3.35,5.043 0,8.394 1.693,10.086 5.043,6.736 8.395,10.087 10.086,8.394 6.734,5.043 10.086,1.692 8.395,0 5.043,3.351 "
|
||||
transform="matrix(1.1123133,0,0,1.1054869,0.39060355,0.42447682)"
|
||||
id="polygon3"
|
||||
style="fill:#000000" /></svg>
|
After Width: | Height: | Size: 1.8 KiB |
58
themes/dark/fileTransferInstance/pause_dark.svg
Normal file
58
themes/dark/fileTransferInstance/pause_dark.svg
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12.000001 12"
|
||||
id="Layer_1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
sodipodi:docname="pause_dark.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="39.333333"
|
||||
inkscape:cx="0.69915255"
|
||||
inkscape:cy="4.3220339"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:pagecheckerboard="0" /><metadata
|
||||
id="metadata13"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs11" /><g
|
||||
transform="matrix(0.91525423,0,0,0.91525423,2.1550175,0.69830581)"
|
||||
id="g3"
|
||||
style="fill:#000000"><rect
|
||||
width="2.1851854"
|
||||
height="10.925926"
|
||||
x="-0.16937082"
|
||||
y="0.32962826"
|
||||
id="rect5"
|
||||
style="fill:#000000" /><rect
|
||||
width="2.1851854"
|
||||
height="10.925926"
|
||||
x="6.3861852"
|
||||
y="0.32962897"
|
||||
id="rect7"
|
||||
style="fill:#000000" /></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
44
themes/default/fileTransferInstance/arrow_black.svg
Normal file
44
themes/default/fileTransferInstance/arrow_black.svg
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
id="Layer_1"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="arrow_black.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="69.416667"
|
||||
inkscape:cx="6"
|
||||
inkscape:cy="5.9927971"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><metadata
|
||||
id="metadata9"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs7" /><polygon
|
||||
points="3.572,6.187 0,0 7.145,0 "
|
||||
transform="matrix(0,-1.3815919,1.3815919,0,1.7260455,10.935737)"
|
||||
id="polygon3"
|
||||
style="fill:#000000" /></svg>
|
After Width: | Height: | Size: 1.6 KiB |
47
themes/default/fileTransferInstance/no_dark.svg
Normal file
47
themes/default/fileTransferInstance/no_dark.svg
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 11.999999 12"
|
||||
id="Layer_1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
sodipodi:docname="no_dark.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="27.812867"
|
||||
inkscape:cx="7.1369845"
|
||||
inkscape:cy="6.2201426"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:pagecheckerboard="0" /><metadata
|
||||
id="metadata9"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs7" /><polygon
|
||||
points="1.693,0.001 0,1.693 3.35,5.043 0,8.394 1.693,10.086 5.043,6.736 8.395,10.087 10.086,8.394 6.734,5.043 10.086,1.692 8.395,0 5.043,3.351 "
|
||||
transform="matrix(1.1123133,0,0,1.1054869,0.39060355,0.42447682)"
|
||||
id="polygon3"
|
||||
style="fill:#000000" /></svg>
|
After Width: | Height: | Size: 1.8 KiB |
58
themes/default/fileTransferInstance/pause_dark.svg
Normal file
58
themes/default/fileTransferInstance/pause_dark.svg
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12.000001 12"
|
||||
id="Layer_1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
sodipodi:docname="pause_dark.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="39.333333"
|
||||
inkscape:cx="0.69915255"
|
||||
inkscape:cy="4.3220339"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:pagecheckerboard="0" /><metadata
|
||||
id="metadata13"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs11" /><g
|
||||
transform="matrix(0.91525423,0,0,0.91525423,2.1550175,0.69830581)"
|
||||
id="g3"
|
||||
style="fill:#000000"><rect
|
||||
width="2.1851854"
|
||||
height="10.925926"
|
||||
x="-0.16937082"
|
||||
y="0.32962826"
|
||||
id="rect5"
|
||||
style="fill:#000000" /><rect
|
||||
width="2.1851854"
|
||||
height="10.925926"
|
||||
x="6.3861852"
|
||||
y="0.32962897"
|
||||
id="rect7"
|
||||
style="fill:#000000" /></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
@ -14,13 +14,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qTox. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
set(HEADER_LIST
|
||||
|
||||
add_library(util_library STATIC
|
||||
"include/util/compatiblerecursivemutex.h"
|
||||
"include/util/interface.h"
|
||||
"include/util/strongtype.h")
|
||||
|
||||
add_library(util_library STATIC ${HEADER_LIST})
|
||||
"include/util/strongtype.h"
|
||||
"include/util/display.h"
|
||||
"src/display.cpp")
|
||||
|
||||
# We need this directory, and users of our library will need it too
|
||||
target_include_directories(util_library PUBLIC include/)
|
||||
target_link_libraries(util_library PRIVATE Qt5::Core)
|
||||
|
||||
|
26
util/include/util/display.h
Normal file
26
util/include/util/display.h
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
Copyright © 2021 by The qTox Project Contributors
|
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox.
|
||||
|
||||
qTox is libre 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 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
qTox is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
QString getHumanReadableSize(uint64_t sizeBytes);
|
34
util/src/display.cpp
Normal file
34
util/src/display.cpp
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright © 2021 by The qTox Project Contributors
|
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox.
|
||||
|
||||
qTox is libre 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 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
qTox is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "util/display.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
QString getHumanReadableSize(uint64_t size)
|
||||
{
|
||||
static const char* suffix[] = {"B", "KiB", "MiB", "GiB", "TiB"};
|
||||
int exp = 0;
|
||||
|
||||
if (size > 0) {
|
||||
exp = std::min(static_cast<int>(log(size) / log(1024)), static_cast<int>(sizeof(suffix) / sizeof(suffix[0]) - 1));
|
||||
}
|
||||
|
||||
return QString().setNum(size / pow(1024, exp), 'f', exp > 1 ? 2 : 0).append(suffix[exp]);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user