utils.cpp 26.3 KB
Newer Older
1
/***************************************************************************
2
 * Copyright (C) 2015-2019 by Savoir-faire Linux                           *
3
 * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>*
4
 * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
5
 * Author: Isa Nanic <isa.nanic@savoirfairelinux.com                       *
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *                                                                         *
 * This program is free software; you can redistribute it and/or modify    *
 * it under the terms of the GNU General Public License as published by    *
 * the Free Software Foundation; either version 3 of the License, or       *
 * (at your option) any later version.                                     *
 *                                                                         *
 * This program is distributed in the hope that it will be useful,         *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
 * GNU General Public License for more details.                            *
 *                                                                         *
 * You should have received a copy of the GNU General Public License       *
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
 **************************************************************************/

#include "utils.h"

23
#ifdef Q_OS_WIN
24
#include <windows.h>
25 26 27 28 29 30 31
#include <lmcons.h>
#include <shobjidl.h>
#include <shlguid.h>
#include <shlobj.h>
#include <shlwapi.h>
#endif

32 33 34 35 36 37 38 39 40
#include "pixbufmanipulator.h"
#include "globalsystemtray.h"
#include "lrcinstance.h"
#include "networkmanager.h"
#include "updateconfirmdialog.h"
#include "version.h"

#include <globalinstances.h>

41
//Qt
42
#include <QBitmap>
43 44
#include <QObject>
#include <QErrorMessage>
Edric Milaret's avatar
Edric Milaret committed
45
#include <QPainter>
46 47
#include <QStackedWidget>
#include <QPropertyAnimation>
48
#include <QApplication>
49
#include <QFile>
50
#include <QMessageBox>
51
#include <QScreen>
52
#include <QtConcurrent/QtConcurrent>
53
#include <QSvgRenderer>
54

55
bool
56
Utils::CreateStartupLink(const std::wstring& wstrAppName)
57
{
58
#ifdef Q_OS_WIN
59 60 61 62
    TCHAR szPath[MAX_PATH];
    GetModuleFileName(NULL, szPath, MAX_PATH);

    std::wstring programPath(szPath);
63

64 65
    TCHAR startupPath[MAX_PATH];
    SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);
66

67
    std::wstring linkPath(startupPath);
68
    linkPath += std::wstring(TEXT("\\") + wstrAppName + TEXT(".lnk"));
69 70

    return Utils::CreateLink(programPath.c_str(), linkPath.c_str());
71 72 73
#else
    return true;
#endif
74 75 76 77
}

bool
Utils::CreateLink(LPCWSTR lpszPathObj, LPCWSTR lpszPathLink) {
78
#ifdef Q_OS_WIN
79 80 81 82 83 84 85 86 87
    HRESULT hres;
    IShellLink* psl;

    hres = CoCreateInstance(CLSID_ShellLink, NULL,
                            CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
    if (SUCCEEDED(hres))
    {
        IPersistFile* ppf;
        psl->SetPath(lpszPathObj);
88
        psl->SetArguments(TEXT("--minimized"));
89 90 91 92 93 94 95 96 97 98

        hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
        if (SUCCEEDED(hres))
        {
            hres = ppf->Save(lpszPathLink, TRUE);
            ppf->Release();
        }
        psl->Release();
    }
    return hres;
99
#else
100 101
    Q_UNUSED(lpszPathObj)
    Q_UNUSED(lpszPathLink)
102 103
    return true;
#endif
104 105 106
}

void
107
Utils::DeleteStartupLink(const std::wstring& wstrAppName) {
108
#ifdef Q_OS_WIN
109 110
    TCHAR startupPath[MAX_PATH];
    SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);
111

112
    std::wstring linkPath(startupPath);
113
    linkPath += std::wstring(TEXT("\\") + wstrAppName + TEXT(".lnk"));
114 115

    DeleteFile(linkPath.c_str());
116
#endif
117 118 119
}

bool
120
Utils::CheckStartupLink(const std::wstring& wstrAppName) {
121
#ifdef Q_OS_WIN
122 123
    TCHAR startupPath[MAX_PATH];
    SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);
124

125
    std::wstring linkPath(startupPath);
126
    linkPath += std::wstring(TEXT("\\") + wstrAppName + TEXT(".lnk"));
127
    return PathFileExists(linkPath.c_str());
