- initial import

This commit is contained in:
Dmytro Bogovych 2022-04-17 15:37:04 +03:00
commit 8745fbe6a0
48 changed files with 2132 additions and 0 deletions

13
README.md Normal file
View File

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

21
app/aboutdlg.cpp Normal file
View File

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

22
app/aboutdlg.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef ABOUTDLG_H
#define ABOUTDLG_H
#include <QDialog>
namespace Ui {
class AboutDlg;
}
class AboutDlg : public QDialog
{
Q_OBJECT
public:
explicit AboutDlg(QWidget *parent = nullptr);
~AboutDlg();
private:
Ui::AboutDlg *ui;
};
#endif // ABOUTDLG_H

133
app/aboutdlg.ui Normal file
View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AboutDlg</class>
<widget class="QDialog" name="AboutDlg">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="mAppNameLabel">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>QBreak.</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="mAppVersionLabel">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>Version</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="mAppInfoLabel">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="text">
<string>App to make breaks in work. Thanks to the authors of similar apps which inspired me to make this one.</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AboutDlg</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AboutDlg</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,3 @@
URL for used image:
https://thenounproject.com/icon/coffee-break-537907/

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m228 72c-9.332 0-14.488 4.707-18.375 9.375-5.1367 6.5391-7.332 15.398-4.125 23.25 18.855 47.793 10.898 59.164 0.375 85.5s-22.758 65.922-0.375 129.75c3.3164 12.883 18.977 20.988 31.5 16.5s19.121-20.566 13.5-32.625c-19.188-54.711-10.637-70.312-0.375-96s23.09-63.176 0.375-120.75c-3.4375-9.0625-12.848-14.941-22.5-15zm324 0c-9.332 0-14.488 4.707-18.375 9.375-5.1367 6.5391-7.332 15.398-4.125 23.25 18.855 47.793 10.898 59.164 0.375 85.5s-22.758 65.922-0.375 129.75c3.3164 12.883 18.977 20.988 31.5 16.5s19.121-20.566 13.5-32.625c-19.188-54.711-10.637-70.312-0.375-96s23.09-63.176 0.375-120.75c-3.4375-9.0625-12.848-14.941-22.5-15zm324 0c-9.332 0-14.488 4.707-18.375 9.375-5.1367 6.5391-7.332 15.398-4.125 23.25 18.855 47.793 10.898 59.164 0.375 85.5s-22.758 65.922-0.375 129.75c3.3164 12.883 18.977 20.988 31.5 16.5s19.121-20.566 13.5-32.625c-19.188-54.711-10.637-70.312-0.375-96s23.09-63.176 0.375-120.75c-3.4375-9.0625-12.848-14.941-22.5-15zm-782.25 360c-11.797 1.1133-21.801 12.148-21.75 24 0 304.82 77.43 453.77 174.75 624h-126.75c-13.297 0-24 10.703-24 24s10.703 24 24 24h864c13.297 0 24-10.703 24-24s-10.703-24-24-24h-126.75c25.375-44.387 49.617-87.379 71.25-132h115.5c46.531 0 84-37.469 84-84v-264c0-46.531-37.465-84-84-84h-13.125c0.69922-19.352 1.125-39.258 1.125-60 0-12.566-11.434-24-24-24h-912c-0.75-0.035156-1.5-0.035156-2.25 0zm27 48h862.5c-4.168 292.51-80.758 424.66-181.5 600h-499.5c-100.74-175.34-177.33-307.49-181.5-600zm907.5 84h15.75c24.383 0 36 11.621 36 36v264c0 24.383-11.617 36-36 36h-93.75c39.703-92.473 68.336-195.91 78-336z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,3 @@
URL: https://ru.seaicons.com/89980/
License: https://creativecommons.org/licenses/by-sa/4.0/

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m228 72c-9.332 0-14.488 4.707-18.375 9.375-5.1367 6.5391-7.332 15.398-4.125 23.25 18.855 47.793 10.898 59.164 0.375 85.5s-22.758 65.922-0.375 129.75c3.3164 12.883 18.977 20.988 31.5 16.5s19.121-20.566 13.5-32.625c-19.188-54.711-10.637-70.312-0.375-96s23.09-63.176 0.375-120.75c-3.4375-9.0625-12.848-14.941-22.5-15zm324 0c-9.332 0-14.488 4.707-18.375 9.375-5.1367 6.5391-7.332 15.398-4.125 23.25 18.855 47.793 10.898 59.164 0.375 85.5s-22.758 65.922-0.375 129.75c3.3164 12.883 18.977 20.988 31.5 16.5s19.121-20.566 13.5-32.625c-19.188-54.711-10.637-70.312-0.375-96s23.09-63.176 0.375-120.75c-3.4375-9.0625-12.848-14.941-22.5-15zm324 0c-9.332 0-14.488 4.707-18.375 9.375-5.1367 6.5391-7.332 15.398-4.125 23.25 18.855 47.793 10.898 59.164 0.375 85.5s-22.758 65.922-0.375 129.75c3.3164 12.883 18.977 20.988 31.5 16.5s19.121-20.566 13.5-32.625c-19.188-54.711-10.637-70.312-0.375-96s23.09-63.176 0.375-120.75c-3.4375-9.0625-12.848-14.941-22.5-15zm-782.25 360c-11.797 1.1133-21.801 12.148-21.75 24 0 304.82 77.43 453.77 174.75 624h-126.75c-13.297 0-24 10.703-24 24s10.703 24 24 24h864c13.297 0 24-10.703 24-24s-10.703-24-24-24h-126.75c25.375-44.387 49.617-87.379 71.25-132h115.5c46.531 0 84-37.469 84-84v-264c0-46.531-37.465-84-84-84h-13.125c0.69922-19.352 1.125-39.258 1.125-60 0-12.566-11.434-24-24-24h-912c-0.75-0.035156-1.5-0.035156-2.25 0zm27 48h862.5c-4.168 292.51-80.758 424.66-181.5 600h-499.5c-100.74-175.34-177.33-307.49-181.5-600zm907.5 84h15.75c24.383 0 36 11.621 36 36v264c0 24.383-11.617 36-36 36h-93.75c39.703-92.473 68.336-195.91 78-336z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

