commit 8745fbe6a051961a1c5bd1aa7fa336d17742d723 Author: Dmytro Bogovych Date: Sun Apr 17 15:37:04 2022 +0300 - initial import diff --git a/README.md b/README.md new file mode 100644 index 0000000..fdbff82 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +This is my experiment to get: +- simple app to make periodical breaks on my Kubuntu 20.04 +- learn Linux GUI apps packaging and distribution +- avoid too heavy apps for simple task + +Please don't blame on me for this app. +My thanks for authors of similar apps ! You inspired me to make this one. + +There is scripts/build_linux.sh script to produce AppImage file. +Release part has ready one. But this was built on Kubuntu 20.04 and will require recent versions of runtime libraries. + +Please send me questions and requests on dmytro.bogovych (at) gmail.com. + diff --git a/app/aboutdlg.cpp b/app/aboutdlg.cpp new file mode 100644 index 0000000..e7f7b1e --- /dev/null +++ b/app/aboutdlg.cpp @@ -0,0 +1,21 @@ +#include "aboutdlg.h" +#include "ui_aboutdlg.h" +#include "config.h" + +AboutDlg::AboutDlg(QWidget *parent) : + QDialog(parent), + ui(new Ui::AboutDlg) +{ + ui->setupUi(this); + + auto version_text = QString("Version %1.%2.%3") + .arg(QBREAK_VERSION_MAJOR) + .arg(QBREAK_VERSION_MINOR) + .arg(QBREAK_VERSION_SUFFIX); + ui->mAppVersionLabel->setText(version_text); +} + +AboutDlg::~AboutDlg() +{ + delete ui; +} diff --git a/app/aboutdlg.h b/app/aboutdlg.h new file mode 100644 index 0000000..eeeb1bb --- /dev/null +++ b/app/aboutdlg.h @@ -0,0 +1,22 @@ +#ifndef ABOUTDLG_H +#define ABOUTDLG_H + +#include + +namespace Ui { +class AboutDlg; +} + +class AboutDlg : public QDialog +{ + Q_OBJECT + +public: + explicit AboutDlg(QWidget *parent = nullptr); + ~AboutDlg(); + +private: + Ui::AboutDlg *ui; +}; + +#endif // ABOUTDLG_H diff --git a/app/aboutdlg.ui b/app/aboutdlg.ui new file mode 100644 index 0000000..0f11bc7 --- /dev/null +++ b/app/aboutdlg.ui @@ -0,0 +1,133 @@ + + + AboutDlg + + + + 0 + 0 + 400 + 300 + + + + + 400 + 300 + + + + Dialog + + + + + + + 16777215 + 25 + + + + QBreak. + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + 16777215 + 25 + + + + Version + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + 16777215 + 100 + + + + App to make breaks in work. Thanks to the authors of similar apps which inspired me to make this one. + + + Qt::PlainText + + + Qt::AlignCenter + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + AboutDlg + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AboutDlg + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/app/assets/images/README.txt b/app/assets/images/README.txt new file mode 100644 index 0000000..b625b0b --- /dev/null +++ b/app/assets/images/README.txt @@ -0,0 +1,3 @@ +URL for used image: +https://thenounproject.com/icon/coffee-break-537907/ + diff --git a/app/assets/images/app_icon.png b/app/assets/images/app_icon.png new file mode 100644 index 0000000..dfdb73d Binary files /dev/null and b/app/assets/images/app_icon.png differ diff --git a/app/assets/images/app_icon.svg b/app/assets/images/app_icon.svg new file mode 100644 index 0000000..fb2f816 --- /dev/null +++ b/app/assets/images/app_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/app_icon_dark.png b/app/assets/images/app_icon_dark.png new file mode 100644 index 0000000..81951f1 Binary files /dev/null and b/app/assets/images/app_icon_dark.png differ diff --git a/app/assets/images/app_icon_light.png b/app/assets/images/app_icon_light.png new file mode 100644 index 0000000..03a43ae Binary files /dev/null and b/app/assets/images/app_icon_light.png differ diff --git a/app/assets/images/coffee_cup/README.txt b/app/assets/images/coffee_cup/README.txt new file mode 100644 index 0000000..d6e9936 --- /dev/null +++ b/app/assets/images/coffee_cup/README.txt @@ -0,0 +1,3 @@ +URL: https://ru.seaicons.com/89980/ +License: https://creativecommons.org/licenses/by-sa/4.0/ + diff --git a/app/assets/images/coffee_cup/alecive-flatwoken-apps-caffeine.icns b/app/assets/images/coffee_cup/alecive-flatwoken-apps-caffeine.icns new file mode 100644 index 0000000..8269629 Binary files /dev/null and b/app/assets/images/coffee_cup/alecive-flatwoken-apps-caffeine.icns differ diff --git a/app/assets/images/coffee_cup/alecive-flatwoken-apps-caffeine.ico b/app/assets/images/coffee_cup/alecive-flatwoken-apps-caffeine.ico new file mode 100644 index 0000000..af3d565 Binary files /dev/null and b/app/assets/images/coffee_cup/alecive-flatwoken-apps-caffeine.ico differ diff --git a/app/assets/images/coffee_cup/icon_128x128.png b/app/assets/images/coffee_cup/icon_128x128.png new file mode 100644 index 0000000..c3355ad Binary files /dev/null and b/app/assets/images/coffee_cup/icon_128x128.png differ diff --git a/app/assets/images/coffee_cup/icon_16x16.png b/app/assets/images/coffee_cup/icon_16x16.png new file mode 100644 index 0000000..0fbe9b0 Binary files /dev/null and b/app/assets/images/coffee_cup/icon_16x16.png differ diff --git a/app/assets/images/coffee_cup/icon_24x24.png b/app/assets/images/coffee_cup/icon_24x24.png new file mode 100644 index 0000000..79abc2f Binary files /dev/null and b/app/assets/images/coffee_cup/icon_24x24.png differ diff --git a/app/assets/images/coffee_cup/icon_32x32.png b/app/assets/images/coffee_cup/icon_32x32.png new file mode 100644 index 0000000..0c0d6b6 Binary files /dev/null and b/app/assets/images/coffee_cup/icon_32x32.png differ diff --git a/app/assets/images/coffee_cup/icon_48x48.png b/app/assets/images/coffee_cup/icon_48x48.png new file mode 100644 index 0000000..2189ba0 Binary files /dev/null and b/app/assets/images/coffee_cup/icon_48x48.png differ diff --git a/app/assets/images/coffee_cup/icon_512x512.png b/app/assets/images/coffee_cup/icon_512x512.png new file mode 100644 index 0000000..1d5bd6b Binary files /dev/null and b/app/assets/images/coffee_cup/icon_512x512.png differ diff --git a/app/assets/images/coffee_cup/icon_64x64.png b/app/assets/images/coffee_cup/icon_64x64.png new file mode 100644 index 0000000..086348f Binary files /dev/null and b/app/assets/images/coffee_cup/icon_64x64.png differ diff --git a/app/assets/images/coffee_cup/icon_72x72.png b/app/assets/images/coffee_cup/icon_72x72.png new file mode 100644 index 0000000..f068941 Binary files /dev/null and b/app/assets/images/coffee_cup/icon_72x72.png differ diff --git a/app/assets/images/coffee_cup/icon_96x96.png b/app/assets/images/coffee_cup/icon_96x96.png new file mode 100644 index 0000000..4afe1bc Binary files /dev/null and b/app/assets/images/coffee_cup/icon_96x96.png differ diff --git a/app/assets/images/noun-coffee-break-537907.png b/app/assets/images/noun-coffee-break-537907.png new file mode 100644 index 0000000..dfdb73d Binary files /dev/null and b/app/assets/images/noun-coffee-break-537907.png differ diff --git a/app/assets/images/noun-coffee-break-537907.svg b/app/assets/images/noun-coffee-break-537907.svg new file mode 100644 index 0000000..fb2f816 --- /dev/null +++ b/app/assets/images/noun-coffee-break-537907.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/misc/qbreak.desktop b/app/assets/misc/qbreak.desktop new file mode 100644 index 0000000..4661425 --- /dev/null +++ b/app/assets/misc/qbreak.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +GenericName=App to make the breaks +Name=QBreak +Version=0.0.5 +Exec=/usr/bin/qbreak +Comment=App to make periodical breaks +Icon=qbreak +Type=Application +Terminal=false +StartupNotify=false +Encoding=UTF-8 +Categories=Utility; diff --git a/app/autostart.cpp b/app/autostart.cpp new file mode 100644 index 0000000..11c4e42 --- /dev/null +++ b/app/autostart.cpp @@ -0,0 +1,140 @@ +#include "autostart.h" +#include + +#if defined(TARGET_LINUX) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +#define QBREAK_DESKTOP_NAME "qbreak.desktop" + +static std::string read_desktop_file() +{ + QFile f(":/assets/misc/qbreak.desktop"); + f.open(QFile::ReadOnly); + auto bytes = f.readAll(); + return bytes.toStdString(); +} + +static std::string fixup_desktop_file(const std::string& desktop_unit, const std::string& path) +{ + // load set with first file + std::istringstream inf(desktop_unit); + std::vector lines; + std::string line; + for (unsigned int i=0; std::getline(inf,line); i++) + { + if (line.find("Exec=") == 0) + { + line = "Exec=" + path; + } + lines.push_back(line); + } + + std::ostringstream oss; + std::copy(lines.begin(), lines.end(), std::ostream_iterator(oss, "\n")); + return oss.str(); +} + +static fs::path home_dir() +{ + struct passwd *pw = getpwuid(getuid()); + return pw ? pw->pw_dir : std::string(); +} + +static fs::path autostart_dir() +{ + return home_dir() / ".config" / "autostart"; +} + +static fs::path autostart_path() +{ + return autostart_dir() / QBREAK_DESKTOP_NAME; +} + +void autostart::enable(const std::string& path_to_me) +{ + // Put .desktop file to ~/.config/autostart + std::ofstream ofs(autostart_path()); + if (ofs.is_open()) + { + ofs << fixup_desktop_file(read_desktop_file(), path_to_me); + ofs.close(); + } + else + qDebug() << "Failed to write the desktop entry into autostart dir. Error: " << errno; +} + +void autostart::disable() +{ + // Remove .desktop file from ~/.config/autostart + ::remove(autostart_path().c_str()); +} + +bool autostart::is_enabled() +{ + return fs::exists(fs::path(autostart_path())); +} + +// ----- appmenu ----- +static fs::path appmenu_install_dir() +{ + // Global one + // return fs::path("/usr/share/applications"); + + // User only + return home_dir() / ".local" / "share" / "applications"; +} + +void appmenu::install(const std::string& path_to_me) +{ + // Put .desktop file to ~/.config/autostart + std::ofstream ofs(appmenu_install_dir() / QBREAK_DESKTOP_NAME); + if (ofs.is_open()) + { + ofs << fixup_desktop_file(read_desktop_file(), path_to_me); + ofs.close(); + } + else + qDebug() << "Failed to write the desktop entry into app menu dir. Error: " << errno; + + // Install latest icons + { + auto icons = {"16x16", "32x32", "64x64", "128x128", "512x512"}; + + // Here Qt part + auto target_dir = QFileInfo(QDir::homePath() + "/.local/share/icons/hicolor").absoluteFilePath(); + if (QFileInfo::exists(target_dir)) + { + // Copy icons from resources + for (auto& icon_suffix: icons) + { + QString icon_src = QString(":/assets/images/coffee_cup/icon_") + icon_suffix + ".png", + icon_dst = target_dir + "/" + icon_suffix + "/apps/qbreak.png"; + QFile::copy(icon_src, icon_dst); + } + } + } +} + +void appmenu::uninstall() +{ + if (is_installed()) + fs::remove(appmenu_install_dir() / QBREAK_DESKTOP_NAME); +} + +bool appmenu::is_installed() +{ + return fs::exists(appmenu_install_dir() / QBREAK_DESKTOP_NAME); +} +#endif diff --git a/app/autostart.h b/app/autostart.h new file mode 100644 index 0000000..4161da0 --- /dev/null +++ b/app/autostart.h @@ -0,0 +1,22 @@ +#ifndef AUTOSTART_H +#define AUTOSTART_H + +#include + +class autostart +{ +public: + static void enable(const std::string& path_to_me); + static void disable(); + static bool is_enabled(); +}; + +class appmenu +{ +public: + static void install(const std::string& path_to_me); + static void uninstall(); + static bool is_installed(); +}; + +#endif // AUTOSTART_H diff --git a/app/config.h b/app/config.h new file mode 100644 index 0000000..8dbeccb --- /dev/null +++ b/app/config.h @@ -0,0 +1,9 @@ +#ifndef CONFIG_H +#define CONFIG_H + +// App version +#define QBREAK_VERSION_MAJOR 0 +#define QBREAK_VERSION_MINOR 0 +#define QBREAK_VERSION_SUFFIX 5 + +#endif // CONFIG_H diff --git a/app/main.cpp b/app/main.cpp new file mode 100644 index 0000000..583db4b --- /dev/null +++ b/app/main.cpp @@ -0,0 +1,57 @@ +#include "mainwindow.h" +#include "autostart.h" +#include "runguard.h" + +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + RunGuard guard("adjfaifaif"); + if ( !guard.tryToRun() ) + return 0; + + QApplication app(argc, argv); + + QCoreApplication::setOrganizationName("qbreak.com"); + QCoreApplication::setOrganizationDomain("qbreak.com"); + QCoreApplication::setApplicationName("QBreak"); + + QTranslator translator; + bool ok = translator.load(":/i18n/strings_" + QLocale::system().name()); + + if (ok) + app.installTranslator(&translator); + + app.setQuitOnLastWindowClosed(false); + + QCommandLineParser parser; + QCommandLineOption test_1(QStringList() << "t1" << "test_1"); + parser.addOption(test_1); + + // Run app with break intervals & duration 60 seconds. + // This should trigger the notification in 30 seconds before breaks. + QCommandLineOption test_2(QStringList() << "t2" << "test_2"); + parser.addOption(test_2); + + parser.process(app); + + // Put itself into app menu + appmenu::install(QFileInfo(argv[0]).absoluteFilePath().toStdString()); + + // Main window is full screen window, so start with tray icon only + MainWindow w; + w.hide(); + + if (parser.isSet(test_1)) + w.test_1(); + else + if (parser.isSet(test_2)) + w.test_2(); + + int retcode = app.exec(); + + return retcode; +} diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp new file mode 100644 index 0000000..dadeb8b --- /dev/null +++ b/app/mainwindow.cpp @@ -0,0 +1,379 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include "settingsdialog.h" +#include "settings.h" +#include "autostart.h" +#include "aboutdlg.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + init(); +} + +MainWindow::~MainWindow() +{ + if (mSettingsDialog) + delete mSettingsDialog; + delete ui; +} + +static bool isDarkTheme() +{ + // Thanks to stackoverflow ! Another idea is to use https://successfulsoftware.net/2021/03/31/how-to-add-a-dark-theme-to-your-qt-application/ + auto label = QLabel("am I in the dark?"); + auto text_hsv_value = label.palette().color(QPalette::WindowText).value(); + auto bg_hsv_value = label.palette().color(QPalette::Background).value(); + bool dark_theme_found = text_hsv_value > bg_hsv_value; + + return dark_theme_found; +} + +QIcon MainWindow::getAppIcon() +{ + QIcon app_icon(QPixmap(QString(":/assets/images/coffee_cup/icon_128x128.png"))); + + return app_icon; +} + +QIcon MainWindow::getTrayIcon() +{ + // QString app_icon_name = isDarkTheme() ? "app_icon_dark.png" : "app_icon_light.png"; + // QIcon app_icon(QPixmap(QString(":/assets/images/%1").arg(app_icon_name))); + // return app_icon; + + return QIcon(QPixmap(":/assets/images/coffee_cup/icon_64x64.png")); +} + +void MainWindow::init() +{ + // Icon + setWindowIcon(getAppIcon()); + + // ToDo: finish the multiple monitor support + /*QDesktopWidget *widget = QApplication::desktop(); + qDebug() << "Number of screens:" << widget->screenCount(); + + connect(widget, &QDesktopWidget::screenCountChanged, this, [](int screenCount){}); + if (widget->screenCount() > 1) { + } else if (widget->screenCount() == 1){ + }*/ + + // Tray icon + createTrayIcon(); + + // Latest config + loadConfig(); + + // Settings dialog + mSettingsDialog = nullptr; + + // Timer + mTimer = new QTimer(this); + mTimer->setTimerType(Qt::TimerType::CoarseTimer); + mTimer->setSingleShot(true); + connect(mTimer, SIGNAL(timeout()), this, SLOT(onLongBreakStart())); + + mNotifyTimer = new QTimer(this); + mNotifyTimer->setTimerType(Qt::TimerType::CoarseTimer); + mNotifyTimer->setSingleShot(true); + connect(mNotifyTimer, SIGNAL(timeout()), this, SLOT(onLongBreakNotify())); + + + mUpdateUITimer = new QTimer(this); + mUpdateUITimer->setTimerType(Qt::TimerType::CoarseTimer); + mUpdateUITimer->setSingleShot(false); + mUpdateUITimer->setInterval(std::chrono::minutes(1)); + connect(mUpdateUITimer, SIGNAL(timeout()), this, SLOT(onUpdateUI())); + mUpdateUITimer->start(); + + mProgressTimer = new QTimer(this); + mProgressTimer->setInterval(std::chrono::milliseconds(1000)); + mProgressTimer->setSingleShot(false); + connect(mProgressTimer, SIGNAL(timeout()), this, SLOT(onProgress())); + + connect(ui->mPostponeButton, SIGNAL(clicked()), this, SLOT(onLongBreakPostpone())); + + // Use the latest config + applyConfig(); + + // Refresh UI + onUpdateUI(); +} + +void MainWindow::loadConfig() +{ + app_settings settings; + mAppConfig = settings.load(); +} + +void MainWindow::applyConfig() +{ + if (mTimer) + { + if (mTimer->interval() != mAppConfig.longbreak_interval) + { + mTimer->stop(); + mTimer->setInterval(std::chrono::seconds(mAppConfig.longbreak_interval)); + mTimer->start(); + + mNotifyTimer->stop(); + mNotifyTimer->setInterval(std::chrono::seconds(mAppConfig.longbreak_interval - 30)); + mNotifyTimer->start(); + } + } + + if (mAppConfig.window_on_top) + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + else + setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); + + if (mAppConfig.autostart) + { + QString path_to_me = QFileInfo(QApplication::arguments().front()).canonicalPath(); + autostart::enable(path_to_me.toStdString()); + } + else + autostart::disable(); +} + +void MainWindow::schedule() +{ + ; +} + +void MainWindow::test_1() +{ + // 10 seconds test break + mAppConfig.longbreak_length = 10; + mAppConfig.longbreak_postpone_interval = 10; + mAppConfig.longbreak_interval = 10; + + mAppConfig.window_on_top = true; + mAppConfig.verbose = true; + + onLongBreakStart(); +} + +void MainWindow::test_2() +{ + // 60 seconds test break + mAppConfig.longbreak_length = 60; + mAppConfig.longbreak_postpone_interval = 60; + mAppConfig.longbreak_interval = 60; + + mAppConfig.window_on_top = true; + mAppConfig.verbose = true; + + applyConfig(); +} + +void MainWindow::showMe() +{ + // Show on primary screen + QScreen* screen = QGuiApplication::primaryScreen(); + showFullScreen(); + if (windowHandle()) + { + windowHandle()->setScreen(screen); + } +} + +void MainWindow::hideMe() +{ + this->hide(); +} + +static QString secondsToText(int seconds) +{ + if (seconds < 60) + return QString("%1s").arg(seconds); + else + return QString("%1m %2s").arg(seconds / 60).arg(seconds % 60); +} + +void MainWindow::createTrayIcon() +{ + mTrayIcon = new QSystemTrayIcon(this); + QMenu* menu = new QMenu(this); + + QAction* nextBreakAction = new QAction(tr("Start next break"), menu); + connect(nextBreakAction, SIGNAL(triggered()), this, SLOT(onNextBreak())); + + QAction* settingsAction = new QAction(tr("Settings"), menu); + connect(settingsAction, SIGNAL(triggered()), this, SLOT(onSettings())); + + QAction* aboutAction = new QAction(tr("About"), menu); + connect(aboutAction, SIGNAL(triggered()), this, SLOT(onAbout())); + + QAction* exitAction = new QAction(tr("Exit"), menu); + connect(exitAction, SIGNAL(triggered()), this, SLOT(onExit())); + + menu->addAction(nextBreakAction); + menu->addAction(settingsAction); + menu->addAction(aboutAction); + menu->addAction(exitAction); + + mTrayIcon->setContextMenu(menu); + mTrayIcon->setIcon(getTrayIcon()); + mTrayIcon->setToolTip(AppName); + mTrayIcon->show(); +} + +void MainWindow::onUpdateUI() +{ + if (mTrayIcon) + { + if (mProgressTimer->isActive()) + { + // Break is in effect now + mTrayIcon->setToolTip(QString()); + } + else + if (mTimer->isActive()) + { + auto remaining = std::chrono::duration_cast(mTimer->remainingTimeAsDuration()); + if (remaining.count() == 0) + mTrayIcon->setToolTip(tr("Less than a minute left until the next break.")); + else + mTrayIcon->setToolTip(tr("There are %1 minutes left until the next break.").arg(remaining.count())); + } + } +} + +void MainWindow::onLongBreakNotify() +{ + mTrayIcon->showMessage(tr("New break"), + tr("New break will start in %1 secs").arg(Default_Notify_Length), + getAppIcon(), + 30000); +} + +void MainWindow::onLongBreakStart() +{ + qDebug() << "Long break starts for " << secondsToText(mAppConfig.longbreak_postpone_interval); + + ui->mPostponeButton->setText(tr("Postpone for ") + secondsToText(mAppConfig.longbreak_postpone_interval)); + showMe(); + + mBreakStartTime = std::chrono::steady_clock::now(); + + // Start progress bar + mProgressTimer->start(); + + // Refresh UI + onUpdateUI(); +} + +void MainWindow::onLongBreakEnd() +{ + qDebug() << "Long break ends."; + + // Prepare to next triggering + ui->mProgressBar->setValue(0); + + // Hide the window + hideMe(); + + // Start new timer + mTimer->start(std::chrono::seconds(mAppConfig.longbreak_interval)); + mNotifyTimer->start(std::chrono::seconds(mAppConfig.longbreak_interval - 30)); + + // Refresh UI + onUpdateUI(); +} + +void MainWindow::onLongBreakPostpone() +{ + qDebug() << "Long break postponed."; + hideMe(); + + mProgressTimer->stop(); + ui->mProgressBar->setValue(0); + + // Start timer again + mTimer->start(std::chrono::seconds(mAppConfig.longbreak_postpone_interval)); + mNotifyTimer->start(std::chrono::seconds(mAppConfig.longbreak_postpone_interval - 30)); + + // Refresh UI + onUpdateUI(); +} + +void MainWindow::onProgress() +{ + auto timestamp = std::chrono::steady_clock::now(); + auto delta = std::chrono::duration_cast(timestamp - mBreakStartTime).count(); + + int percents = (delta / float(mAppConfig.longbreak_length) * 100); + ui->mProgressBar->setValue(percents); + int remaining = mAppConfig.longbreak_length - delta; + if (remaining < 0) + remaining = 0; + + ui->mRemainingLabel->setText(QString("Remaining: ") + secondsToText(remaining)); + + if (percents > 100) + { + mProgressTimer->stop(); + onLongBreakEnd(); + } + else + showMe(); +} + +void MainWindow::onNextBreak() +{ + mTimer->stop(); + mNotifyTimer->stop(); + + onLongBreakStart(); +} + +void MainWindow::onSettings() +{ + if (mSettingsDialog == nullptr) + { + // Settings dialog must be top level window to be positioned on the primary screen + mSettingsDialog = new SettingsDialog(nullptr); + connect(mSettingsDialog, &QDialog::accepted, [this]() + { + mAppConfig = app_settings::load(); + applyConfig(); + mSettingsDialog->hide(); + }); + + connect(mSettingsDialog, &QDialog::rejected, [this]() + { + mSettingsDialog->hide(); + }); + } + // Move to primary screen + mSettingsDialog->show(); + QScreen* screen = QGuiApplication::primaryScreen(); + mSettingsDialog->windowHandle()->setScreen(screen); +} + +void MainWindow::onAbout() +{ + AboutDlg dlg; + dlg.exec(); +} + +void MainWindow::onExit() +{ + this->close(); + QApplication::exit(); +} diff --git a/app/mainwindow.h b/app/mainwindow.h new file mode 100644 index 0000000..7a990d0 --- /dev/null +++ b/app/mainwindow.h @@ -0,0 +1,67 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include + +#include +#include "settings.h" +#include "settingsdialog.h" + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + + // Start screen immediately with 15 seconds interval + void test_1(); + + // Run 60/60 seconds interval & duration + void test_2(); + +private: + Ui::MainWindow *ui; + QTimer* mTimer; + QTimer* mNotifyTimer; + QTimer* mShowNotifyTimer; + QTimer* mUpdateUITimer; + QTimer* mProgressTimer; + QSystemTrayIcon* mTrayIcon; + SettingsDialog* mSettingsDialog; + std::chrono::steady_clock::time_point mBreakStartTime; + app_settings::config mAppConfig; + + void init(); + void loadConfig(); + void applyConfig(); + void schedule(); + void createTrayIcon(); + void showMe(); + void hideMe(); + + void startNotification(); + + QIcon getAppIcon(); + QIcon getTrayIcon(); + +public slots: + void onUpdateUI(); + void onLongBreakNotify(); + void onLongBreakStart(); + void onLongBreakPostpone(); + void onLongBreakEnd(); + void onProgress(); + void onNextBreak(); + void onSettings(); + void onAbout(); + void onExit(); +}; +#endif // MAINWINDOW_H diff --git a/app/mainwindow.ui b/app/mainwindow.ui new file mode 100644 index 0000000..b68676d --- /dev/null +++ b/app/mainwindow.ui @@ -0,0 +1,143 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + + Qt::Vertical + + + + 20 + 75 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 400 + 0 + + + + 0 + + + + + + + Remaining: + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 150 + 0 + + + + + 300 + 100 + + + + Postpone for 10 minutes + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + + diff --git a/app/qbreak.pro b/app/qbreak.pro new file mode 100644 index 0000000..43f2fde --- /dev/null +++ b/app/qbreak.pro @@ -0,0 +1,43 @@ +QT += core gui svg + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++17 lrelease embed_translations + +# You can make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + aboutdlg.cpp \ + autostart.cpp \ + main.cpp \ + mainwindow.cpp \ + settings.cpp \ + settingsdialog.cpp \ + runguard.cpp + +HEADERS += \ + aboutdlg.h \ + autostart.h \ + config.h \ + mainwindow.h \ + settings.h \ + settingsdialog.h \ + runguard.h + +FORMS += \ + aboutdlg.ui \ + mainwindow.ui \ + settingsdialog.ui + +RESOURCES = qbreak.qrc + +TRANSLATIONS = strings.ts strings_en.ts strings_ru.ts + +unix:!macx: DEFINES += TARGET_LINUX + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/app/qbreak.qrc b/app/qbreak.qrc new file mode 100644 index 0000000..64262ec --- /dev/null +++ b/app/qbreak.qrc @@ -0,0 +1,16 @@ + + + assets/images/app_icon_dark.png + assets/images/app_icon_light.png + assets/misc/qbreak.desktop + assets/images/coffee_cup/icon_16x16.png + assets/images/coffee_cup/icon_24x24.png + assets/images/coffee_cup/icon_32x32.png + assets/images/coffee_cup/icon_48x48.png + assets/images/coffee_cup/icon_64x64.png + assets/images/coffee_cup/icon_72x72.png + assets/images/coffee_cup/icon_96x96.png + assets/images/coffee_cup/icon_128x128.png + assets/images/coffee_cup/icon_512x512.png + + diff --git a/app/runguard.cpp b/app/runguard.cpp new file mode 100644 index 0000000..2be1222 --- /dev/null +++ b/app/runguard.cpp @@ -0,0 +1,79 @@ +#include "runguard.h" + +#include + +namespace +{ + +QString generateKeyHash( const QString& key, const QString& salt ) +{ + QByteArray data; + + data.append( key.toUtf8() ); + data.append( salt.toUtf8() ); + data = QCryptographicHash::hash( data, QCryptographicHash::Sha1 ).toHex(); + + return data; +} + +} + + +RunGuard::RunGuard( const QString& key ) + : key( key ) + , memLockKey( generateKeyHash( key, "_memLockKey" ) ) + , sharedmemKey( generateKeyHash( key, "_sharedmemKey" ) ) + , sharedMem( sharedmemKey ) + , memLock( memLockKey, 1 ) +{ + memLock.acquire(); + { + QSharedMemory fix( sharedmemKey ); // Fix for *nix: http://habrahabr.ru/post/173281/ + fix.attach(); + } + memLock.release(); +} + +RunGuard::~RunGuard() +{ + release(); +} + +bool RunGuard::isAnotherRunning() +{ + if ( sharedMem.isAttached() ) + return false; + + memLock.acquire(); + const bool isRunning = sharedMem.attach(); + if ( isRunning ) + sharedMem.detach(); + memLock.release(); + + return isRunning; +} + +bool RunGuard::tryToRun() +{ + if ( isAnotherRunning() ) // Extra check + return false; + + memLock.acquire(); + const bool result = sharedMem.create( sizeof( quint64 ) ); + memLock.release(); + if ( !result ) + { + release(); + return false; + } + + return true; +} + +void RunGuard::release() +{ + memLock.acquire(); + if ( sharedMem.isAttached() ) + sharedMem.detach(); + memLock.release(); +} diff --git a/app/runguard.h b/app/runguard.h new file mode 100644 index 0000000..06d45a8 --- /dev/null +++ b/app/runguard.h @@ -0,0 +1,32 @@ +#ifndef RUNGUARD_H +#define RUNGUARD_H + +#include +#include +#include + + +class RunGuard +{ + +public: + RunGuard( const QString& key ); + ~RunGuard(); + + bool isAnotherRunning(); + bool tryToRun(); + void release(); + +private: + const QString key; + const QString memLockKey; + const QString sharedmemKey; + + QSharedMemory sharedMem; + QSystemSemaphore memLock; + + Q_DISABLE_COPY( RunGuard ) +}; + + +#endif // RUNGUARD_H diff --git a/app/settings.cpp b/app/settings.cpp new file mode 100644 index 0000000..c236cf2 --- /dev/null +++ b/app/settings.cpp @@ -0,0 +1,37 @@ +#include "settings.h" + +#include + +// Key names for settings +const QString Key_LongBreak_Interval = "LongBreak_Interval"; +const QString Key_LongBreak_PostponeInterval = "LongBreak_Postpone_Interval"; +const QString Key_LongBreak_Length = "LongBreak_Length"; +const QString Key_Window_On_Top = "Window_On_Top"; +const QString Key_Verbose = "Verbose"; +const QString Key_Autostart = "Autostart"; + +void app_settings::save(const config &cfg) +{ + QSettings s; + + s.setValue(Key_LongBreak_Interval, cfg.longbreak_interval); + s.setValue(Key_LongBreak_Length, cfg.longbreak_length); + s.setValue(Key_LongBreak_PostponeInterval, cfg.longbreak_postpone_interval); + s.setValue(Key_Window_On_Top, cfg.window_on_top); + s.setValue(Key_Verbose, cfg.verbose); + s.setValue(Key_Autostart, cfg.autostart); +} + +app_settings::config app_settings::load() +{ + QSettings s; + config r; + + r.longbreak_interval = s.value(Key_LongBreak_Interval, Default_LongBreak_Interval).toInt(); + r.longbreak_length = s.value(Key_LongBreak_Length, Default_LongBreak_Length).toInt(); + r.longbreak_postpone_interval = s.value(Key_LongBreak_PostponeInterval, Default_LongBreak_PostponeInterval).toInt(); + r.window_on_top = s.value(Key_Window_On_Top, Default_Autostart).toBool(); + r.verbose = s.value(Key_Verbose, Default_Verbose).toBool(); + r.autostart = s.value(Key_Autostart, Default_Autostart).toBool(); + return r; +} diff --git a/app/settings.h b/app/settings.h new file mode 100644 index 0000000..a54a517 --- /dev/null +++ b/app/settings.h @@ -0,0 +1,44 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include + +// Default values in seconds +const int Default_LongBreak_Interval = 50 * 60; +const int Default_LongBreak_PostponeInterval = 10 * 60; +const int Default_LongBreak_Length = 10 * 60; +const int Default_Notify_Length = 30; + +// Is break window is running on the top ? +const bool Default_WindowOnTop = true; + +// Is verbose debug output +const bool Default_Verbose = false; + +// Default autostart +const bool Default_Autostart = true; + + +// Used app name +const QString AppName = "QBreak"; + +class app_settings +{ +public: + struct config + { + // Seconds + int longbreak_interval = Default_LongBreak_Interval; + int longbreak_postpone_interval = Default_LongBreak_PostponeInterval; + int longbreak_length = Default_LongBreak_Length; + bool window_on_top = Default_WindowOnTop; + bool verbose = Default_Verbose; + bool autostart = Default_Autostart; + }; + + static void save(const config& cfg); + static config load(); +}; + + +#endif // SETTINGS_H diff --git a/app/settingsdialog.cpp b/app/settingsdialog.cpp new file mode 100644 index 0000000..9073c9b --- /dev/null +++ b/app/settingsdialog.cpp @@ -0,0 +1,47 @@ +#include "settingsdialog.h" +#include "ui_settingsdialog.h" +#include "settings.h" + +#include +#include +#include + +const QString ConversionError = "Integer value expected."; + +SettingsDialog::SettingsDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::SettingsDialog) +{ + ui->setupUi(this); + init(); +} + +SettingsDialog::~SettingsDialog() +{ + delete ui; +} + +void SettingsDialog::init() +{ + setWindowTitle("Settings"); + + auto c = app_settings::load(); + ui->mAutostartCheckbox->setChecked(c.autostart); + ui->mWindowOnTopCheckbox->setChecked(c.window_on_top); + ui->mBreakIntervalEdit->setText(QString::number(c.longbreak_interval / 60)); + ui->mBreakDurationEdit->setText(QString::number(c.longbreak_length / 60)); + ui->mPostponeTimeEdit->setText(QString::number(c.longbreak_postpone_interval / 60)); +} + +void SettingsDialog::accept() +{ + auto c = app_settings::load(); + c.autostart = ui->mAutostartCheckbox->isChecked(); + c.window_on_top = ui->mWindowOnTopCheckbox->isChecked(); + c.longbreak_interval = ui->mBreakIntervalEdit->text().toInt() * 60; + c.longbreak_length = ui->mBreakDurationEdit->text().toInt() * 60; + c.longbreak_postpone_interval = ui->mPostponeTimeEdit->text().toInt() * 60; + app_settings::save(c); + + emit accepted(); +} diff --git a/app/settingsdialog.h b/app/settingsdialog.h new file mode 100644 index 0000000..11b21b6 --- /dev/null +++ b/app/settingsdialog.h @@ -0,0 +1,27 @@ +#ifndef SETTINGSDIALOG_H +#define SETTINGSDIALOG_H + +#include + +namespace Ui { +class SettingsDialog; +} + +class SettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SettingsDialog(QWidget *parent = nullptr); + ~SettingsDialog(); + +private: + Ui::SettingsDialog *ui; + + void init(); + +private slots: + void accept(); +}; + +#endif // SETTINGSDIALOG_H diff --git a/app/settingsdialog.ui b/app/settingsdialog.ui new file mode 100644 index 0000000..55508b8 --- /dev/null +++ b/app/settingsdialog.ui @@ -0,0 +1,156 @@ + + + SettingsDialog + + + + 0 + 0 + 746 + 167 + + + + + 746 + 167 + + + + + 800 + 400 + + + + Dialog + + + true + + + false + + + + + + 8 + + + 8 + + + 8 + + + 6 + + + + + Autostart + + + + + + + + + + + + + + Window on top + + + + + + + + + + + + + + Break interval (minutes) + + + + + + + + + + Break duration (minutes) + + + + + + + + + + Postpone time (minutes) + + + + + + + + + + + + Qt::Vertical + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/app/strings.ts b/app/strings.ts new file mode 100644 index 0000000..be16bb9 --- /dev/null +++ b/app/strings.ts @@ -0,0 +1,113 @@ + + + + + AboutDlg + + + Dialog + + + + + QBreak. + + + + + Version + + + + + App to make breaks in work. Thanks to the authors of similar apps which inspired me to make this one. + + + + + MainWindow + + + MainWindow + + + + + Start next break + + + + + Settings + + + + + About + + + + + Exit + + + + + Less than a minute left until the next break. + + + + + There are %1 minutes left until the next break. + + + + + New break + + + + + New break will start in %1 secs + + + + + Postpone for + + + + + SettingsDialog + + + Dialog + + + + + Autostart + + + + + Window on top + + + + + Break interval (minutes) + + + + + Break duration (minutes) + + + + + Postpone time (minutes) + + + + diff --git a/app/strings_en.ts b/app/strings_en.ts new file mode 100644 index 0000000..be16bb9 --- /dev/null +++ b/app/strings_en.ts @@ -0,0 +1,113 @@ + + + + + AboutDlg + + + Dialog + + + + + QBreak. + + + + + Version + + + + + App to make breaks in work. Thanks to the authors of similar apps which inspired me to make this one. + + + + + MainWindow + + + MainWindow + + + + + Start next break + + + + + Settings + + + + + About + + + + + Exit + + + + + Less than a minute left until the next break. + + + + + There are %1 minutes left until the next break. + + + + + New break + + + + + New break will start in %1 secs + + + + + Postpone for + + + + + SettingsDialog + + + Dialog + + + + + Autostart + + + + + Window on top + + + + + Break interval (minutes) + + + + + Break duration (minutes) + + + + + Postpone time (minutes) + + + + diff --git a/app/strings_ru.ts b/app/strings_ru.ts new file mode 100644 index 0000000..aac12c4 --- /dev/null +++ b/app/strings_ru.ts @@ -0,0 +1,125 @@ + + + + + AboutDlg + + + Dialog + О программе + + + + QBreak. + QBreak. + + + + Version + Версия + + + + App to make breaks in work. Thanks to the authors of similar apps which inspired me to make this one. + Приложение чтобы устраивать себе перерывы в работе. Спасибо авторам аналогичных приложений которые вдохновили меня сделать это приложение. + + + + MainWindow + + + MainWindow + QBreak + + + + Start next break + Начать перерыв + + + + Settings + Настройки + + + + About + О программе + + + + Exit + Выход + + + + Less than a minute left until the next break. + До перерыва осталось меньше минуты. + + + + There are %1 minutes left until the next break. + Осталось %1 минут до перерыва. + + + + New break + Скоро перерыв + + + + New break will start in %1 secs + Перерыв начнется через %1 секунд. + + + + Postpone for + Отложить на + + + + SettingsDialog + + + Dialog + Настройки + + + + Autostart + Автозапуск + + + + Window on top + Показывать поверх всех окон + + + + Break interval (minutes) + Промежуток между перерывами (в минутах) + + + + Break duration (minutes) + Длина перерывов (в минутах) + + + + Postpone time (minutes) + Насколько можно отложить перерыв (в минутах) + + + Break interval (seconds) + Промежуток между перерывами в секундах + + + Break duration (seconds) + Длина перерыва в секундах + + + Postpone time (seconds) + На сколько можно отложить перерыв (в секундах) + + + diff --git a/scripts/appimage_dir.tar.gz b/scripts/appimage_dir.tar.gz new file mode 100644 index 0000000..311bed1 Binary files /dev/null and b/scripts/appimage_dir.tar.gz differ diff --git a/scripts/build_linux.sh b/scripts/build_linux.sh new file mode 100755 index 0000000..2b32f0e --- /dev/null +++ b/scripts/build_linux.sh @@ -0,0 +1,5 @@ +#export QTDIR=/home/$USER/qt/5.12.10/gcc_64 +export QTDIR=/home/$USER/qt5/5.12.12/gcc_64 + +/usr/bin/python3 build_qbreak.py + diff --git a/scripts/build_qbreak.py b/scripts/build_qbreak.py new file mode 100755 index 0000000..4ccb1a5 --- /dev/null +++ b/scripts/build_qbreak.py @@ -0,0 +1,93 @@ +#!/usr/bin/python3 + +import typing +import platform +import os +import shutil +import glob +from pathlib import Path +import multiprocessing +import build_utils + +EXIT_OK = 0 +EXIT_ERROR = 1 + +# Check if Qt is specified +if not 'QTDIR' in os.environ: + print('Qt location must be set in QTDIR environment variable.') + exit(1) + +# Prepare build directory +build_dir = Path('build') +if build_dir.exists(): + shutil.rmtree(build_dir) +os.mkdir(build_dir) + +app_source = Path('../app').resolve() +version_suffix = build_utils.get_version(app_source / 'config.h', 'QBREAK_VERSION_SUFFIX') +version_minor = build_utils.get_version(app_source / 'config.h', 'QBREAK_VERSION_MINOR') +version_major = build_utils.get_version(app_source / 'config.h', 'QBREAK_VERSION_MAJOR') + +app_version = f'{version_major}.{version_minor}.{version_suffix}' +print (f'Found QBreak version: {app_version}') + +# Go to build directory +os.chdir(build_dir) + +if platform.system() == 'Linux': + print('Linux detected') + print('Configure...') + retcode = os.system('qmake ../../app') + if retcode != 0: + print(f'qmake call failed with code {retcode}') + exit(retcode) + + print('Build...') + retcode = os.system('make -j4') + if retcode != 0: + print(f'make call failed with code {retcode}') + exit(retcode) + + # Build appimage + print('Assembling app...') + os.chdir('..') + + # Remove possible old image + if os.path.exists('appimage_dir'): + shutil.rmtree('appimage_dir') + + # Expand image template + retcode = os.system('tar -xvzf appimage_dir.tar.gz') + if retcode != 0: + print(f'Failed to expand template directory, code {retcode}') + exit(retcode) + + # Copy binary file + shutil.copy('build/qbreak', 'appimage_dir/usr/bin') + + deploy_options = [ + '-always-overwrite', + '-verbose=2', + '-appimage', + '-qmake=' + os.environ['QTDIR'] + '/bin/qmake', + '-unsupported-allow-new-glibc', + #'-no-translations', + '-extra-plugins=iconengines,platformthemes/libqgtk3.so' + ] + + desktop_path = 'appimage_dir/usr/share/applications/qbreak.desktop' + cmd_deploy = f'./linuxdeployqt {desktop_path} {" ".join(deploy_options)}' + retcode = os.system(cmd_deploy) + if retcode != 0: + print(f'linuxdeployqt failed with code {retcode}') + print(cmd_deploy) + exit(retcode) + + releases_dir = Path('releases') + if not releases_dir.exists(): + os.mkdir(releases_dir) + for f in os.listdir(): + if f.endswith('x86_64.AppImage') and f.startswith('QBreak'): + shutil.move(f, releases_dir / f'qbreak-{app_version}-x86_64.AppImage') + + exit(0) \ No newline at end of file diff --git a/scripts/build_utils.py b/scripts/build_utils.py new file mode 100644 index 0000000..00f317b --- /dev/null +++ b/scripts/build_utils.py @@ -0,0 +1,170 @@ +#!/usr/bin/python3 + +import os +import sys +import multiprocessing +import shutil +import platform +import re +import glob +import typing +import codecs + +# from PyInquirer import prompt, print_json + + +AR_TEMP_DIR = "ar_temp_dir" + +def show_menu(message, lst): + print() + # Print selection menu + for l in range(len(lst)): + print(f'{l}) {lst[l]}') + + return lst[int(input(message))] + + +def ask_password(message): + return input(message) + + +# Replace substring in a file +def inplace_change(filename, old_string, new_string): + # Safely read the input filename using 'with' + with open(filename) as f: + s = f.read() + if old_string not in s: + print('"{old_string}" not found in {filename}.'.format(**locals())) + return + + # Safely write the changed content, if found in the file + with open(filename, 'w') as f: + print('Changing "{old_string}" to "{new_string}" in {filename}'.format(**locals())) + s = s.replace(old_string, new_string) + f.write(s) + + +# Architecture used for VS2019 architecture parameters +def make_build(path: str, profile: str, params: dict, merge_libs :bool, generator: str=None, architecture: str = None): + cmdline = "cmake " + if generator is not None: + cmdline += f'-G "{generator}"' + if architecture is not None: + cmdline += f' -A {architecture}' + + for key in params: + cmdline += f" -D {key}={params[key]}" + + cmdline += f" -D CMAKE_BUILD_TYPE={profile}" + cmdline += f" {path}" + print(f"Cmake configure call: {cmdline}") + + retcode = os.system(cmdline) + + if retcode != 0: + return retcode + + if platform.system() == "Windows": + cmdline = "cmake --build . --config %s -j%d" % (profile, multiprocessing.cpu_count()) + else: + cmdline = f"cmake --build . -j{multiprocessing.cpu_count()}" + + retcode = os.system(cmdline) + if retcode != 0: + return retcode + + return 0 + + +def get_version(path, name): + with codecs.open(path, mode='r', encoding='utf-8') as f: + t = f.read() + + pattern = r"#define " + name + r" (?P[\d]+)" + m = re.search(pattern=pattern, string=t, flags=re.MULTILINE) + if m is not None: + return m.group("number") + else: + return None + + +def make_chdir(path): + # Prepare build directory + if os.path.exists(path): + shutil.rmtree(path) + os.mkdir(path) + os.chdir(path) + + +def upload_to_builds(path): + # Build command line + if platform.system().lower() == "windows": + cmdline = f"pscp.exe {path} uploader@f.sevana.biz:/var/www/f.sevana.biz/public_html/" + else: + cmdline = f"scp -o \"StrictHostKeyChecking no\" {path} uploader@f.sevana.biz:/var/www/f.sevana.biz/public_html/" + + # Upload + retcode = os.system(cmdline) + + # Print results + if retcode == 0: + print (f"https://f.sevana.biz/{os.path.basename(path)}") + else: + print ("Failed to upload. Return code: %d" % retcode) + + +def merge_static_libs(main_lib, build_dir, use_android_ndk: bool): + # Find all static libraries + print ("Looking for additional static libraries to merge...") + files = glob.glob("**/*.a", recursive=True) + print (files) + + if platform.system() in ["Linux", "Darwin"]: + print ("Merging with binutils...") + + if use_android_ndk: + if platform.system() == "Darwin": + toolchain_prefix = "darwin-x86_64" + else: + toolchain_prefix = "linux-x86_64" + + ar_command = f"{path_android_ndk}/toolchains/llvm/prebuilt/{toolchain_prefix}/bin/aarch64-linux-android-ar" + else: + ar_command = "ar" + + # Extract all object files + os.system("mkdir %s" % AR_TEMP_DIR) + os.chdir(AR_TEMP_DIR) + for libname in files: + cmdline_extract = f"{ar_command} -x {os.path.join('..', libname)}" + retcode = os.system(cmdline_extract) + if retcode != 0: + print(f"Failed: {cmdline_extract}") + sys.exit(1) + + # Extract object files from main library + cmdline_extract = f"{ar_command} -x {os.path.join('..', main_lib)}" + retcode = os.system(cmdline_extract) + if retcode != 0: + print(f"Failed: {cmdline_extract}") + sys.exit(1) + + cmdline = f"{ar_command} -qc {os.path.basename(main_lib)} *.o" + retcode = os.system(cmdline) + if retcode != 0: + print(f"Failed: {cmdline}") + sys.exit(1) + + # Replace original library with a new one + os.chdir("..") + os.remove(main_lib) + shutil.move(os.path.join(AR_TEMP_DIR, os.path.basename(main_lib)), main_lib) + # os.system("strip %s" % main_lib) + shutil.rmtree(AR_TEMP_DIR) + +if 'ANDROID_NDK_HOME' in os.environ: + path_android_ndk = os.environ['ANDROID_NDK_HOME'] +else: + path_android_ndk = None + +# "~/Library/Android/sdk/ndk/21.0.6113669" diff --git a/scripts/linuxdeployqt b/scripts/linuxdeployqt new file mode 100755 index 0000000..2d65297 Binary files /dev/null and b/scripts/linuxdeployqt differ