128 129 130
#else
    return true;
#endif
131 132
}

133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
const char*
Utils::WinGetEnv(const char* name)
{
#ifdef Q_OS_WIN
    const DWORD buffSize = 65535;
    static char buffer[buffSize];
    if (GetEnvironmentVariableA(name, buffer, buffSize)) {
        return buffer;
    } else {
        return 0;
    }
#else
    return 0;
#endif
}

void
Utils::removeOldVersions()
{
#ifdef Q_OS_WIN
    // As per: https://git.jami.net/savoirfairelinux/ring-client-windows/issues/429
    // NB: As only the 64-bit version of this application is distributed, we will only
    // remove 1. the configuration reg keys for Ring-x64, 2. the startup links for Ring,
    // 3. the winsparkle reg keys. The NSIS uninstall reg keys for Jami-x64 are removed
    // by the MSI installer.
    // Uninstallation of Ring, either 32 or 64 bit, is left to the user.
    // The current version of Jami will attempt to kill Ring.exe upon start if a startup
    // link is found.
    QString node64 = "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node";
    QString hkcuSoftwareKey = "HKEY_CURRENT_USER\\Software\\";
    QString uninstKey = "\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\";
    QString company = "Savoir-Faire Linux";

    // 1. configuration reg keys for Ring-x64
    QSettings(hkcuSoftwareKey + "jami.net\\Ring", QSettings::NativeFormat).remove("");
    QSettings(hkcuSoftwareKey + "ring.cx", QSettings::NativeFormat).remove("");
    // 2. unset Ring as a startup application
    if (Utils::CheckStartupLink(TEXT("Ring"))) {
        qDebug() << "Found startup link for Ring. Removing it and killing Ring.exe.";
        Utils::DeleteStartupLink(TEXT("Ring"));
        QProcess::execute("taskkill /im Ring.exe /f");
    }
    // 3. remove registry entries for winsparkle(both Jami-x64 and Ring-x64)
    QSettings(hkcuSoftwareKey + company, QSettings::NativeFormat).remove("");
#else
    return;
#endif
}

182 183
QString
Utils::GetRingtonePath() {
184
#ifdef Q_OS_WIN
185 186 187 188
    TCHAR workingDirectory[MAX_PATH];
    GetCurrentDirectory(MAX_PATH, workingDirectory);

    QString ringtonePath = QString::fromWCharArray(workingDirectory);
189
    ringtonePath += QStringLiteral("\\ringtones\\default.opus");
190
    return ringtonePath;
191 192 193
#else
    return QString("/usr/local");
#endif
194 195
}

196 197
QString
Utils::GenGUID() {
198
#ifdef Q_OS_WIN
199 200 201 202 203 204 205 206 207
    GUID gidReference;
    wchar_t *str;
    HRESULT hCreateGuid = CoCreateGuid(&gidReference);
    if (hCreateGuid == S_OK) {
        StringFromCLSID(gidReference, &str);
        auto gStr = QString::fromWCharArray(str);
        return gStr.remove("{").remove("}").toLower();
    }
    else
208
        return QString();
209 210 211 212 213 214 215
#else
    return QString("");
#endif
}

QString
Utils::GetISODate() {
216
#ifdef Q_OS_WIN
217 218 219 220 221
    SYSTEMTIME lt;
    GetSystemTime(&lt);
    return QString("%1-%2-%3T%4:%5:%6Z").arg(lt.wYear).arg(lt.wMonth,2,10,QChar('0')).arg(lt.wDay,2,10,QChar('0'))
            .arg(lt.wHour,2,10,QChar('0')).arg(lt.wMinute,2,10,QChar('0')).arg(lt.wSecond,2,10,QChar('0'));
#else
222 223 224 225
    return QString();
#endif
}

226 227 228 229
void
Utils::InvokeMailto(const QString& subject,
                    const QString& body,
                    const QString& attachement) {
230 231 232 233 234 235 236 237 238 239 240 241 242
#ifdef Q_OS_WIN
    HKEY hKey;
    LONG lRes = RegOpenKeyExW(HKEY_CLASSES_ROOT, L"mailto", 0, KEY_READ, &hKey);
    if (lRes != ERROR_FILE_NOT_FOUND) {
        auto addr = QString("mailto:?subject=%1&body=%2").arg(subject).arg(body);
        if (not attachement.isEmpty())
            addr += QString("&attachement=%1").arg(attachement);
        ShellExecute(nullptr, L"open", addr.toStdWString().c_str(), NULL, NULL, SW_SHOWNORMAL);
    } else {
        QErrorMessage errorMessage;
        errorMessage.showMessage(QObject::tr("No default mail client found"));
    }
#endif
243
}
Edric Milaret's avatar
Edric Milaret committed
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265