140
app/autostart.cpp Normal file
View File

@ -0,0 +1,140 @@
#include "autostart.h"
#include <QDebug>
#if defined(TARGET_LINUX)
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <fstream>
#include <filesystem>
#include <QFile>
#include <QSettings>
#include <QFileInfo>
#include <QDir>
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<std::string> 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<std::string>(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

22
app/autostart.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef AUTOSTART_H
#define AUTOSTART_H
#include <string>
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

9
app/config.h Normal file
View File

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

57
app/main.cpp Normal file
View File

@ -0,0 +1,57 @@
#include "mainwindow.h"
#include "autostart.h"
#include "runguard.h"
#include <QApplication>
#include <QCommandLineParser>
#include <QTranslator>
#include <QFileInfo>
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;
}

379
app/mainwindow.cpp Normal file
View File

@ -0,0 +1,379 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "settingsdialog.h"
#include "settings.h"
#include "autostart.h"
#include "aboutdlg.h"
#include <QMenu>
#include <QAction>
#include <QSettings>
#include <QDebug>
#include <QDesktopWidget>
#include <QSvgGenerator>
#include <QPalette>
#include <QScreen>
#include <QWindow>
#include <QFileInfo>
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<std::chrono::minutes>(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<std::chrono::seconds>(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();
}

67
app/mainwindow.h Normal file
View File

@ -0,0 +1,67 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTimer>
#include <QSystemTrayIcon>
#include <chrono>
#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

143
app/mainwindow.ui Normal file
View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>75</height>
</size>
</property>
</spacer>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<spacer name="verticalSpacer_6">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QProgressBar" name="mProgressBar">
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="mRemainingLabel">
<property name="text">
<string notr="true">Remaining:</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QPushButton" name="mPostponeButton">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>100</height>
</size>
</property>
<property name="text">
<string notr="true">Postpone for 10 minutes</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

43
app/qbreak.pro Normal file
View File

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

16
app/qbreak.qrc Normal file
View File

@ -0,0 +1,16 @@
<RCC>
<qresource prefix="/">
<file>assets/images/app_icon_dark.png</file>
<file>assets/images/app_icon_light.png</file>
<file>assets/misc/qbreak.desktop</file>
<file>assets/images/coffee_cup/icon_16x16.png</file>
<file>assets/images/coffee_cup/icon_24x24.png</file>
<file>assets/images/coffee_cup/icon_32x32.png</file>
<file>assets/images/coffee_cup/icon_48x48.png</file>
<file>assets/images/coffee_cup/icon_64x64.png</file>
<file>assets/images/coffee_cup/icon_72x72.png</file>
<file>assets/images/coffee_cup/icon_96x96.png</file>
<file>assets/images/coffee_cup/icon_128x128.png</file>
<file>assets/images/coffee_cup/icon_512x512.png</file>
</qresource>
</RCC>

79
app/runguard.cpp Normal file
View File

@ -0,0 +1,79 @@
#include "runguard.h"
#include <QCryptographicHash>
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();
}

