mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
feat(filesform): Add in progress transfers to files form
As part of #1532 it was identified that long running file transfers could get lost deep in the chatlog. This could result in unexpected use of bandwidth over time if users lose track of old/large transfers. This commit updates the files form to show in progress file transfers and offer a way to control them. * FilesForm now works on ToxFiles instead of finished file paths * FilesForm widgets have been replaced with an MV tree view with depth 1. The existing QListWidget did not provide us the controls to render more complex items. The use of delegates allows us to efficiently draw progress bars and controls * getHumanReadableSize has been extracted from FileTransferWidget into a more general utils file
This commit is contained in:
parent
c7efe320d2
commit
257a19caaa
|
@ -57,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>
|
||||
|
@ -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) {
|
||||
|
|
|
@ -42,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:
|
||||
|
|
|
@ -448,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;
|
||||
|
@ -519,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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
@ -1136,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)
|
||||
|
|
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…
Reference in New Issue
Block a user