QImage
Utils::getCirclePhoto(const QImage original, int sizePhoto)
{
    QImage target(sizePhoto, sizePhoto, QImage::Format_ARGB32_Premultiplied);
    target.fill(Qt::transparent);

    QPainter painter(&target);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
    painter.setBrush(QBrush(Qt::white));
    auto scaledPhoto = original
            .scaled(sizePhoto, sizePhoto, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)
            .convertToFormat(QImage::Format_ARGB32_Premultiplied);
    int margin = 0;
    if (scaledPhoto.width() > sizePhoto) {
        margin = (scaledPhoto.width() - sizePhoto) / 2;
    }
    painter.drawEllipse(0, 0, sizePhoto, sizePhoto);
    painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
    painter.drawImage(0, 0, scaledPhoto, margin, 0);
    return target;
}
266 267

void
268
Utils::setStackWidget(QStackedWidget* stack, QWidget* widget)
269
{
270
    if (stack->indexOf(widget) != -1 && stack->currentWidget() != widget) {
271 272 273
        stack->setCurrentWidget(widget);
    }
}
274

275
void Utils::showSystemNotification(QWidget* widget, const QString& message, long delay)
276
{
Andreas Traczyk's avatar
Andreas Traczyk committed
277 278 279 280 281
    GlobalSystemTray::instance().showMessage(message, "", QIcon(":images/jami.png"));
    QApplication::alert(widget, delay);
}

void Utils::showSystemNotification(QWidget* widget,
282 283 284
                                   const QString & sender,
                                   const QString & message,
                                   long delay)
Andreas Traczyk's avatar
Andreas Traczyk committed
285
{
286
    GlobalSystemTray::instance().showMessage(sender, message, QIcon(":images/jami.png"));
287 288 289
    QApplication::alert(widget, delay);
}

290 291 292
QSize
Utils::getRealSize(QScreen* screen)
{
293
#ifdef Q_OS_WIN
294 295 296 297 298 299
    DEVMODE dmThisScreen;
    ZeroMemory(&dmThisScreen, sizeof(dmThisScreen));
    EnumDisplaySettings((const wchar_t *)screen->name().utf16(),
                        ENUM_CURRENT_SETTINGS,
                        (DEVMODE*)&dmThisScreen);
    return QSize(dmThisScreen.dmPelsWidth, dmThisScreen.dmPelsHeight);
300 301 302
#else
    return {};
#endif
303 304
}

305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
void
Utils::forceDeleteAsync(const QString& path)
{
    // keep deleting file until the process holding it let go
    // Or the file itself does not exist anymore
    QtConcurrent::run(
        [path] {
            QFile file(path);
            if (!QFile::exists(path))
                return;
            int retries{ 0 };
            while (!file.remove() && retries < 5) {
                qDebug().noquote() << "\n" << file.errorString() << "\n";
                QThread::msleep(10);
                ++retries;
            }
        });
}

324 325 326 327
QString
Utils::getChangeLog()
{
    QString logs;
328
    QFile changeLogFile(":/changelog.html");
329 330 331 332 333 334 335 336 337 338 339
    if (!changeLogFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug().noquote() << " Change log file failed to load";
        return {};
    }
    QTextStream in(&changeLogFile);
    while (!in.atEnd()) {
        logs += in.readLine();
    }
    return logs;
}

340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
void
Utils::cleanUpdateFiles()
{
    // Delete all logs and msi in the %TEMP% directory before launching
    QString dir = QString(Utils::WinGetEnv("TEMP"));
    QDir log_dir(dir, { "jami*.log" });
    for (const QString& filename : log_dir.entryList()) {
        log_dir.remove(filename);
    }
    QDir msi_dir(dir, { "jami*.msi" });
    for (const QString& filename : msi_dir.entryList()) {
        msi_dir.remove(filename);
    }
    QDir version_dir(dir, { "version" });
    for (const QString& filename : version_dir.entryList()) {
        version_dir.remove(filename);
    }
}

