1
0
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:
Anthony Bilinski 2021-12-20 05:55:52 -08:00
commit cea54c17c9
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
29 changed files with 1769 additions and 228 deletions

View File

@ -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

View File

@ -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 "")

View File

@ -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>

View 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();
}
}

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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) {

View File

@ -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);

View File

@ -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

View File

@ -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;
};

View 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();
}

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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());

View File

@ -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', ""));

View File

@ -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()

View File

@ -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;
};

View File

@ -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)

View 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"

View 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"

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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)

View 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
View 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]);
}