32
app/runguard.h Normal file
View File

@ -0,0 +1,32 @@
#ifndef RUNGUARD_H
#define RUNGUARD_H
#include <QObject>
#include <QSharedMemory>
#include <QSystemSemaphore>
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

37
app/settings.cpp Normal file
View File

@ -0,0 +1,37 @@
#include "settings.h"
#include <QSettings>
// 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;
}

44
app/settings.h Normal file
View File

@ -0,0 +1,44 @@
#ifndef SETTINGS_H
#define SETTINGS_H
#include <QString>
// 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

47
app/settingsdialog.cpp Normal file
View File

@ -0,0 +1,47 @@
#include "settingsdialog.h"
#include "ui_settingsdialog.h"
#include "settings.h"
#include <QToolTip>
#include <QTimer>
#include <QWindow>
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();
}

27
app/settingsdialog.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QDialog>
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

156
app/settingsdialog.ui Normal file
View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>746</width>
<height>167</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>746</width>
<height>167</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>800</width>
<height>400</height>
</size>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<property name="leftMargin">
<number>8</number>
</property>
<property name="topMargin">
<number>8</number>
</property>
<property name="rightMargin">
<number>8</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Autostart</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="mAutostartCheckbox">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Window on top</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="mWindowOnTopCheckbox">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Break interval (minutes)</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="mBreakIntervalEdit"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Break duration (minutes)</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="mBreakDurationEdit"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Postpone time (minutes)</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="mPostponeTimeEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

113
app/strings.ts Normal file
View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US" sourcelanguage="en">
<context>
<name>AboutDlg</name>
<message>
<location filename="aboutdlg.ui" line="20"/>
<source>Dialog</source>
<translation></translation>
</message>
<message>
<location filename="aboutdlg.ui" line="32"/>
<source>QBreak.</source>
<translation></translation>
</message>
<message>
<location filename="aboutdlg.ui" line="51"/>
<source>Version</source>
<translation></translation>
</message>
<message>
<location filename="aboutdlg.ui" line="70"/>
<source>App to make breaks in work. Thanks to the authors of similar apps which inspired me to make this one.</source>
<translation></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="mainwindow.ui" line="14"/>
<source>MainWindow</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="213"/>
<source>Start next break</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="216"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="219"/>
<source>About</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="222"/>
<source>Exit</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="250"/>
<source>Less than a minute left until the next break.</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="252"/>
<source>There are %1 minutes left until the next break.</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="259"/>
<source>New break</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="260"/>
<source>New break will start in %1 secs</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="269"/>
<source>Postpone for </source>
<translation></translation>
</message>
</context>
<context>
<name>SettingsDialog</name>
<message>
<location filename="settingsdialog.ui" line="26"/>
<source>Dialog</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="52"/>
<source>Autostart</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="66"/>
<source>Window on top</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="80"/>
<source>Break interval (minutes)</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="90"/>
<source>Break duration (minutes)</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="100"/>
<source>Postpone time (minutes)</source>
<translation></translation>
</message>
</context>
</TS>

113
app/strings_en.ts Normal file
View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US" sourcelanguage="en">
<context>
<name>AboutDlg</name>
<message>
<location filename="aboutdlg.ui" line="20"/>
<source>Dialog</source>
<translation></translation>
</message>
<message>
<location filename="aboutdlg.ui" line="32"/>
<source>QBreak.</source>
<translation></translation>
</message>
<message>
<location filename="aboutdlg.ui" line="51"/>
<source>Version</source>
<translation></translation>
</message>
<message>
<location filename="aboutdlg.ui" line="70"/>
<source>App to make breaks in work. Thanks to the authors of similar apps which inspired me to make this one.</source>
<translation></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="mainwindow.ui" line="14"/>
<source>MainWindow</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="213"/>
<source>Start next break</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="216"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="219"/>
<source>About</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="222"/>
<source>Exit</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="250"/>
<source>Less than a minute left until the next break.</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="252"/>
<source>There are %1 minutes left until the next break.</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="259"/>
<source>New break</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="260"/>
<source>New break will start in %1 secs</source>
<translation></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="269"/>
<source>Postpone for </source>
<translation></translation>
</message>
</context>
<context>
<name>SettingsDialog</name>
<message>
<location filename="settingsdialog.ui" line="26"/>
<source>Dialog</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="52"/>
<source>Autostart</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="66"/>
<source>Window on top</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="80"/>
<source>Break interval (minutes)</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="90"/>
<source>Break duration (minutes)</source>
<translation></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="100"/>
<source>Postpone time (minutes)</source>
<translation></translation>
</message>
</context>
</TS>