void
Utils::checkForUpdates(bool withUI, QWidget* parent)
{
    Utils::cleanUpdateFiles();
363 364 365 366 367
    QUrl downloadPath { isBeta ?
                        QUrl::fromEncoded("https://dl.jami.net/windows/beta/version") :
                        QUrl::fromEncoded("https://dl.jami.net/windows/version")
    };

368
    LRCInstance::instance().getNetworkManager()->getRequestReply(
369
        downloadPath,
370
        [parent, withUI] (int status, const QString& onlineVersion) {
371
            if (status != 200) {
372
                if (withUI) {
373 374 375
                    QMessageBox::critical(0,
                        QObject::tr("Update"),
                        QObject::tr("Version cannot be verified"));
376
                }
377 378
                return;
            }
379
            auto currentVersion = QString(VERSION_STRING).toULongLong();
380 381
            if (onlineVersion.isEmpty()) {
                qWarning() << "No version file found";
382
            } else if (onlineVersion.toULongLong() > currentVersion) {
383
                qDebug() << "New version found";
384
                Utils::applyUpdates(false, parent);
385 386 387 388 389 390 391 392 393 394 395 396
            } else {
                qDebug() << "No new version found";
                if (withUI) {
                    QMessageBox::information(0,
                        QObject::tr("Update"),
                        QObject::tr("No new version found"));
                }
            }
        });
}

void
397
Utils::applyUpdates(bool updateToBeta, QWidget* parent)
398 399 400
{
    if (!parent->findChild<UpdateConfirmDialog*>()) {
        UpdateConfirmDialog updateDialog(parent);
401 402
        if(updateToBeta)
            updateDialog.changeToUpdateToBetaVersionText();
403 404 405 406 407 408
        auto ret = updateDialog.exec();
        if (ret != QDialog::Accepted)
            return;
    } else
        return;

409 410 411 412 413 414 415
    QUrl downloadPath;
    if (updateToBeta || isBeta) {
        downloadPath = QUrl::fromEncoded("https://dl.jami.net/windows/beta/jami.beta.x64.msi");
    } else {
        downloadPath = QUrl::fromEncoded("https://dl.jami.net/windows/jami.release.x64.msi");
    }

416
    LRCInstance::instance().getNetworkManager()->getRequestFile(
417
        downloadPath,
418 419
        WinGetEnv("TEMP"),
        true,
420
        [parent, downloadPath](int status) {
421 422 423 424 425 426 427 428
            if (status != 200) {
                QMessageBox::critical(0,
                    QObject::tr("Update"),
                    QObject::tr("Installer download failed, please contact support"));
                return;
            }
            auto args = QString(" /passive /norestart WIXNONUILAUNCH=1");
            auto dir = Utils::WinGetEnv("TEMP");
429
            auto cmd = "powershell " + QString(dir) + "\\" + downloadPath.fileName()
430 431 432 433 434 435 436 437
                + " /L*V " + QString(dir) + "\\jami_x64_install.log" + args;
            auto retq = QProcess::startDetached(cmd);
            if (retq) {
                QCoreApplication::exit();
            }
        });
}

438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498
// new lrc helpers

inline std::string
removeEndlines(const std::string& str)
{
    std::string trimmed(str);
    trimmed.erase(std::remove(trimmed.begin(), trimmed.end(), '\n'), trimmed.end());
    trimmed.erase(std::remove(trimmed.begin(), trimmed.end(), '\r'), trimmed.end());
    return trimmed;
}

std::string
Utils::bestIdForConversation(const lrc::api::conversation::Info& conv, const lrc::api::ConversationModel& model)
{
    auto contact = model.owner.contactModel->getContact(conv.participants[0]);
    if (!contact.registeredName.empty()) {
        return removeEndlines(contact.registeredName);
    }
    return removeEndlines(contact.profileInfo.uri);
}

std::string
Utils::bestIdForAccount(const lrc::api::account::Info& account)
{
    if (!account.registeredName.empty()) {
        return removeEndlines(account.registeredName);
    }
    return removeEndlines(account.profileInfo.uri);
}

std::string
Utils::bestNameForAccount(const lrc::api::account::Info& account)
{
    if (account.profileInfo.alias.empty()) {
        return bestIdForAccount(account);
    }
    return account.profileInfo.alias;
}

std::string
Utils::bestIdForContact(const lrc::api::contact::Info& contact)
{
    if (!contact.registeredName.empty()) {
        return removeEndlines(contact.registeredName);
    }
    return removeEndlines(contact.profileInfo.uri);
}

std::string
Utils::bestNameForContact(const lrc::api::contact::Info& contact)
{
    auto alias = removeEndlines(contact.profileInfo.alias);
    if (alias.length() == 0) {
        return bestIdForContact(contact);
    }
    return alias;
}

std::string
Utils::bestNameForConversation(const lrc::api::conversation::Info& conv, const lrc::api::ConversationModel& model)
{
499 500 501 502 503 504 505 506 507
    try {
        auto contact = model.owner.contactModel->getContact(conv.participants[0]);
        auto alias = removeEndlines(contact.profileInfo.alias);
        if (alias.length() == 0) {
            return bestIdForConversation(conv, model);
        }
        return alias;
    } catch (...) {}
    return {};
508 509
}

510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
// returns empty string if only infoHash is available, second best identifier otherwise
std::string
Utils::secondBestNameForAccount(const lrc::api::account::Info& account)
{
    auto alias = removeEndlines(account.profileInfo.alias);
    auto registeredName = removeEndlines(account.registeredName);
    auto infoHash = account.profileInfo.uri;

    if (!alias.length() == 0) { // if alias exists
        if (!registeredName.length() == 0) { // if registeredName exists
            return registeredName;
        }
        else {
            return infoHash;
        }
    }
    else {
        if (!registeredName.length() == 0) { // if registeredName exists
            return infoHash;
        }
        else {
            return "";
        }
    }
}

536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
lrc::api::profile::Type
Utils::profileType(const lrc::api::conversation::Info& conv, const lrc::api::ConversationModel& model)
{
    try {
        auto contact = model.owner.contactModel->getContact(conv.participants[0]);
        return contact.profileInfo.type;
    }
    catch (...) {
        return lrc::api::profile::Type::INVALID;
    }
}

std::string
Utils::formatTimeString(const std::time_t& timestamp)
{
    std::time_t now = std::time(nullptr);
    char interactionDay[64];
    char nowDay[64];
    std::strftime(interactionDay, sizeof(interactionDay), "%D", std::localtime(&timestamp));
    std::strftime(nowDay, sizeof(nowDay), "%D", std::localtime(&now));
    if (std::string(interactionDay) == std::string(nowDay)) {
        char interactionTime[64];
        std::strftime(interactionTime, sizeof(interactionTime), "%R", std::localtime(&timestamp));
        return interactionTime;
    }
    else {
        return interactionDay;
    }
}

bool
Utils::isInteractionGenerated(const lrc::api::interaction::Type& type)
{
    return  type == lrc::api::interaction::Type::CALL ||
            type == lrc::api::interaction::Type::CONTACT;
}

bool
Utils::isContactValid(const std::string& contactUid, const lrc::api::ConversationModel& model)
{
    auto contact = model.owner.contactModel->getContact(contactUid);
    return  (contact.profileInfo.type == lrc::api::profile::Type::PENDING ||
            contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY ||
            contact.profileInfo.type == lrc::api::profile::Type::RING ||
            contact.profileInfo.type == lrc::api::profile::Type::SIP) &&
            !contact.profileInfo.uri.empty();
}

QImage
Utils::conversationPhoto(const std::string & convUid, const lrc::api::account::Info& accountInfo)
{
587
    auto convInfo = LRCInstance::getConversationFromConvUid(convUid, accountInfo.id, false);
588 589 590 591
    if (!convInfo.uid.empty()) {
        return GlobalInstances::pixmapManipulator()
            .decorationRole(convInfo, accountInfo)
            .value<QImage>();
592
    }
593
    return QImage();
594
}
595

Andreas Traczyk's avatar
Andreas Traczyk committed
596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633
QColor
Utils::getAvatarColor(const QString& canonicalUri) {
    if (canonicalUri.isEmpty()) {
        return RingTheme::defaultAvatarColor_;
    }
    auto h = QString(QCryptographicHash::hash(canonicalUri.toLocal8Bit(), QCryptographicHash::Md5).toHex());
    if (h.isEmpty() || h.isNull()) {
        return RingTheme::defaultAvatarColor_;
    }
    auto colorIndex = std::string("0123456789abcdef").find(h.at(0).toLatin1());
    return RingTheme::avatarColors_[colorIndex];
}