125
app/strings_ru.ts Normal file
View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="ru" sourcelanguage="en">
<context>
<name>AboutDlg</name>
<message>
<location filename="aboutdlg.ui" line="20"/>
<source>Dialog</source>
<translation>О программе</translation>
</message>
<message>
<location filename="aboutdlg.ui" line="32"/>
<source>QBreak.</source>
<translation>QBreak.</translation>
</message>
<message>
<location filename="aboutdlg.ui" line="51"/>
<source>Version</source>
<translation>Версия</translation>
</message>
<message>
<location filename="aboutdlg.ui" line="70"/>
<source>App to make breaks in work. Thanks to the authors of similar apps which inspired me to make this one.</source>
<translation>Приложение чтобы устраивать себе перерывы в работе. Спасибо авторам аналогичных приложений которые вдохновили меня сделать это приложение.</translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="mainwindow.ui" line="14"/>
<source>MainWindow</source>
<translation>QBreak</translation>
</message>
<message>
<location filename="mainwindow.cpp" line="213"/>
<source>Start next break</source>
<translation>Начать перерыв</translation>
</message>
<message>
<location filename="mainwindow.cpp" line="216"/>
<source>Settings</source>
<translation>Настройки</translation>
</message>
<message>
<location filename="mainwindow.cpp" line="219"/>
<source>About</source>
<translation>О программе</translation>
</message>
<message>
<location filename="mainwindow.cpp" line="222"/>
<source>Exit</source>
<translation>Выход</translation>
</message>
<message>
<location filename="mainwindow.cpp" line="250"/>
<source>Less than a minute left until the next break.</source>
<translation>До перерыва осталось меньше минуты.</translation>
</message>
<message>
<location filename="mainwindow.cpp" line="252"/>
<source>There are %1 minutes left until the next break.</source>
<translation>Осталось %1 минут до перерыва.</translation>
</message>
<message>
<location filename="mainwindow.cpp" line="259"/>
<source>New break</source>
<translation>Скоро перерыв</translation>
</message>
<message>
<location filename="mainwindow.cpp" line="260"/>
<source>New break will start in %1 secs</source>
<translation>Перерыв начнется через %1 секунд.</translation>
</message>
<message>
<location filename="mainwindow.cpp" line="269"/>
<source>Postpone for </source>
<translation>Отложить на</translation>
</message>
</context>
<context>
<name>SettingsDialog</name>
<message>
<location filename="settingsdialog.ui" line="26"/>
<source>Dialog</source>
<translation>Настройки</translation>
</message>
<message>
<location filename="settingsdialog.ui" line="52"/>
<source>Autostart</source>
<translation>Автозапуск</translation>
</message>
<message>
<location filename="settingsdialog.ui" line="66"/>
<source>Window on top</source>
<translation>Показывать поверх всех окон</translation>
</message>
<message>
<location filename="settingsdialog.ui" line="80"/>
<source>Break interval (minutes)</source>
<translation>Промежуток между перерывами (в минутах)</translation>
</message>
<message>
<location filename="settingsdialog.ui" line="90"/>
<source>Break duration (minutes)</source>
<translation>Длина перерывов (в минутах)</translation>
</message>
<message>
<location filename="settingsdialog.ui" line="100"/>
<source>Postpone time (minutes)</source>
<translation>Насколько можно отложить перерыв (в минутах)</translation>
</message>
<message>
<source>Break interval (seconds)</source>
<translation type="vanished">Промежуток между перерывами в секундах</translation>
</message>
<message>
<source>Break duration (seconds)</source>
<translation type="vanished">Длина перерыва в секундах</translation>
</message>
<message>
<source>Postpone time (seconds)</source>
<translation type="vanished">На сколько можно отложить перерыв (в секундах)</translation>
</message>
</context>
</TS>

BIN
scripts/appimage_dir.tar.gz Normal file

Binary file not shown.

5
scripts/build_linux.sh Executable file
View File

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

93
scripts/build_qbreak.py Executable file
View File

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

170
scripts/build_utils.py Normal file
View File

@ -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<number>[\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"

BIN
scripts/linuxdeployqt Executable file

Binary file not shown.