// Generate a QImage representing a dummy user avatar, when user doesn't provide it.
// Current rendering is a flat colored circle with a centered letter.
// The color of the letter is computed from the circle color to be visible whaterver be the circle color.
QImage
Utils::fallbackAvatar(const QSize size, const QString& canonicalUriStr, const QString& letterStr)
{
    // We start with a transparent avatar
    QImage avatar(size, QImage::Format_ARGB32);
    avatar.fill(Qt::transparent);

    // We pick a color based on the passed character
    QColor avColor = getAvatarColor(canonicalUriStr);

    // We draw a circle with this color
    QPainter painter(&avatar);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
    painter.setPen(Qt::transparent);
    painter.setBrush(avColor.lighter(110));
    painter.drawEllipse(avatar.rect());

    // If a letter was passed, then we paint a letter in the circle,
    // otherwise we draw the default avatar icon
    QString letterStrCleaned(letterStr);
    letterStrCleaned.remove(QRegExp("[\\n\\t\\r]"));
    if (!letterStr.isEmpty()) {
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654
        auto unicode = letterStr.toUcs4().at(0);
        if (unicode >= 0x1F000 && unicode <= 0x1FFFF) { // is Emoticon
            auto letter = QString::fromUcs4(&unicode, 1);
            QFont font(QStringLiteral("Segoe UI Emoji"), avatar.height() / 2.66667, QFont::Medium);
            painter.setFont(font);
            QRect emojiRect(avatar.rect());
            emojiRect.moveTop(-6);
            painter.drawText(emojiRect, letter, QTextOption(Qt::AlignCenter));
        } else if (unicode >= 0x0000 && unicode <= 0x00FF) { // is Basic Latin
            auto letter = letterStr.at(0).toUpper();
            QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
            painter.setFont(font);
            painter.setPen(Qt::white);
            painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
        } else {
            auto letter = QString::fromUcs4(&unicode, 1);
            QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
            painter.setFont(font);
            painter.setPen(Qt::white);
            painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
        }
Andreas Traczyk's avatar
Andreas Traczyk committed
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697
    } else {
        QRect overlayRect = avatar.rect();
        qreal margin = (0.05 * overlayRect.width());
        overlayRect.moveLeft(overlayRect.left() + margin * 0.5);
        overlayRect.moveTop(overlayRect.top() + margin * 0.5);
        overlayRect.setWidth(overlayRect.width() - margin);
        overlayRect.setHeight(overlayRect.height() - margin);
        painter.drawPixmap(overlayRect, QPixmap(":/images/default_avatar_overlay.svg"));
    }

    return avatar;
}

QImage
Utils::fallbackAvatar(const QSize size, const std::string& alias, const std::string& uri)
{
    return fallbackAvatar(size,
        QString::fromStdString(uri),
        QString::fromStdString(alias));
}

QByteArray
Utils::QImageToByteArray(QImage image)
{
    QByteArray ba;
    QBuffer buffer(&ba);
    buffer.open(QIODevice::WriteOnly);
    image.save(&buffer, "PNG");
    return ba;
}

QImage
Utils::cropImage(const QImage& img)
{
    QRect rect;
    auto w = img.width();
    auto h = img.height();
    if (w > h) {
        return img.copy({ (w - h) / 2, 0, h, h });
    }
    return img.copy({ 0, (h - w) / 2, w, w });
}

698 699 700 701 702 703 704 705 706 707 708
QPixmap
Utils::pixmapFromSvg(const QString& svg_resource, const QSize& size)
{
    QSvgRenderer svgRenderer(svg_resource);
    QPixmap pixmap(size);
    pixmap.fill(Qt::transparent);
    QPainter pixPainter(&pixmap);
    svgRenderer.render(&pixPainter);
    return pixmap;
}

709
QString
710
Utils::formattedTime(int duration)
711
{
712 713 714 715 716 717 718 719 720
    if (duration == 0) return {};
    std::string formattedString;
    auto minutes = duration / 60;
    auto seconds = duration % 60;
    if (minutes > 0) {
        formattedString += std::to_string(minutes) + ":";
        if (formattedString.length() == 2) {
            formattedString = "0" + formattedString;
        }
721
    } else {
722
        formattedString += "00:";
723
    }
724 725 726
    if (seconds < 10) formattedString += "0";
    formattedString += std::to_string(seconds);
    return QString::fromStdString(formattedString);
727 728
}

729 730 731 732 733 734 735 736 737 738
QByteArray
Utils::QByteArrayFromFile(const QString& filename)
{
    QFile file(filename);
    if (file.open(QIODevice::ReadOnly)) {
        return file.readAll();
    } else {
        qDebug() << "can't open file";
        return QByteArray();
    }
Andreas Traczyk's avatar
Andreas Traczyk committed
739 740 741 742 743 744 745 746 747 748 749
}

QPixmap
Utils::generateTintedPixmap(const QString& filename, QColor color)
{
    QPixmap px(filename);
    QImage tmpImage = px.toImage();
    for (int y = 0; y < tmpImage.height(); y++) {
        for (int x = 0; x < tmpImage.width(); x++) {
            color.setAlpha(tmpImage.pixelColor(x, y).alpha());
            tmpImage.setPixelColor(x, y, color);
750 751 752 753 754 755 756 757 758 759 760 761 762
        }
    }
    return QPixmap::fromImage(tmpImage);
}

QPixmap Utils::generateTintedPixmap(const QPixmap& pix, QColor color)
{
    QPixmap px = pix;
    QImage tmpImage = px.toImage();
    for (int y = 0; y < tmpImage.height(); y++) {
        for (int x = 0; x < tmpImage.width(); x++) {
            color.setAlpha(tmpImage.pixelColor(x, y).alpha());
            tmpImage.setPixelColor(x, y, color);
Andreas Traczyk's avatar
Andreas Traczyk committed
763 764 765
        }
    }
    return QPixmap::fromImage(tmpImage);
766 767
}

Andreas Traczyk's avatar
Andreas Traczyk committed
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784
QImage
Utils::scaleAndFrame(const QImage photo, const QSize& size)
{
    return photo.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}

QImage
Utils::accountPhoto(const lrc::api::account::Info& accountInfo, const QSize& size)
{
    QImage photo;
    if (!accountInfo.profileInfo.avatar.empty()) {
        QByteArray ba = QByteArray::fromStdString(accountInfo.profileInfo.avatar);
        photo = GlobalInstances::pixmapManipulator().personPhoto(ba, nullptr).value<QImage>();
    } else {
        auto bestId = bestIdForAccount(accountInfo);
        auto bestName = bestNameForAccount(accountInfo);
        QString letterStr = bestId == bestName ? QString() : QString::fromStdString(bestName);
785
        QString prefix = accountInfo.profileInfo.type == lrc::api::profile::Type::RING ? "ring:" : "sip:";
Andreas Traczyk's avatar
Andreas Traczyk committed
786
        photo = fallbackAvatar(size,
787
            prefix + QString::fromStdString(accountInfo.profileInfo.uri),
Andreas Traczyk's avatar
Andreas Traczyk committed
788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803
            letterStr);
    }
    return scaleAndFrame(photo, size);
}

void
Utils::swapQListWidgetItems(QListWidget* list, bool down)
{
    QListWidgetItem *current = list->currentItem();
    int currIndex = list->row(current);
    QListWidgetItem *other = list->item(list->row(current) + (down ? 1 : -1));
    int otherIndex = list->row(other);
    QListWidgetItem *temp = list->takeItem(otherIndex);
    down ? list->insertItem(currIndex, temp) : list->insertItem(otherIndex, current);
    down ? list->insertItem(otherIndex, current) : list->insertItem(currIndex, temp);
}
804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823

QString
Utils::humanFileSize(qint64 fileSize)
{
    float fileSizeF = static_cast<float>(fileSize);
    float thresh = 1024;

    if(abs(fileSizeF) < thresh) {
        return QString::number(fileSizeF) + " B";
    }
    QString units[] = { "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
    int unit_position = -1;
    do {
        fileSizeF /= thresh;
        ++unit_position;
    } while (abs(fileSizeF) >= thresh && unit_position < units->size() - 1);
    //Round up to two decimal
    fileSizeF = roundf(fileSizeF * 100) / 100;
    return QString::number(fileSizeF) + " " + units[unit_position];
}