├── docs ├── _config.yml ├── api_password.gif ├── kemai_settings.gif └── screenshots │ ├── windows_running.jpg │ ├── windows_settings.jpg │ └── windows_add_project.jpg ├── bundle ├── windows │ ├── kemai.rc │ └── kemai.ico ├── macos │ ├── kemai.icns │ ├── Info.plist │ └── create_dmg.sh └── linux │ ├── sysroot │ ├── kemai-wrapper.png │ ├── kemai-wrapper.desktop │ └── usr │ │ └── bin │ │ └── kemai-wrapper.sh │ └── create_appimage.sh ├── src ├── resources │ ├── icons │ │ ├── kimai.png │ │ ├── kimai-red.png │ │ ├── play.svg │ │ ├── add.svg │ │ ├── warning.svg │ │ ├── remove.svg │ │ ├── backspace.svg │ │ ├── stop.svg │ │ ├── update.svg │ │ ├── visibility.svg │ │ ├── refresh.svg │ │ ├── update-off.svg │ │ └── visibility-off.svg │ ├── misc │ │ └── licences.html │ ├── kemai.qrc │ └── data │ │ ├── iso3166-alpha2.json │ │ └── iso4217.json ├── kemaiConfig.h.in ├── misc │ ├── dataReader.h │ ├── helpers.h │ ├── customFmt.h │ ├── dataReader.cpp │ └── helpers.cpp ├── client │ ├── kimaiAPI.cpp │ ├── kimaiFeatures.h │ ├── kimaiFeatures.cpp │ ├── kimaiReply.cpp │ ├── kimaiCache.h │ ├── parser.h │ ├── kimaiReply.h │ ├── kimaiClient.h │ ├── kimaiAPI.h │ ├── kimaiClient_p.h │ └── kimaiCache.cpp ├── models │ ├── taskFilterProxyModel.h │ ├── taskListModel.h │ ├── taskFilterProxyModel.cpp │ ├── kimaiDataListModel.cpp │ ├── kimaiDataSortFilterProxyModel.h │ ├── kimaiDataSortFilterProxyModel.cpp │ ├── kimaiDataListModel.h │ ├── taskListModel.cpp │ ├── loggerTreeModel.h │ └── loggerTreeModel.cpp ├── gui │ ├── aboutDialog.h │ ├── aboutDialog.cpp │ ├── durationEdit.h │ ├── activityDialog.h │ ├── projectDialog.h │ ├── loggerWidget.h │ ├── customerDialog.h │ ├── timeSheetListWidgetItem.h │ ├── loggerWidget.cpp │ ├── CMakeLists.txt │ ├── taskWidget.h │ ├── timeSheetListWidgetItem.cpp │ ├── durationEdit.cpp │ ├── settingsDialog.h │ ├── projectDialog.cpp │ ├── activityDialog.cpp │ ├── loggerWidget.ui │ ├── mainWindow.ui │ ├── autoCompleteComboBox.h │ ├── autoCompleteComboBox.cpp │ ├── activityWidget.h │ ├── customerDialog.cpp │ ├── taskWidget.ui │ ├── mainWindow.h │ ├── taskWidget.cpp │ ├── projectDialog.ui │ ├── activityDialog.ui │ ├── aboutDialog.ui │ ├── customerDialog.ui │ └── timeSheetListWidgetItem.ui ├── monitor │ ├── macDesktopEventsMonitor.h │ ├── linuxDesktopEventsMonitor.h │ ├── desktopEventsMonitor.h │ ├── kimaiEventsMonitor.h │ ├── windowsDesktopEventsMonitor.h │ ├── desktopEventsMonitor.cpp │ ├── macDesktopEventsMonitor.mm │ ├── linuxDesktopEventsMonitor.cpp │ ├── windowsDesktopEventsMonitor.cpp │ └── kimaiEventsMonitor.cpp ├── updater │ ├── kemaiUpdater_p.h │ ├── kemaiUpdater.h │ └── kemaiUpdater.cpp ├── settings │ ├── settings.h │ └── settings.cpp ├── context │ ├── kemaiSession.h │ └── kemaiSession.cpp ├── main.cpp └── CMakeLists.txt ├── share └── kemai.desktop ├── .clang-tidy ├── cmake ├── LocalizationTools.cmake ├── WinDeployOpenSSL.cmake └── WinDeployQt.cmake ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── CONTRIBUTING.md ├── CMakePresets.json ├── .clang-format ├── CMakeLists.txt └── CHANGELOG.md /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /bundle/windows/kemai.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON "kemai.ico" 2 | -------------------------------------------------------------------------------- /docs/api_password.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/docs/api_password.gif -------------------------------------------------------------------------------- /bundle/macos/kemai.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/bundle/macos/kemai.icns -------------------------------------------------------------------------------- /bundle/windows/kemai.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/bundle/windows/kemai.ico -------------------------------------------------------------------------------- /docs/kemai_settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/docs/kemai_settings.gif -------------------------------------------------------------------------------- /src/resources/icons/kimai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/src/resources/icons/kimai.png -------------------------------------------------------------------------------- /src/resources/icons/kimai-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/src/resources/icons/kimai-red.png -------------------------------------------------------------------------------- /docs/screenshots/windows_running.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/docs/screenshots/windows_running.jpg -------------------------------------------------------------------------------- /docs/screenshots/windows_settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/docs/screenshots/windows_settings.jpg -------------------------------------------------------------------------------- /bundle/linux/sysroot/kemai-wrapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/bundle/linux/sysroot/kemai-wrapper.png -------------------------------------------------------------------------------- /docs/screenshots/windows_add_project.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandrePTJ/kemai/HEAD/docs/screenshots/windows_add_project.jpg -------------------------------------------------------------------------------- /src/kemaiConfig.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define KEMAI_VERSION "@KemaiProject_VERSION@" 4 | #cmakedefine KEMAI_ENABLE_UPDATE_CHECK 5 | -------------------------------------------------------------------------------- /bundle/linux/sysroot/kemai-wrapper.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Kemai 4 | Exec=kemai-wrapper.sh 5 | Icon=kemai-wrapper 6 | Categories= -------------------------------------------------------------------------------- /share/kemai.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Kemai 4 | Comment=Kimai timetracking frontend 5 | TryExec=Kemai 6 | Exec=Kemai 7 | Terminal=false 8 | Categories=Office;Utility; 9 | Icon=kimai 10 | -------------------------------------------------------------------------------- /src/misc/dataReader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace kemai { 6 | 7 | class DataReader 8 | { 9 | public: 10 | static QMap countries(); 11 | static QMap currencies(); 12 | }; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/resources/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/resources/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/resources/icons/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/misc/helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace kemai::helpers { 7 | 8 | QString getDurationString(const QDateTime& beginAt, const QDateTime& endAt); 9 | 10 | QString getLogDirPath(); 11 | 12 | QString getLogFilePath(); 13 | 14 | } // namespace kemai::helpers 15 | -------------------------------------------------------------------------------- /src/client/kimaiAPI.cpp: -------------------------------------------------------------------------------- 1 | #include "kimaiAPI.h" 2 | 3 | namespace kemai { 4 | 5 | ApiPlugin pluginByName(const QString& pluginName) 6 | { 7 | if (pluginName == "TaskManagementBundle") 8 | { 9 | return ApiPlugin::TaskManagement; 10 | } 11 | return ApiPlugin::Unknown; 12 | } 13 | 14 | } // namespace kemai 15 | -------------------------------------------------------------------------------- /src/resources/icons/remove.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/resources/icons/backspace.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/resources/icons/stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/misc/customFmt.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | template<> struct fmt::formatter 8 | { 9 | constexpr auto parse(format_parse_context& ctx) { return ctx.end(); } 10 | 11 | template auto format(const QString& str, FormatContext& ctx) const { return fmt::format_to(ctx.out(), "{}", str.toStdString()); } 12 | }; 13 | -------------------------------------------------------------------------------- /src/models/taskFilterProxyModel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace kemai { 6 | 7 | class TaskFilterProxyModel : public QSortFilterProxyModel 8 | { 9 | public: 10 | void setUserId(int userId); 11 | 12 | protected: 13 | bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; 14 | 15 | private: 16 | int mUserId = 0; 17 | }; 18 | 19 | } // namespace kemai 20 | -------------------------------------------------------------------------------- /src/resources/icons/update.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gui/aboutDialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace Ui { 8 | class AboutDialog; 9 | } 10 | 11 | namespace kemai { 12 | 13 | class AboutDialog : public QDialog 14 | { 15 | Q_OBJECT 16 | 17 | public: 18 | AboutDialog(QWidget* parent = nullptr); 19 | ~AboutDialog() override; 20 | 21 | private: 22 | std::unique_ptr mUi; 23 | }; 24 | 25 | } // namespace kemai 26 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: > 2 | -*, 3 | bugprone-*, 4 | modernize-*, 5 | -modernize-pass-by-value, 6 | -modernize-use-nodiscard, 7 | -modernize-use-trailing-return-type, 8 | -modernize-redundant-void-arg, 9 | performance-*, 10 | readability-*, 11 | -readability-convert-member-functions-to-static, 12 | -readability-qualified-auto, 13 | -readability-identifier-length 14 | WarningsAsErrors: '' 15 | HeaderFilterRegex: '' 16 | FormatStyle: none 17 | -------------------------------------------------------------------------------- /src/gui/aboutDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "aboutDialog.h" 2 | #include "ui_aboutDialog.h" 3 | 4 | #include "kemaiConfig.h" 5 | 6 | using namespace kemai; 7 | 8 | AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), mUi(std::make_unique()) 9 | { 10 | mUi->setupUi(this); 11 | mUi->versionLabel->setText(KEMAI_VERSION); 12 | mUi->licencesTextBrowser->setSource({"qrc:/misc/licences"}); 13 | } 14 | 15 | AboutDialog::~AboutDialog() = default; 16 | -------------------------------------------------------------------------------- /src/resources/icons/visibility.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/resources/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gui/durationEdit.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace kemai { 7 | 8 | class DurationEdit : public QLineEdit 9 | { 10 | Q_OBJECT 11 | Q_PROPERTY(int seconds READ seconds WRITE setSeconds) 12 | 13 | public: 14 | DurationEdit(QWidget* parent = nullptr); 15 | ~DurationEdit() override = default; 16 | 17 | void setSeconds(int seconds); 18 | int seconds() const; 19 | 20 | private: 21 | const QRegularExpression mDurationRx; 22 | }; 23 | 24 | } // namespace kemai 25 | -------------------------------------------------------------------------------- /src/client/kimaiFeatures.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace kemai { 6 | 7 | class KimaiFeatures 8 | { 9 | public: 10 | // Allow current client instance to get instance version and list of available plugins. Only available from Kimai 1.14.1 11 | static bool canRequestPlugins(const QVersionNumber& kimaiVersion); 12 | 13 | // Starting from 2.14, auth/token headers are deprecated. Should use API Token with Authorization header. 14 | static bool shouldUseAPIToken(const QVersionNumber& kimaiVersion); 15 | }; 16 | 17 | } // namespace kemai 18 | -------------------------------------------------------------------------------- /src/gui/activityDialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "client/kimaiClient.h" 6 | 7 | namespace Ui { 8 | class ActivityDialog; 9 | } 10 | 11 | namespace kemai { 12 | 13 | class ActivityDialog : public QDialog 14 | { 15 | Q_OBJECT 16 | 17 | public: 18 | ActivityDialog(QWidget* parent = nullptr); 19 | ~ActivityDialog() override; 20 | 21 | Activity activity() const; 22 | 23 | private: 24 | void enableSave(bool enable); 25 | void validateForm(); 26 | 27 | std::unique_ptr mUi; 28 | }; 29 | 30 | } // namespace kemai 31 | -------------------------------------------------------------------------------- /src/gui/projectDialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "client/kimaiClient.h" 6 | 7 | namespace Ui { 8 | class ProjectDialog; 9 | } 10 | 11 | namespace kemai { 12 | 13 | class ProjectDialog : public QDialog 14 | { 15 | Q_OBJECT 16 | 17 | public: 18 | ProjectDialog(QWidget* parent = nullptr); 19 | ~ProjectDialog() override; 20 | 21 | Project project() const; 22 | 23 | private: 24 | void enableSave(bool enable); 25 | void validateForm(); 26 | 27 | private: 28 | std::unique_ptr mUi; 29 | }; 30 | 31 | } // namespace kemai 32 | -------------------------------------------------------------------------------- /src/gui/loggerWidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | 9 | namespace Ui { 10 | class LoggerWidget; 11 | } 12 | 13 | namespace kemai { 14 | 15 | class LoggerWidget : public QWidget 16 | { 17 | public: 18 | LoggerWidget(QWidget* parent = nullptr); 19 | ~LoggerWidget() override; 20 | 21 | void setModel(const std::shared_ptr& model); 22 | 23 | private: 24 | std::unique_ptr mUi; 25 | std::shared_ptr mLoggerModel; 26 | }; 27 | 28 | } // namespace kemai 29 | -------------------------------------------------------------------------------- /src/gui/customerDialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "client/kimaiAPI.h" 8 | 9 | namespace Ui { 10 | class CustomerDialog; 11 | } 12 | 13 | namespace kemai { 14 | 15 | class CustomerDialog : public QDialog 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | CustomerDialog(QWidget* parent = nullptr); 21 | ~CustomerDialog() override; 22 | 23 | Customer customer() const; 24 | 25 | private: 26 | void enableSave(bool enable); 27 | void validateForm(); 28 | 29 | std::unique_ptr mUi; 30 | }; 31 | 32 | } // namespace kemai 33 | -------------------------------------------------------------------------------- /src/client/kimaiFeatures.cpp: -------------------------------------------------------------------------------- 1 | #include "kimaiFeatures.h" 2 | 3 | using namespace kemai; 4 | 5 | /* 6 | * Static helpers 7 | */ 8 | static const auto gKimaiVersionForPluginRequest = QVersionNumber(1, 14, 1); 9 | static const auto gKimaiVersionForAPIToken = QVersionNumber(2, 14, 0); 10 | 11 | /* 12 | * Class impl 13 | */ 14 | bool KimaiFeatures::canRequestPlugins(const QVersionNumber& kimaiVersion) 15 | { 16 | return kimaiVersion >= gKimaiVersionForPluginRequest; 17 | } 18 | 19 | bool KimaiFeatures::shouldUseAPIToken(const QVersionNumber& kimaiVersion) 20 | { 21 | return kimaiVersion >= gKimaiVersionForAPIToken; 22 | } 23 | -------------------------------------------------------------------------------- /src/monitor/macDesktopEventsMonitor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "desktopEventsMonitor.h" 6 | 7 | namespace kemai { 8 | 9 | class MacDesktopEventsMonitor : public DesktopEventsMonitor 10 | { 11 | public: 12 | MacDesktopEventsMonitor(); 13 | ~MacDesktopEventsMonitor() override = default; 14 | 15 | void initialize(const Settings::Events& eventsSettings) override; 16 | void start() override; 17 | void stop() override; 18 | 19 | private: 20 | void onPollTimeout(); 21 | 22 | QTimer mPollTimer; 23 | Settings::Events mEventsSettings; 24 | }; 25 | 26 | } // namespace kemai 27 | -------------------------------------------------------------------------------- /src/monitor/linuxDesktopEventsMonitor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "desktopEventsMonitor.h" 6 | 7 | namespace kemai { 8 | 9 | class LinuxDesktopEventsMonitor : public DesktopEventsMonitor 10 | { 11 | public: 12 | LinuxDesktopEventsMonitor(); 13 | ~LinuxDesktopEventsMonitor() override = default; 14 | 15 | void initialize(const Settings::Events& eventsSettings) override; 16 | void start() override; 17 | void stop() override; 18 | 19 | private: 20 | void onPollTimeout(); 21 | 22 | QTimer mPollTimer; 23 | Settings::Events mEventsSettings; 24 | }; 25 | 26 | } // namespace kemai 27 | -------------------------------------------------------------------------------- /src/models/taskListModel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "client/kimaiAPI.h" 6 | 7 | namespace kemai { 8 | 9 | class TaskListModel : public QAbstractListModel 10 | { 11 | public: 12 | enum TaskModelRole 13 | { 14 | TaskIDRole = Qt::UserRole + 1, 15 | UserIdRole 16 | }; 17 | 18 | void setTasks(const Tasks& tasks); 19 | 20 | QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; 21 | int rowCount(const QModelIndex& parent = QModelIndex()) const override; 22 | 23 | private: 24 | Tasks mTasks; 25 | }; 26 | 27 | } // namespace kemai 28 | -------------------------------------------------------------------------------- /src/models/taskFilterProxyModel.cpp: -------------------------------------------------------------------------------- 1 | #include "taskFilterProxyModel.h" 2 | #include "taskListModel.h" 3 | 4 | using namespace kemai; 5 | 6 | void TaskFilterProxyModel::setUserId(int userId) 7 | { 8 | mUserId = userId; 9 | invalidateFilter(); 10 | } 11 | 12 | bool TaskFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const 13 | { 14 | const auto& index = sourceModel()->index(source_row, 0, source_parent); 15 | return sourceModel()->data(index, Qt::DisplayRole).toString().contains(filterRegularExpression()) && 16 | sourceModel()->data(index, TaskListModel::UserIdRole).toInt() == mUserId; 17 | } 18 | -------------------------------------------------------------------------------- /src/resources/icons/update-off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/kimaiReply.cpp: -------------------------------------------------------------------------------- 1 | #include "kimaiReply.h" 2 | 3 | #include 4 | 5 | #include "misc/customFmt.h" 6 | 7 | using namespace kemai; 8 | 9 | void KimaiApiBaseResult::markAsReady() 10 | { 11 | mIsReady = true; 12 | emit ready(); 13 | } 14 | 15 | bool KimaiApiBaseResult::hasError() const 16 | { 17 | return mIsReady && mError.has_value(); 18 | } 19 | 20 | void KimaiApiBaseResult::setError(const QString& errorMessage) 21 | { 22 | mIsReady = true; 23 | mError = errorMessage; 24 | spdlog::error("<=== {}", errorMessage); 25 | emit error(); 26 | } 27 | 28 | QString KimaiApiBaseResult::errorMessage() const 29 | { 30 | return mError.value_or(""); 31 | } 32 | -------------------------------------------------------------------------------- /cmake/LocalizationTools.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Add processed qm files to qrc file. 3 | # Files are added into 'l10n' prefix. 4 | # 5 | macro(add_qm_files_to_qrc qrc_file) 6 | set(_qm_files ${ARGN}) 7 | set(_qrc_file ${CMAKE_CURRENT_BINARY_DIR}/l10n.qrc) 8 | 9 | file(WRITE ${_qrc_file} "\n\n") 10 | foreach (_lang ${_qm_files}) 11 | get_filename_component(_filename ${_lang} NAME) 12 | file(APPEND ${_qrc_file} " ${_filename}\n") 13 | endforeach () 14 | file(APPEND ${_qrc_file} " \n\n") 15 | 16 | add_custom_target(${PROJECT_NAME}-l10n-qrc DEPENDS ${_qm_files}) 17 | 18 | set(${qrc_file} ${_qrc_file}) 19 | endmacro() 20 | -------------------------------------------------------------------------------- /src/updater/kemaiUpdater_p.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "kemaiUpdater.h" 8 | 9 | namespace kemai { 10 | 11 | class KemaiUpdater::KemaiUpdaterPrivate : public QObject 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit KemaiUpdaterPrivate(KemaiUpdater* c); 17 | 18 | QNetworkRequest prepareGithubRequest(const QString& path); 19 | void onNamFinished(QNetworkReply* reply); 20 | 21 | QVersionNumber currentCheckSinceVersion; 22 | bool silenceIfNoNew; 23 | 24 | QScopedPointer networkAccessManager; 25 | 26 | private: 27 | KemaiUpdater* const mQ; 28 | }; 29 | 30 | } // namespace kemai 31 | -------------------------------------------------------------------------------- /src/models/kimaiDataListModel.cpp: -------------------------------------------------------------------------------- 1 | #include "kimaiDataListModel.h" 2 | 3 | using namespace kemai; 4 | 5 | int KimaiDataListModel::rowCount(const QModelIndex& parent) const 6 | { 7 | return static_cast(mData.size()); 8 | } 9 | 10 | QVariant KimaiDataListModel::data(const QModelIndex& index, int role) const 11 | { 12 | if (!index.isValid() || index.row() > mData.size()) 13 | { 14 | return {}; 15 | } 16 | 17 | const auto& it = std::next(mData.begin(), index.row()); 18 | switch (role) 19 | { 20 | case Qt::EditRole: 21 | case Qt::DisplayRole: 22 | return it->second; 23 | 24 | case Qt::UserRole: 25 | return it->first; 26 | 27 | default: 28 | return {}; 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/gui/timeSheetListWidgetItem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "client/kimaiAPI.h" 8 | 9 | namespace Ui { 10 | class TimeSheetListWidgetItem; 11 | } 12 | 13 | namespace kemai { 14 | 15 | class TimeSheetListWidgetItem : public QWidget 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | TimeSheetListWidgetItem(const TimeSheet& timeSheet, QWidget* parent = nullptr); 21 | ~TimeSheetListWidgetItem() override; 22 | 23 | signals: 24 | void timeSheetStartRequested(const TimeSheet& timeSheet); 25 | void timeSheetFillRequested(const TimeSheet& timeSheet); 26 | 27 | private: 28 | std::unique_ptr mUi; 29 | const TimeSheet mTimeSheet; 30 | }; 31 | 32 | } // namespace kemai 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [AlexandrePTJ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/resources/misc/licences.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/updater/kemaiUpdater.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace kemai { 8 | 9 | struct VersionDetails 10 | { 11 | QVersionNumber vn; 12 | QString description; 13 | QUrl url; 14 | }; 15 | 16 | class KemaiUpdater : public QObject 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | explicit KemaiUpdater(QObject* parent = nullptr); 22 | ~KemaiUpdater() override; 23 | 24 | void checkAvailableNewVersion(const QVersionNumber& sinceVersion = QVersionNumber(0, 0, 0), bool silenceIfNoNew = false); 25 | 26 | signals: 27 | void checkFinished(const kemai::VersionDetails& kv); 28 | 29 | private: 30 | class KemaiUpdaterPrivate; 31 | QScopedPointer mD; 32 | }; 33 | 34 | } // namespace kemai 35 | -------------------------------------------------------------------------------- /src/models/kimaiDataSortFilterProxyModel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace kemai { 8 | 9 | class KimaiDataSortFilterProxyModel : public QSortFilterProxyModel 10 | { 11 | public: 12 | KimaiDataSortFilterProxyModel(); 13 | ~KimaiDataSortFilterProxyModel() override; 14 | 15 | template void setKimaiFilter(const std::vector& kds) 16 | { 17 | mIds.clear(); 18 | for (const auto& k : kds) 19 | { 20 | mIds.push_back(k.id); 21 | } 22 | invalidateFilter(); 23 | } 24 | 25 | protected: 26 | bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; 27 | 28 | private: 29 | std::vector mIds; 30 | }; 31 | 32 | } // namespace kemai 33 | -------------------------------------------------------------------------------- /src/gui/loggerWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "loggerWidget.h" 2 | #include "ui_loggerWidget.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | using namespace kemai; 10 | 11 | LoggerWidget::LoggerWidget(QWidget* parent) : QWidget(parent), mUi(std::make_unique()) 12 | { 13 | mUi->setupUi(this); 14 | mUi->leLogFilePath->setText(helpers::getLogFilePath()); 15 | 16 | connect(mUi->tbOpenExplorerToLog, &QToolButton::clicked, []() { QDesktopServices::openUrl(QUrl(QString("file:///%1").arg(helpers::getLogDirPath()))); }); 17 | } 18 | 19 | LoggerWidget::~LoggerWidget() = default; 20 | 21 | void LoggerWidget::setModel(const std::shared_ptr& model) 22 | { 23 | mLoggerModel = model; 24 | mUi->tvLogView->setModel(mLoggerModel.get()); 25 | } 26 | -------------------------------------------------------------------------------- /src/gui/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(Kemai) 2 | 3 | add_executable(${PROJECT_NAME} ${OS_BUNDLE}) 4 | 5 | set(SRCS) 6 | 7 | set(HDRS) 8 | 9 | 10 | 11 | 12 | 13 | if (WIN32) 14 | list(APPEND SRCS ${CMAKE_SOURCE_DIR}/bundle/windows/kemai.rc) 15 | elseif (APPLE) 16 | set(KEMAI_ICNS "${CMAKE_SOURCE_DIR}/bundle/macos/kemai.icns") 17 | set_source_files_properties(${KEMAI_ICNS} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") 18 | set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/bundle/macos/Info.plist) 19 | endif () 20 | 21 | target_sources(${PROJECT_NAME} PRIVATE ${SRCS} ${HDRS} ${UIS} ${RESX} ${KEMAI_ICNS}) 22 | target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) 23 | target_link_libraries(${PROJECT_NAME} Qt::Widgets KemaiCore) 24 | -------------------------------------------------------------------------------- /src/models/kimaiDataSortFilterProxyModel.cpp: -------------------------------------------------------------------------------- 1 | #include "kimaiDataSortFilterProxyModel.h" 2 | 3 | using namespace kemai; 4 | 5 | KimaiDataSortFilterProxyModel::KimaiDataSortFilterProxyModel() = default; 6 | 7 | KimaiDataSortFilterProxyModel::~KimaiDataSortFilterProxyModel() = default; 8 | 9 | bool KimaiDataSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const 10 | { 11 | if (mIds.empty()) 12 | { 13 | return true; 14 | } 15 | 16 | auto index = sourceModel()->index(sourceRow, 0, sourceParent); 17 | auto id = sourceModel()->data(index, Qt::UserRole).toInt(); 18 | auto name = sourceModel()->data(index, Qt::DisplayRole).toString(); 19 | 20 | // Show filtered values and empty field 21 | return std::find(mIds.begin(), mIds.end(), id) != mIds.end() || name.isEmpty(); 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> C++ 2 | # Prerequisites 3 | *.d 4 | 5 | # Compiled Object files 6 | *.slo 7 | *.lo 8 | *.o 9 | *.obj 10 | 11 | # Precompiled Headers 12 | *.gch 13 | *.pch 14 | 15 | # Compiled Dynamic libraries 16 | *.so 17 | *.dylib 18 | *.dll 19 | 20 | # Fortran module files 21 | *.mod 22 | *.smod 23 | 24 | # Compiled Static libraries 25 | *.lai 26 | *.la 27 | *.a 28 | *.lib 29 | 30 | # Executables 31 | *.exe 32 | *.out 33 | *.app 34 | 35 | # ---> CMake 36 | CMakeCache.txt 37 | CMakeLists.txt.user 38 | CMakeFiles 39 | CMakeScripts 40 | Testing 41 | Makefile 42 | cmake_install.cmake 43 | install_manifest.txt 44 | compile_commands.json 45 | CTestTestfile.cmake 46 | 47 | # ---> JetBrains 48 | .idea/ 49 | 50 | # ---> Build output 51 | cmake-build-*/ 52 | out/ 53 | /build/ 54 | /dist/ 55 | *.AppImage 56 | 57 | # ---> MacOSX 58 | .DS_Store 59 | 60 | # ---> Visual Studio 61 | .vs/ 62 | -------------------------------------------------------------------------------- /src/resources/icons/visibility-off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gui/taskWidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "client/kimaiClient.h" 6 | #include "context/kemaiSession.h" 7 | #include "models/taskFilterProxyModel.h" 8 | #include "models/taskListModel.h" 9 | 10 | namespace Ui { 11 | class TaskWidget; 12 | } 13 | 14 | namespace kemai { 15 | 16 | class TaskWidget : public QWidget 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | TaskWidget(QWidget* parent = nullptr); 22 | ~TaskWidget() override; 23 | 24 | void setKemaiSession(std::shared_ptr kemaiSession); 25 | 26 | private: 27 | void updateTasks(); 28 | void onTaskItemChanged(const QModelIndex& current, const QModelIndex& previous); 29 | void onStartStopClicked(); 30 | void onCloseClicked(); 31 | 32 | std::unique_ptr mUi; 33 | std::shared_ptr mSession; 34 | TaskListModel mTaskModel; 35 | TaskFilterProxyModel mTaskProxyModel; 36 | }; 37 | 38 | } // namespace kemai 39 | -------------------------------------------------------------------------------- /src/models/kimaiDataListModel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace kemai { 6 | 7 | class KimaiDataListModel : public QAbstractListModel 8 | { 9 | public: 10 | int rowCount(const QModelIndex& parent) const override; 11 | QVariant data(const QModelIndex& index, int role) const override; 12 | 13 | template void setKimaiData(const std::vector& kds) 14 | { 15 | beginResetModel(); 16 | if (!kds.empty()) 17 | { 18 | mData = {{0, ""}}; 19 | for (const auto& kd : kds) 20 | { 21 | mData.emplace_back(std::make_pair(kd.id, kd.name)); 22 | } 23 | std::sort(mData.begin(), mData.end(), [](const auto& a, const auto& b) { return a.second.toLower() < b.second.toLower(); }); 24 | } 25 | endResetModel(); 26 | } 27 | 28 | private: 29 | std::vector> mData; 30 | }; 31 | 32 | } // namespace kemai 33 | -------------------------------------------------------------------------------- /src/monitor/desktopEventsMonitor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "settings/settings.h" 9 | 10 | namespace kemai { 11 | 12 | class DesktopEventsMonitor : public QObject 13 | { 14 | Q_OBJECT 15 | 16 | protected: 17 | DesktopEventsMonitor(bool hasLockSupport, bool hasIdleSupport); 18 | 19 | public: 20 | static std::shared_ptr create(QWidget* widget = nullptr); 21 | 22 | virtual void initialize(const Settings::Events& eventsSettings) = 0; 23 | virtual void start() = 0; 24 | virtual void stop() = 0; 25 | 26 | bool hasLockSupport() const; 27 | bool hasIdleSupport() const; 28 | 29 | signals: 30 | void lockDetected(); 31 | void idleDetected(); 32 | 33 | private: 34 | const bool mHasLockSupport = false; 35 | const bool mHasIdleSupport = false; 36 | }; 37 | 38 | } // namespace kemai 39 | -------------------------------------------------------------------------------- /src/monitor/kimaiEventsMonitor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "client/kimaiClient.h" 9 | 10 | namespace kemai { 11 | 12 | class KimaiEventsMonitor : public QObject 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | KimaiEventsMonitor(std::shared_ptr kimaiClient); 18 | ~KimaiEventsMonitor() override; 19 | 20 | void refreshCurrentTimeSheet(); 21 | 22 | std::optional currentTimeSheet() const; 23 | bool hasCurrentTimeSheet() const; 24 | 25 | signals: 26 | void currentTimeSheetChanged(); 27 | 28 | private: 29 | void onSecondTimeout(); 30 | void onClientError(KimaiApiBaseResult* apiBaseResult); 31 | void onActiveTimeSheetsReceived(TimeSheetsResult timeSheetsResult); 32 | 33 | std::shared_ptr mKimaiClient; 34 | std::optional mCurrentTimeSheet; 35 | std::optional mLastTimeSheetUpdate; 36 | QTimer mSecondTimer; 37 | }; 38 | 39 | } // namespace kemai 40 | -------------------------------------------------------------------------------- /src/monitor/windowsDesktopEventsMonitor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "desktopEventsMonitor.h" 7 | 8 | namespace kemai { 9 | 10 | class WindowsDesktopEventsMonitor : public DesktopEventsMonitor, public QAbstractNativeEventFilter 11 | { 12 | public: 13 | WindowsDesktopEventsMonitor(QWidget* widget); 14 | ~WindowsDesktopEventsMonitor() override = default; 15 | 16 | void initialize(const Settings::Events& eventsSettings) override; 17 | void start() override; 18 | void stop() override; 19 | 20 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 21 | bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; 22 | #else 23 | bool nativeEventFilter(const QByteArray& eventType, void* message, qintptr* result) override; 24 | #endif // 25 | 26 | private: 27 | void onPollTimeout(); 28 | 29 | QWidget* mListenerWidget = nullptr; 30 | QTimer mPollTimer; 31 | Settings::Events mEventsSettings; 32 | }; 33 | 34 | } // namespace kemai 35 | -------------------------------------------------------------------------------- /src/gui/timeSheetListWidgetItem.cpp: -------------------------------------------------------------------------------- 1 | #include "timeSheetListWidgetItem.h" 2 | #include "ui_timeSheetListWidgetItem.h" 3 | 4 | #include 5 | 6 | using namespace kemai; 7 | 8 | TimeSheetListWidgetItem::TimeSheetListWidgetItem(const TimeSheet& timeSheet, QWidget* parent) 9 | : QWidget(parent), mTimeSheet(timeSheet), mUi(std::make_unique()) 10 | { 11 | mUi->setupUi(this); 12 | mUi->frame->setFrameShape(QFrame::StyledPanel); 13 | mUi->lbActivity->setText(mTimeSheet.activity.name); 14 | mUi->lbDescription->setText(mTimeSheet.project.name); 15 | mUi->lbDuration->setText(helpers::getDurationString(timeSheet.beginAt, timeSheet.endAt)); 16 | mUi->lbStartedAt->setText(timeSheet.beginAt.toString(Qt::ISODate)); 17 | 18 | connect(mUi->btStart, &QPushButton::clicked, [this]() { emit timeSheetStartRequested(mTimeSheet); }); 19 | connect(mUi->btFill, &QPushButton::clicked, [this]() { emit timeSheetFillRequested(mTimeSheet); }); 20 | } 21 | 22 | TimeSheetListWidgetItem::~TimeSheetListWidgetItem() = default; 23 | -------------------------------------------------------------------------------- /src/gui/durationEdit.cpp: -------------------------------------------------------------------------------- 1 | #include "durationEdit.h" 2 | 3 | #include 4 | 5 | using namespace kemai; 6 | 7 | DurationEdit::DurationEdit(QWidget* parent) : QLineEdit(parent), mDurationRx("^([0-9]*)(:?([0-9]{2})?)$") 8 | { 9 | setAlignment(Qt::AlignRight | Qt::AlignVCenter); 10 | setValidator(new QRegularExpressionValidator(mDurationRx, this)); 11 | } 12 | 13 | void DurationEdit::setSeconds(int seconds) 14 | { 15 | setText(QString("%1:%2").arg(seconds / 3600, seconds % 3600)); 16 | } 17 | 18 | int DurationEdit::seconds() const 19 | { 20 | int nSecs = 0; 21 | 22 | auto match = mDurationRx.match(text()); 23 | if (match.hasMatch()) 24 | { 25 | // hours 26 | auto hourStr = match.captured(1); 27 | if (!hourStr.isEmpty()) 28 | { 29 | nSecs += 3600 * hourStr.toInt(); 30 | } 31 | 32 | // minutes 33 | auto minStr = match.captured(3); 34 | if (!minStr.isEmpty()) 35 | { 36 | nSecs += 60 * minStr.toInt(); 37 | } 38 | } 39 | 40 | return nSecs; 41 | } 42 | -------------------------------------------------------------------------------- /src/monitor/desktopEventsMonitor.cpp: -------------------------------------------------------------------------------- 1 | #include "desktopEventsMonitor.h" 2 | 3 | #ifdef Q_OS_WINDOWS 4 | # include "windowsDesktopEventsMonitor.h" 5 | #elif defined Q_OS_MAC 6 | # include "macDesktopEventsMonitor.h" 7 | #elif defined Q_OS_LINUX 8 | # include "linuxDesktopEventsMonitor.h" 9 | #endif 10 | 11 | using namespace kemai; 12 | 13 | DesktopEventsMonitor::DesktopEventsMonitor(bool hasLockSupport, bool hasIdleSupport) : mHasLockSupport(hasLockSupport), mHasIdleSupport(hasIdleSupport) {} 14 | 15 | std::shared_ptr DesktopEventsMonitor::create(QWidget* widget) 16 | { 17 | #ifdef Q_OS_WINDOWS 18 | return std::make_shared(widget); 19 | #elif defined Q_OS_MAC 20 | return std::make_shared(); 21 | #elif defined Q_OS_LINUX 22 | return std::make_shared(); 23 | #else 24 | return {}; 25 | #endif // 26 | } 27 | 28 | bool DesktopEventsMonitor::hasLockSupport() const 29 | { 30 | return mHasLockSupport; 31 | } 32 | 33 | bool DesktopEventsMonitor::hasIdleSupport() const 34 | { 35 | return mHasIdleSupport; 36 | } 37 | -------------------------------------------------------------------------------- /src/misc/dataReader.cpp: -------------------------------------------------------------------------------- 1 | #include "dataReader.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace kemai; 9 | 10 | QMap DataReader::countries() 11 | { 12 | QMap res; 13 | 14 | QFile fdata(":/data/countries"); 15 | fdata.open(QIODevice::ReadOnly | QIODevice::Text); 16 | 17 | auto jdoc = QJsonDocument::fromJson(fdata.readAll()); 18 | auto jobj = jdoc.object(); 19 | for (auto it = jobj.begin(); it != jobj.end(); ++it) 20 | { 21 | res.insert(it.key(), it.value().toString()); 22 | } 23 | 24 | return res; 25 | } 26 | 27 | QMap DataReader::currencies() 28 | { 29 | QMap res; 30 | 31 | QFile fdata(":/data/currencies"); 32 | fdata.open(QIODevice::ReadOnly | QIODevice::Text); 33 | 34 | auto jdoc = QJsonDocument::fromJson(fdata.readAll()); 35 | auto jobj = jdoc.object(); 36 | for (auto it = jobj.begin(); it != jobj.end(); ++it) 37 | { 38 | res.insert(it.key(), it.value().toString()); 39 | } 40 | 41 | return res; 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexandre Petitjean 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/resources/kemai.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | data/iso3166-alpha2.json 4 | data/iso4217.json 5 | 6 | 7 | icons/add.svg 8 | icons/play.svg 9 | icons/stop.svg 10 | icons/kimai.png 11 | icons/kimai-red.png 12 | icons/visibility.svg 13 | icons/visibility-off.svg 14 | icons/update.svg 15 | icons/update-off.svg 16 | icons/backspace.svg 17 | icons/refresh.svg 18 | icons/remove.svg 19 | icons/warning.svg 20 | 21 | 22 | misc/licences.html 23 | 24 | 25 | -------------------------------------------------------------------------------- /bundle/macos/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | kemai 9 | CFBundleGetInfoString 10 | 11 | CFBundleIconFile 12 | kemai 13 | CFBundleIdentifier 14 | 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleLongVersionString 18 | 19 | CFBundleName 20 | 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | 25 | CFBundleVersion 26 | 27 | CSResourcesFileMapped 28 | 29 | NSHumanReadableCopyright 30 | 31 | NSPrincipalClass 32 | NSApplication 33 | NSHighResolutionCapable 34 | True 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/misc/helpers.cpp: -------------------------------------------------------------------------------- 1 | #include "helpers.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace kemai::helpers { 7 | 8 | QString getDurationString(const QDateTime& beginAt, const QDateTime& endAt) 9 | { 10 | if (!beginAt.isValid() || !endAt.isValid()) 11 | { 12 | return "--:--"; 13 | } 14 | 15 | auto nSecs = beginAt.secsTo(endAt); 16 | 17 | // NOLINTBEGIN(readability-magic-numbers) 18 | const auto nDays = nSecs / 86400; 19 | nSecs -= nDays * 86400; 20 | 21 | const auto nHours = nSecs / 3600; 22 | nSecs -= nHours * 3600; 23 | 24 | const auto nMinutes = nSecs / 60; 25 | nSecs -= nMinutes * 60; 26 | 27 | return QString("%1%2:%3:%4") 28 | .arg(nDays > 0 ? QString::number(nDays) + "d " : "") 29 | .arg(nHours, 2, 10, QChar('0')) 30 | .arg(nMinutes, 2, 10, QChar('0')) 31 | .arg(nSecs, 2, 10, QChar('0')); 32 | // NOLINTEND(readability-magic-numbers) 33 | } 34 | 35 | QString getLogDirPath() 36 | { 37 | return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); 38 | } 39 | 40 | QString getLogFilePath() 41 | { 42 | return QDir(getLogDirPath()).absoluteFilePath("kemai.log"); 43 | } 44 | 45 | } // namespace kemai::helpers 46 | -------------------------------------------------------------------------------- /bundle/macos/create_dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | set -e 5 | 6 | # Run this script once Kemai is built. Install and AppImage will be handle here. 7 | # It just path to Qt and to build directory. 8 | 9 | 10 | # Read args 11 | qt_path=${qt_path:-} 12 | build_path=${build_path:-} 13 | 14 | while [ $# -gt 0 ]; do 15 | if [[ $1 == *"--"* ]]; then 16 | param="${1/--/}" 17 | declare $param="$2" 18 | fi 19 | shift 20 | done 21 | 22 | # Cleanup 23 | rm dist/*.dmg || true 24 | 25 | # Run install to dist 26 | cmake --build $build_path --target install 27 | 28 | # Cleanup unneeded install files 29 | rm -r dist/include 30 | rm -r dist/lib 31 | 32 | # Gets create-dmg tools 33 | brew install create-dmg 34 | 35 | # Export vars 36 | export VERSION=$(cat ${build_path}/version.txt) 37 | export NOUPDATE=$(grep -e "KEMAI_ENABLE_UPDATE.*OFF" ${build_path}/CMakeCache.txt | wc -l) 38 | 39 | DMG_NAME="Kemai-$VERSION" 40 | if [[ $NOUPDATE -eq "1" ]]; then 41 | DMG_NAME="$DMG_NAME-NoUpdate" 42 | fi 43 | 44 | 45 | # Run dmg builder 46 | pushd dist 47 | ${qt_path}/bin/macdeployqt Kemai.app -executable=Kemai.app/Contents/MacOS/Kemai -always-overwrite 48 | create-dmg --volicon Kemai.app/Contents/Resources/kemai.icns --app-drop-link 0 0 --skip-jenkins $DMG_NAME.dmg Kemai.app 49 | -------------------------------------------------------------------------------- /src/models/taskListModel.cpp: -------------------------------------------------------------------------------- 1 | #include "taskListModel.h" 2 | 3 | #include 4 | #include 5 | 6 | using namespace kemai; 7 | 8 | void TaskListModel::setTasks(const Tasks& tasks) 9 | { 10 | beginResetModel(); 11 | mTasks = tasks; 12 | endResetModel(); 13 | } 14 | 15 | QVariant TaskListModel::data(const QModelIndex& index, int role) const 16 | { 17 | if (!index.isValid() || (index.row() > mTasks.size())) 18 | { 19 | return {}; 20 | } 21 | 22 | const auto& task = mTasks.at(index.row()); 23 | switch (role) 24 | { 25 | case TaskIDRole: 26 | return task.id; 27 | 28 | case UserIdRole: 29 | return task.user.id; 30 | 31 | case Qt::DisplayRole: { 32 | auto display = task.title; 33 | if (!task.description.isEmpty()) 34 | { 35 | display += "\n- " + task.description; 36 | } 37 | return display; 38 | } 39 | 40 | case Qt::DecorationRole: 41 | return task.activeTimeSheets.empty() ? QIcon() : qApp->style()->standardIcon(QStyle::SP_ArrowRight); 42 | 43 | default: 44 | return {}; 45 | } 46 | } 47 | 48 | int TaskListModel::rowCount(const QModelIndex& /*parent*/) const 49 | { 50 | return static_cast(mTasks.size()); 51 | } 52 | -------------------------------------------------------------------------------- /src/monitor/macDesktopEventsMonitor.mm: -------------------------------------------------------------------------------- 1 | #include "macDesktopEventsMonitor.h" 2 | 3 | #import 4 | 5 | using namespace kemai; 6 | 7 | MacDesktopEventsMonitor::MacDesktopEventsMonitor() : DesktopEventsMonitor(false, true) 8 | { 9 | connect(&mPollTimer, &QTimer::timeout, this, &MacDesktopEventsMonitor::onPollTimeout); 10 | } 11 | 12 | void MacDesktopEventsMonitor::initialize(const Settings::Events& eventsSettings) 13 | { 14 | stop(); 15 | 16 | mEventsSettings = eventsSettings; 17 | } 18 | 19 | void MacDesktopEventsMonitor::start() 20 | { 21 | if (mEventsSettings.stopOnIdle) 22 | { 23 | mPollTimer.start(std::chrono::seconds(1)); 24 | } 25 | } 26 | 27 | void MacDesktopEventsMonitor::stop() 28 | { 29 | mPollTimer.stop(); 30 | } 31 | 32 | void MacDesktopEventsMonitor::onPollTimeout() 33 | { 34 | auto interval = CGEventSourceSecondsSinceLastEventType(CGEventSourceStateID::kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType); 35 | 36 | auto idleSinceNSecs = std::chrono::seconds(static_cast(interval)); 37 | 38 | if (mEventsSettings.stopOnIdle && mEventsSettings.idleDelayMinutes <= std::chrono::duration_cast(idleSinceNSecs).count()) 39 | { 40 | emit idleDetected(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/gui/settingsDialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "client/kimaiClient.h" 9 | #include "monitor/desktopEventsMonitor.h" 10 | #include "settings/settings.h" 11 | 12 | namespace Ui { 13 | class SettingsDialog; 14 | } 15 | 16 | namespace kemai { 17 | 18 | class SettingsDialog : public QDialog 19 | { 20 | Q_OBJECT 21 | 22 | public: 23 | SettingsDialog(const std::shared_ptr& desktopEventsMonitor, QWidget* parent = nullptr); 24 | ~SettingsDialog() override; 25 | 26 | void setSettings(const Settings& settings); 27 | Settings settings() const; 28 | 29 | private: 30 | void onProfilesListCurrentItemChanged(QListWidgetItem* current, QListWidgetItem* previous); 31 | void onBtTestClicked(); 32 | void onProfileFieldValueChanged(); 33 | void onProfileAddButtonClicked(); 34 | void onProfileDelButtonClicked(); 35 | 36 | void toggleProfileAPITokenWarningVisibility(bool visible); 37 | 38 | std::unique_ptr mUi; 39 | QAction* mActToggleTokenVisible = nullptr; 40 | QAction* mActToggleAPITokenVisible = nullptr; 41 | 42 | Settings m_settings; 43 | 44 | // for connection testing 45 | std::unique_ptr mKimaiClient; 46 | }; 47 | 48 | } // namespace kemai 49 | -------------------------------------------------------------------------------- /bundle/linux/sysroot/usr/bin/kemai-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Check if fallback libraries are present on host. If not, add embedded fallback libraries to path 5 | # 6 | function find_library() 7 | { 8 | # Print full path to a library or return exit status 1 if not found 9 | local library="$1" # take library name as input (e.g. "libfoo.so") 10 | # Look for the library in $LD_LIBRARY_PATH 11 | local dir IFS=':' # split path into array on this separator 12 | for dir in "${LD_LIBRARY_PATH[@]}"; do 13 | if [[ -e "${dir}/${library}" ]]; then 14 | echo "${dir}/${library}" 15 | return # Library found 16 | fi 17 | done 18 | # Not found yet so check system cache 19 | ldconfig -p | awk -v lib="${library}" -v arch="${HOSTTYPE}" \ 20 | ' 21 | BEGIN {status=1} 22 | ($1 == lib && index($0, arch)) {print $NF; status=0; exit} 23 | END {exit status} 24 | ' 25 | } 26 | 27 | fallback_libs="" 28 | for fallback_dir in "${APPDIR}/fallback"/*; do 29 | libname="${fallback_dir##*/}" 30 | if ! find_library "${libname}"; then 31 | echo "${APPIMAGE}: Using fallback for ${libname}" 32 | fallback_libs="${fallback_libs}:${fallback_dir}" 33 | fi 34 | done 35 | 36 | export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}${fallback_libs}" 37 | 38 | # 39 | # Wrapper to run expected embedded Qinertia app 40 | # 41 | ${APPDIR}/usr/bin/Kemai 42 | -------------------------------------------------------------------------------- /src/gui/projectDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "projectDialog.h" 2 | #include "ui_projectDialog.h" 3 | 4 | #include 5 | #include 6 | 7 | using namespace kemai; 8 | 9 | ProjectDialog::ProjectDialog(QWidget* parent) : QDialog(parent), mUi(std::make_unique()) 10 | { 11 | mUi->setupUi(this); 12 | enableSave(false); 13 | 14 | connect(mUi->leName, &QLineEdit::textChanged, this, &ProjectDialog::validateForm); 15 | connect(mUi->leTimeBudget, &QLineEdit::textChanged, this, &ProjectDialog::validateForm); 16 | } 17 | 18 | ProjectDialog::~ProjectDialog() = default; 19 | 20 | Project ProjectDialog::project() const 21 | { 22 | Project project; 23 | project.name = mUi->leName->text(); 24 | project.budget = mUi->sbBudget->value(); 25 | project.timeBudget = mUi->leTimeBudget->seconds(); 26 | return project; 27 | } 28 | 29 | void ProjectDialog::enableSave(bool enable) 30 | { 31 | if (auto btn = mUi->buttonBox->button(QDialogButtonBox::Save)) 32 | { 33 | btn->setEnabled(enable); 34 | } 35 | } 36 | 37 | void ProjectDialog::validateForm() 38 | { 39 | const auto& name = mUi->leName->text(); 40 | bool nameOk = (!name.isEmpty()) && (name.size() > 1); 41 | bool timeBudgetOk = mUi->leTimeBudget->hasAcceptableInput(); 42 | enableSave(nameOk && timeBudgetOk); 43 | } 44 | -------------------------------------------------------------------------------- /cmake/WinDeployOpenSSL.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Looking for OpenSSL 3 | # 4 | 5 | function(kemai_find_openssl directory) 6 | 7 | message("Looking for OpenSSL in ${directory}") 8 | 9 | find_file(_WINDEPLOYOPENSSL_CRYPTO 10 | NAMES libcrypto-3-x64.dll 11 | PATHS ${directory} ${directory}/bin 12 | NO_DEFAULT_PATH) 13 | find_file(_WINDEPLOYOPENSSL_SSL 14 | NAMES libssl-3-x64.dll 15 | PATHS ${directory} ${directory}/bin 16 | NO_DEFAULT_PATH) 17 | 18 | if(_WINDEPLOYOPENSSL_CRYPTO) 19 | message("-- Found ${_WINDEPLOYOPENSSL_CRYPTO}") 20 | set(WINDEPLOYOPENSSL_CRYPTO ${_WINDEPLOYOPENSSL_CRYPTO} PARENT_SCOPE) 21 | mark_as_advanced(WINDEPLOYOPENSSL_CRYPTO) 22 | endif() 23 | 24 | if(_WINDEPLOYOPENSSL_SSL) 25 | message("-- Found ${_WINDEPLOYOPENSSL_SSL}") 26 | set(WINDEPLOYOPENSSL_SSL ${_WINDEPLOYOPENSSL_SSL} PARENT_SCOPE) 27 | mark_as_advanced(WINDEPLOYOPENSSL_SSL) 28 | endif() 29 | 30 | endfunction() 31 | 32 | function(windeployopenssl directory) 33 | install( 34 | FILES ${WINDEPLOYOPENSSL_CRYPTO} ${WINDEPLOYOPENSSL_SSL} 35 | DESTINATION ${directory}) 36 | endfunction() 37 | 38 | # Try to find from user dir 39 | if(OPENSSL_ROOT) 40 | kemai_find_openssl(${OPENSSL_ROOT}) 41 | endif() 42 | 43 | # Try to find from github actions 44 | if (NOT WINDEPLOYOPENSSL_CRYPTO) 45 | if ($ENV{IQTA_TOOLS}) 46 | kemai_find_openssl($ENV{IQTA_TOOLS}) 47 | endif() 48 | endif() 49 | -------------------------------------------------------------------------------- /src/settings/settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace kemai { 10 | 11 | struct Settings 12 | { 13 | struct Profile 14 | { 15 | QUuid id; 16 | QString name; 17 | QString host; 18 | QString username; 19 | QString token; 20 | QString apiToken; // Since Kimai 2.14 21 | }; 22 | std::vector profiles; 23 | 24 | QStringList trustedCertificates; 25 | 26 | struct Kemai 27 | { 28 | bool closeToSystemTray = false; 29 | bool minimizeToSystemTray = false; 30 | bool checkUpdateAtStartup = true; 31 | QString ignoredVersion; 32 | QByteArray geometry; 33 | QLocale language; 34 | QUuid lastConnectedProfile; 35 | } kemai; 36 | 37 | struct Events 38 | { 39 | bool stopOnLock = false; 40 | bool stopOnIdle = false; 41 | int idleDelayMinutes = 1; 42 | bool autoRefreshCurrentTimeSheet = false; 43 | int autoRefreshCurrentTimeSheetDelaySeconds = 5; 44 | } events; 45 | }; 46 | 47 | class SettingsHelper 48 | { 49 | public: 50 | static Settings load(); 51 | static void save(const Settings& settings); 52 | }; 53 | 54 | } // namespace kemai 55 | -------------------------------------------------------------------------------- /src/gui/activityDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "activityDialog.h" 2 | #include "ui_activityDialog.h" 3 | 4 | #include 5 | #include 6 | 7 | using namespace kemai; 8 | 9 | ActivityDialog::ActivityDialog(QWidget* parent) : QDialog(parent), mUi(std::make_unique()) 10 | { 11 | mUi->setupUi(this); 12 | enableSave(false); 13 | 14 | connect(mUi->leName, &QLineEdit::textChanged, this, &ActivityDialog::validateForm); 15 | connect(mUi->leTimeBudget, &QLineEdit::textChanged, this, &ActivityDialog::validateForm); 16 | } 17 | 18 | ActivityDialog::~ActivityDialog() = default; 19 | 20 | Activity ActivityDialog::activity() const 21 | { 22 | Activity activity; 23 | activity.name = mUi->leName->text(); 24 | activity.budget = mUi->sbBudget->value(); 25 | activity.timeBudget = mUi->leTimeBudget->seconds(); 26 | return activity; 27 | } 28 | 29 | void ActivityDialog::enableSave(bool enable) 30 | { 31 | auto button = mUi->buttonBox->button(QDialogButtonBox::Save); 32 | if (button != nullptr) 33 | { 34 | button->setEnabled(enable); 35 | } 36 | } 37 | 38 | void ActivityDialog::validateForm() 39 | { 40 | const auto& name = mUi->leName->text(); 41 | bool nameOk = (!name.isEmpty()) && (name.size() > 1); 42 | bool timeBudgetOk = mUi->leTimeBudget->hasAcceptableInput(); 43 | enableSave(nameOk && timeBudgetOk); 44 | } 45 | -------------------------------------------------------------------------------- /src/gui/loggerWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | LoggerWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 481 10 | 295 11 | 12 | 13 | 14 | Log 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Log file path 26 | 27 | 28 | 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | ... 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/monitor/linuxDesktopEventsMonitor.cpp: -------------------------------------------------------------------------------- 1 | #include "linuxDesktopEventsMonitor.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | using namespace kemai; 8 | 9 | LinuxDesktopEventsMonitor::LinuxDesktopEventsMonitor() : DesktopEventsMonitor(false, true) 10 | { 11 | connect(&mPollTimer, &QTimer::timeout, this, &LinuxDesktopEventsMonitor::onPollTimeout); 12 | } 13 | 14 | void LinuxDesktopEventsMonitor::initialize(const Settings::Events& eventsSettings) 15 | { 16 | stop(); 17 | 18 | mEventsSettings = eventsSettings; 19 | } 20 | 21 | void LinuxDesktopEventsMonitor::start() 22 | { 23 | if (mEventsSettings.stopOnIdle) 24 | { 25 | mPollTimer.start(std::chrono::seconds(1)); 26 | } 27 | } 28 | 29 | void LinuxDesktopEventsMonitor::stop() 30 | { 31 | mPollTimer.stop(); 32 | } 33 | 34 | void LinuxDesktopEventsMonitor::onPollTimeout() 35 | { 36 | auto x11Display = XOpenDisplay(nullptr); 37 | 38 | if (x11Display == nullptr) 39 | { 40 | spdlog::error("Cannot open display to check idle"); 41 | return; 42 | } 43 | 44 | auto xssInfo = XScreenSaverAllocInfo(); 45 | XScreenSaverQueryInfo(x11Display, DefaultRootWindow(x11Display), xssInfo); 46 | 47 | auto idleSinceNMilliSecs = std::chrono::milliseconds(xssInfo->idle); 48 | if (mEventsSettings.stopOnIdle && mEventsSettings.idleDelayMinutes <= std::chrono::duration_cast(idleSinceNMilliSecs).count()) 49 | { 50 | emit idleDetected(); 51 | } 52 | 53 | XCloseDisplay(x11Display); 54 | } 55 | -------------------------------------------------------------------------------- /src/gui/mainWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 280 10 | 256 11 | 12 | 13 | 14 | Kemai - Kimai client 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 0 23 | 24 | 25 | 0 26 | 27 | 28 | 0 29 | 30 | 31 | 0 32 | 33 | 34 | 0 35 | 36 | 37 | 38 | 39 | -1 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Settings... 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/gui/autoCompleteComboBox.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace kemai { 9 | 10 | class AutoCompleteValidator; 11 | 12 | class AutoCompleteComboBox : public QComboBox 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | AutoCompleteComboBox(QWidget* parent = nullptr); 18 | ~AutoCompleteComboBox() override; 19 | 20 | template void setKimaiData(const std::vector& kds) 21 | { 22 | auto current = QVariant(); 23 | if (!mModelSet) // Avoid to call virtual setModel in ctor and to have to call explicitly somewhere else. 24 | { 25 | mModelSet = true; 26 | setModel(&mProxyModel); 27 | } 28 | else 29 | { 30 | // Save which item was selected before the model reset 31 | current = currentData(); 32 | } 33 | mModel.setKimaiData(kds); 34 | if (!current.isNull()) 35 | { 36 | // Find the item with a matching ID and make it current again 37 | int foundIdx = findData(current, Qt::UserRole); 38 | if (foundIdx > 0) 39 | { 40 | setCurrentIndex(foundIdx); 41 | } 42 | } 43 | } 44 | 45 | template void setFilter(const std::vector& kds) { mProxyModel.setKimaiFilter(kds); } 46 | 47 | private: 48 | bool mModelSet = false; 49 | KimaiDataListModel mModel; 50 | KimaiDataSortFilterProxyModel mProxyModel; 51 | }; 52 | 53 | } // namespace kemai 54 | -------------------------------------------------------------------------------- /src/gui/autoCompleteComboBox.cpp: -------------------------------------------------------------------------------- 1 | #include "autoCompleteComboBox.h" 2 | 3 | #include 4 | 5 | using namespace kemai; 6 | 7 | class kemai::AutoCompleteValidator : public QValidator 8 | { 9 | public: 10 | explicit AutoCompleteValidator(KimaiDataListModel* model) : mModel(model) {} 11 | ~AutoCompleteValidator() override = default; 12 | 13 | QValidator::State validate(QString& input, int& /*pos*/) const override 14 | { 15 | auto nRows = mModel->rowCount({}); 16 | 17 | if (input.isEmpty() || nRows == 0) 18 | { 19 | return Acceptable; 20 | } 21 | 22 | for (auto i = 0; i < nRows; ++i) 23 | { 24 | auto str = mModel->data(mModel->index(i), Qt::DisplayRole).toString(); 25 | if (str == input) 26 | { 27 | return Acceptable; 28 | } 29 | if (str.contains(input, Qt::CaseInsensitive)) 30 | { 31 | return Intermediate; 32 | } 33 | } 34 | 35 | return Invalid; 36 | } 37 | 38 | void fixup(QString& input) const override { input.clear(); } 39 | 40 | private: 41 | KimaiDataListModel* mModel; 42 | }; 43 | 44 | AutoCompleteComboBox::AutoCompleteComboBox(QWidget* parent) : QComboBox(parent) 45 | { 46 | mProxyModel.setSourceModel(&mModel); 47 | 48 | setEditable(true); 49 | 50 | auto completer = new QCompleter(&mModel); 51 | completer->setCaseSensitivity(Qt::CaseInsensitive); 52 | completer->setFilterMode(Qt::MatchContains); 53 | setCompleter(completer); 54 | 55 | setValidator(new AutoCompleteValidator(&mModel)); 56 | } 57 | 58 | AutoCompleteComboBox::~AutoCompleteComboBox() = default; 59 | -------------------------------------------------------------------------------- /src/gui/activityWidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "client/kimaiClient.h" 7 | #include "context/kemaiSession.h" 8 | 9 | namespace Ui { 10 | class ActivityWidget; 11 | } 12 | 13 | namespace kemai { 14 | 15 | class ActivityWidget : public QWidget 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | ActivityWidget(QWidget* parent = nullptr); 21 | ~ActivityWidget() override; 22 | 23 | void setKemaiSession(std::shared_ptr kemaiSession); 24 | void stopCurrentTimeSheet(); 25 | 26 | signals: 27 | void currentActivityChanged(bool started); 28 | 29 | private: 30 | void onCbCustomerFieldChanged(); 31 | void onCbProjectFieldChanged(); 32 | void onCbActivityFieldChanged(); 33 | 34 | void onTbAddCustomerClicked(); 35 | void onTbAddProjectClicked(); 36 | void onTbAddActivityClicked(); 37 | 38 | void onBtStartStopClicked(); 39 | 40 | void onSecondTimeout(); 41 | 42 | void onSessionCurrentTimeSheetChanged(); 43 | void onSessionCacheSynchronizeFinished(); 44 | 45 | void onHistoryTimeSheetStartRequested(const TimeSheet& timeSheet); 46 | void onHistoryTimeSheetFillRequested(const TimeSheet& timeSheet); 47 | void fillFromTimesheet(const TimeSheet& timeSheet); 48 | 49 | void updateControls(); 50 | 51 | void updateCustomersCombo(); 52 | void updateProjectsCombo(); 53 | void updateActivitiesCombo(); 54 | void updateRecentTimeSheetsView(); 55 | 56 | void startPendingTimeSheet(); 57 | 58 | std::unique_ptr mUi; 59 | QTimer mSecondTimer; 60 | std::shared_ptr mSession; 61 | std::optional mPendingStartRequest; 62 | }; 63 | 64 | } // namespace kemai 65 | -------------------------------------------------------------------------------- /src/client/kimaiCache.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "client/kimaiClient.h" 10 | 11 | namespace kemai { 12 | 13 | class KimaiCache : public QObject 14 | { 15 | Q_OBJECT 16 | 17 | public: 18 | enum class Category 19 | { 20 | Customers, 21 | Projects, 22 | Activities, 23 | RecentTimeSheets 24 | }; 25 | 26 | enum class Status 27 | { 28 | Empty, 29 | SyncPending, 30 | Ready 31 | }; 32 | 33 | void synchronize(const std::shared_ptr& client, const std::set& categories = {}); 34 | Status status() const; 35 | 36 | Customers customers() const; 37 | Projects projects(std::optional customerId = std::nullopt) const; 38 | Activities activities(std::optional projectId = std::nullopt) const; 39 | TimeSheets recentTimeSheets() const; 40 | 41 | signals: 42 | void synchronizeStarted(); 43 | void synchronizeFinished(); 44 | 45 | private: 46 | void updateSyncProgress(Category finishedCategory); 47 | void processCustomersResult(CustomersResult customersResult); 48 | void processProjectsResult(ProjectsResult projectsResult); 49 | void processActivitiesResult(ActivitiesResult activitiesResult); 50 | void processRecentTimeSheetsResult(TimeSheetsResult timeSheetsResult); 51 | 52 | std::set mPendingSync; 53 | 54 | Customers mCustomers; 55 | Projects mProjects; 56 | Activities mActivities; 57 | TimeSheets mRecentTimeSheets; 58 | 59 | kemai::KimaiCache::Status mStatus = kemai::KimaiCache::Status::Empty; 60 | std::mutex mSyncMutex; 61 | std::mutex mProgressMutex; 62 | }; 63 | 64 | } // namespace kemai 65 | -------------------------------------------------------------------------------- /src/client/parser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "kimaiAPI.h" 13 | 14 | namespace kemai { 15 | 16 | class KimaiApiTypesParser 17 | { 18 | public: 19 | /*! 20 | * 21 | * \throw std::runtime_error if data is not json 22 | */ 23 | KimaiApiTypesParser(const QByteArray& data); 24 | 25 | template std::vector getArrayOf() const 26 | { 27 | if (!m_jsonDocument.isArray()) 28 | { 29 | throw std::runtime_error("JSON value is not an array"); 30 | } 31 | return parseArrayOf(m_jsonDocument.array()); 32 | } 33 | 34 | template T getValueOf() const 35 | { 36 | if (!m_jsonDocument.isObject()) 37 | { 38 | throw std::runtime_error("JSON value is not an object"); 39 | } 40 | return parseValue(m_jsonDocument.object()); 41 | } 42 | 43 | static QJsonValue toJson(const TimeSheet& inst, TimeSheetConfig::TrackingMode trackingMode); 44 | static QJsonValue toJson(const Customer& inst); 45 | static QJsonValue toJson(const Project& inst); 46 | static QJsonValue toJson(const Activity& inst); 47 | 48 | private: 49 | template T parseValue(const QJsonValue& jsonValue) const; 50 | 51 | template std::vector parseArrayOf(const QJsonArray& jsonArray) const 52 | { 53 | std::vector values; 54 | for (const auto& jsonValue : jsonArray) 55 | { 56 | values.push_back(parseValue(jsonValue)); 57 | } 58 | return values; 59 | } 60 | 61 | QJsonDocument m_jsonDocument; 62 | }; 63 | 64 | } // namespace kemai 65 | -------------------------------------------------------------------------------- /src/context/kemaiSession.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "client/kimaiAPI.h" 8 | #include "client/kimaiCache.h" 9 | #include "client/kimaiClient.h" 10 | #include "monitor/kimaiEventsMonitor.h" 11 | 12 | namespace kemai { 13 | 14 | class KemaiSession : public QObject 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | KemaiSession(const std::shared_ptr& kimaiClient); 20 | ~KemaiSession() override; 21 | 22 | std::shared_ptr client() const; 23 | const KimaiCache& cache() const; 24 | 25 | /* 26 | * send some request to identify instance 27 | */ 28 | void refreshSessionInfos(); 29 | void refreshCurrentTimeSheet(); 30 | void refreshCache(KimaiCache::Category category); 31 | void refreshCache(const std::set& categories = {}); 32 | 33 | bool hasPlugin(ApiPlugin apiPlugin) const; 34 | QVersionNumber kimaiVersion() const; 35 | User me() const; 36 | TimeSheetConfig timeSheetConfig() const; 37 | 38 | std::optional currentTimeSheet() const; 39 | bool hasCurrentTimeSheet() const; 40 | 41 | QDateTime computeTZDateTime(const QDateTime& dateTime) const; 42 | 43 | signals: 44 | void pluginsChanged(); 45 | void currentTimeSheetChanged(); 46 | void versionChanged(); 47 | void meChanged(); 48 | void timeSheetConfigChanged(); 49 | 50 | private: 51 | void onClientError(KimaiApiBaseResult* apiBaseResult); 52 | 53 | void requestMe(); 54 | void requestVersion(); 55 | void requestTimeSheetConfig(); 56 | void requestPlugins(); 57 | 58 | std::shared_ptr mKimaiClient; 59 | KimaiCache mKimaiCache; 60 | KimaiEventsMonitor mKimaiMonitor; 61 | QVersionNumber mKimaiVersion; 62 | Plugins mPlugins; 63 | User mMe; 64 | TimeSheetConfig mTimeSheetConfig; 65 | }; 66 | 67 | } // namespace kemai 68 | -------------------------------------------------------------------------------- /src/gui/customerDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "customerDialog.h" 2 | #include "ui_customerDialog.h" 3 | 4 | #include "misc/dataReader.h" 5 | 6 | #include 7 | #include 8 | 9 | using namespace kemai; 10 | 11 | CustomerDialog::CustomerDialog(QWidget* parent) : QDialog(parent), mUi(std::make_unique()) 12 | { 13 | mUi->setupUi(this); 14 | enableSave(false); 15 | 16 | connect(mUi->leName, &QLineEdit::textChanged, this, &CustomerDialog::validateForm); 17 | connect(mUi->leTimeBudget, &QLineEdit::textChanged, this, &CustomerDialog::validateForm); 18 | 19 | const auto& countries = DataReader::countries(); 20 | for (auto it = countries.begin(); it != countries.end(); ++it) 21 | { 22 | mUi->cbCountry->addItem(it.value(), it.key()); 23 | } 24 | const auto& currencies = DataReader::currencies(); 25 | for (auto it = currencies.begin(); it != currencies.end(); ++it) 26 | { 27 | mUi->cbCurrency->addItem(it.value(), it.key()); 28 | } 29 | 30 | for (const auto& tz : QTimeZone::availableTimeZoneIds()) 31 | { 32 | mUi->cbTimezone->addItem(tz); 33 | } 34 | } 35 | 36 | CustomerDialog::~CustomerDialog() = default; 37 | 38 | Customer CustomerDialog::customer() const 39 | { 40 | Customer customer; 41 | customer.name = mUi->leName->text(); 42 | customer.countryKey = mUi->cbCountry->currentData(Qt::UserRole).toString(); 43 | customer.currencyKey = mUi->cbCurrency->currentData(Qt::UserRole).toString(); 44 | customer.timezone = mUi->cbTimezone->currentText(); 45 | customer.budget = mUi->sbBudget->value(); 46 | customer.timeBudget = mUi->leTimeBudget->seconds(); 47 | 48 | return customer; 49 | } 50 | 51 | void CustomerDialog::enableSave(bool enable) 52 | { 53 | if (auto btn = mUi->buttonBox->button(QDialogButtonBox::Save)) 54 | { 55 | btn->setEnabled(enable); 56 | } 57 | } 58 | 59 | void CustomerDialog::validateForm() 60 | { 61 | const auto& name = mUi->leName->text(); 62 | bool nameOk = (!name.isEmpty()) && (name.size() > 1); 63 | bool timeBudgetOk = mUi->leTimeBudget->hasAcceptableInput(); 64 | enableSave(nameOk && timeBudgetOk); 65 | } 66 | -------------------------------------------------------------------------------- /bundle/linux/create_appimage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run this script once Kemai is built. Install and AppImage will be handle here. 4 | # It just need path to Qt and to build directory. 5 | 6 | # Read args 7 | usage() { echo "Usage: $0 -b -o -q "; exit 0; } 8 | 9 | while getopts ":hb:o:q:" o; do 10 | case "${o}" in 11 | b) 12 | BUILD_DIR=${OPTARG} 13 | ;; 14 | o) 15 | OPENSSL_DIR=${OPTARG} 16 | ;; 17 | q) 18 | QT_DIR=${OPTARG} 19 | ;; 20 | h | *) 21 | usage 22 | ;; 23 | esac 24 | done 25 | 26 | if [[ -z ${BUILD_DIR+x} ]] || [[ -z ${OPENSSL_DIR+x} ]] || [[ -z ${QT_DIR+x} ]]; then 27 | usage 28 | fi 29 | 30 | # Cleanup 31 | APPIMAGE_DESTDIR=${BUILD_DIR}/AppDir 32 | rm -r ${APPIMAGE_DESTDIR} 33 | 34 | # Run install to AppDir and add linux files 35 | DESTDIR=AppDir cmake --build ${BUILD_DIR} --target install || exit 1 36 | 37 | # Copy sysroot 38 | cp -rv bundle/linux/sysroot/* ${APPIMAGE_DESTDIR}/ 39 | chmod +x ${APPIMAGE_DESTDIR}/usr/bin/kemai-wrapper.sh 40 | 41 | # Gets AppImage tools 42 | wget -nc -P ${BUILD_DIR}/ https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage 43 | wget -nc -P ${BUILD_DIR}/ https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage 44 | chmod +x ${BUILD_DIR}/linuxdeploy*.AppImage 45 | 46 | # Export vars 47 | export QMAKE=${QT_DIR}/bin/qmake 48 | export LD_LIBRARY_PATH=${QT_DIR}/lib 49 | export VERSION=$(cat ${BUILD_DIR}/version.txt) 50 | export EXTRA_QT_PLUGINS=platforms,iconengines,wayland-decoration-client,wayland-graphics-integration-client,wayland-shell-integration,platformthemes,tls 51 | 52 | # Copy OpenSSLv3 binaries 53 | OPENSSL_APPDIR=${APPIMAGE_DESTDIR}/fallback/libssl.so.3 54 | mkdir -p ${OPENSSL_APPDIR} 55 | cp -vL ${OPENSSL_DIR}/libssl.so* ${OPENSSL_APPDIR}/ 56 | cp -vL ${OPENSSL_DIR}/libcrypto.so* ${OPENSSL_APPDIR}/ 57 | 58 | # Run appimage builder 59 | ${BUILD_DIR}/linuxdeploy-x86_64.AppImage \ 60 | --appimage-extract-and-run \ 61 | --appdir ${APPIMAGE_DESTDIR} \ 62 | --plugin qt \ 63 | -d ${APPIMAGE_DESTDIR}/kemai-wrapper.desktop \ 64 | -i ${APPIMAGE_DESTDIR}/kemai-wrapper.png \ 65 | --output appimage 66 | -------------------------------------------------------------------------------- /src/client/kimaiReply.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace kemai { 10 | 11 | /*! 12 | * API Request result base class 13 | * This class is just here because of Q_OBJECT limitation : This macro can't be used in template class 14 | * So we define a base class that just defines the signal that we want to emit in the templated class 15 | */ 16 | class KimaiApiBaseResult : public QObject 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | /*! 22 | * Mark the request result as ready 23 | * It will emit the ready finished 24 | */ 25 | void markAsReady(); 26 | 27 | bool hasError() const; 28 | 29 | /*! 30 | * Mark the request result as ready 31 | * It will emit the ready finished 32 | */ 33 | void setError(const QString& errorMessage); 34 | 35 | QString errorMessage() const; 36 | 37 | signals: 38 | void ready(); 39 | void error(); 40 | 41 | private: 42 | std::atomic mIsReady = false; 43 | std::optional mError; 44 | }; 45 | 46 | template class KimaiApiResult : public KimaiApiBaseResult 47 | { 48 | public: 49 | /*! 50 | * Return the wrapped result 51 | * Note that the return result is undefined before ready signal receiving 52 | * \return A reference on the request result 53 | */ 54 | const ResultType& getResult() const { return m_result; } 55 | 56 | /*! 57 | * Set the request result 58 | * Calling this method lead to the ready signal emission 59 | * \param[in] result Reference on the request result 60 | */ 61 | void setResult(const ResultType& result) 62 | { 63 | m_result = result; 64 | 65 | this->markAsReady(); 66 | } 67 | 68 | /*! 69 | * Set the request result (move version) 70 | * Calling this method lead to the ready signal emission 71 | * \param[in] result Reference on the request result 72 | */ 73 | void setResult(ResultType&& result) 74 | { 75 | m_result = std::move(result); 76 | 77 | this->markAsReady(); 78 | } 79 | 80 | template 81 | typename std::enable_if::value, Q>::type takeResult() 82 | { 83 | return std::move(m_result); 84 | } 85 | 86 | private: 87 | ResultType m_result; 88 | }; 89 | 90 | } // namespace kemai 91 | -------------------------------------------------------------------------------- /src/updater/kemaiUpdater.cpp: -------------------------------------------------------------------------------- 1 | #include "kemaiUpdater.h" 2 | #include "kemaiUpdater_p.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "misc/customFmt.h" 10 | 11 | using namespace kemai; 12 | 13 | /* 14 | * Private impl 15 | */ 16 | KemaiUpdater::KemaiUpdaterPrivate::KemaiUpdaterPrivate(KemaiUpdater* c) : networkAccessManager(new QNetworkAccessManager), mQ(c) 17 | { 18 | connect(networkAccessManager.data(), &QNetworkAccessManager::finished, this, &KemaiUpdaterPrivate::onNamFinished); 19 | } 20 | 21 | QNetworkRequest KemaiUpdater::KemaiUpdaterPrivate::prepareGithubRequest(const QString& path) 22 | { 23 | QUrl url; 24 | url.setScheme("https"); 25 | url.setHost("api.github.com"); 26 | url.setPath(path); 27 | 28 | QNetworkRequest r; 29 | r.setUrl(url); 30 | r.setRawHeader("accept", "application/vnd.github.v3+json"); 31 | return r; 32 | } 33 | 34 | void KemaiUpdater::KemaiUpdaterPrivate::onNamFinished(QNetworkReply* reply) 35 | { 36 | if (reply->error() != QNetworkReply::NoError) 37 | { 38 | spdlog::error("Error on update check: {}", reply->errorString()); 39 | } 40 | else 41 | { 42 | // Parse json 43 | auto jdoc = QJsonDocument::fromJson(reply->readAll()); 44 | auto jobj = jdoc.object(); 45 | 46 | auto newVersion = QVersionNumber::fromString(jobj.value("tag_name").toString()); 47 | if (newVersion > currentCheckSinceVersion) 48 | { 49 | VersionDetails vd; 50 | vd.vn = newVersion; 51 | vd.description = jobj.value("body").toString(); 52 | vd.url = jobj.value("html_url").toString(); 53 | 54 | emit mQ->checkFinished(vd); 55 | } 56 | else if (!silenceIfNoNew) 57 | { 58 | emit mQ->checkFinished(VersionDetails()); 59 | } 60 | } 61 | } 62 | 63 | /* 64 | * Public impl 65 | */ 66 | KemaiUpdater::KemaiUpdater(QObject* parent) : QObject(parent), mD(new KemaiUpdaterPrivate(this)) {} 67 | 68 | KemaiUpdater::~KemaiUpdater() = default; 69 | 70 | void KemaiUpdater::checkAvailableNewVersion(const QVersionNumber& sinceVersion, bool silenceIfNoNew) 71 | { 72 | mD->currentCheckSinceVersion = sinceVersion; 73 | mD->silenceIfNoNew = silenceIfNoNew; 74 | 75 | auto rq = mD->prepareGithubRequest("/repos/AlexandrePTJ/kemai/releases/latest"); 76 | mD->networkAccessManager->get(rq); 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kemai, a Kimai Desktop Client 2 | 3 | > ⚠️ **Looking for a C++ developer with a Mac**: 4 | I need help debugging a specific issue on macOS (#120). Please reach out if interested! 5 | 6 | ## Build Status 7 | 8 | |Develop|Master|Translation| 9 | |:--:|:--:|:--:| 10 | |![Build status](https://github.com/AlexandrePTJ/kemai/actions/workflows/main.yml/badge.svg?branch=develop)|![Build status](https://github.com/AlexandrePTJ/kemai/actions/workflows/main.yml/badge.svg?branch=master)|[![Translation status](https://hosted.weblate.org/widgets/kemai/-/kemai/svg-badge.svg)](https://hosted.weblate.org/engage/kemai/)| 11 | 12 | ## How to install 13 | 14 | Several methods to install _Kemai_ are available. 15 | 16 | 17 | ### From GitHub releases 18 | 19 | Pre-build packages are available from [Releases](https://github.com/AlexandrePTJ/kemai/releases/latest). Just select the asset for your OS. 20 | 21 | Binaries with `-NoUpdate` suffix will not automaticaly check for new update. Thoses binaries are intent to be use with OS specifics packages manager. 22 | 23 | 24 | ### From Homebrew (mac OS only) 25 | 26 | A [Tap](https://github.com/AlexandrePTJ/homebrew-cask) repository is available to simplify _Kemai_ install through `brew` 27 | 28 | ```shell script 29 | > brew tap alexandreptj/cask 30 | > brew install kemai 31 | ``` 32 | 33 | 34 | ### From source 35 | 36 | _Kemai_ is Qt6 based application. It uses CMake to build. So regular cmake process will work. 37 | 38 | ```shell script 39 | > cmake . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH= 40 | > cmake --build build --config Release 41 | ``` 42 | 43 | 44 | ## How to use 45 | 46 | _Kemai_ connects to your _Kimai_ instance through its API. As credentials for API are not the same as login, here is how to create them: 47 | 48 | ![API password](https://github.com/AlexandrePTJ/kemai/blob/master/docs/api_password.gif) 49 | 50 | Then, you can set this credentials to _Kemai_ settings : 51 | 52 | ![Kemai settings](https://github.com/AlexandrePTJ/kemai/blob/master/docs/kemai_settings.gif) 53 | 54 | 55 | ## How to help 56 | 57 | Ideas, pull requests and translation are welcome. 58 | 59 | For the later, [Weblate](https://hosted.weblate.org/engage/kemai/) is used. Here is the current status: 60 | 61 | [![Translation status](https://hosted.weblate.org/widgets/kemai/-/kemai/multi-auto.svg)](https://hosted.weblate.org/engage/kemai/) 62 | 63 | 64 | ## Why Kemai 65 | 66 | Because "Il n'y a que Maille qui m'aille". 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | First off, thank you for considering contributing to Kemai. It's people like you that make it such a great tool. 4 | 5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests. 6 | 7 | ### kinds of contributions 8 | 9 | Kemai is an open source project, and we love to receive contributions from our community. There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into Kemai itself. 10 | 11 | ### No expected 12 | 13 | Please, don't use the issue tracker for support questions. Check whether the [discussions](https://github.com/AlexandrePTJ/kemai/discussions) tab on this GitHub project to find help with your issue. 14 | 15 | ## Responsibilities 16 | * Ensure cross-platform compatibility for every change that's accepted. Windows, Mac, Linux. [Actions](https://github.com/AlexandrePTJ/kemai/actions) should help. 17 | * Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. 18 | * Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. 19 | 20 | ## Making Changes 21 | 22 | Changes are welcome via GitHub pull requests. If you are new to the project and looking for a way to get involved, try picking up an issue with a "good first issue" label. Hints about what needs to be done are usually provided. 23 | 24 | For all contributions, please respect the following guidelines: 25 | 26 | * Each pull request should implement ONE feature or bugfix. If you want to add or fix more than one thing, submit more than one pull request. 27 | * Always start a new feature from `develop` branch. 28 | * Do not commit changes to files that are irrelevant to your feature or bugfix (eg: `.gitignore`). 29 | * Be aware that the pull request review process is not immediate, and is generally proportional to the size of the pull request. 30 | * If your pull request is merged, please do not ask for an immediate release. 31 | * Code formatting debates can be endless. Please ensure to use `clang-format` with root `.clang-format` file before any pull request. 32 | * Coding style must be as consistent as possible. Good full classes are `KimaiCache` and `KemaiSession`. 33 | -------------------------------------------------------------------------------- /src/models/loggerTreeModel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace kemai { 13 | 14 | /* 15 | * Log entry 16 | */ 17 | struct LoggerEntry 18 | { 19 | QDateTime dateTime; 20 | QString message; 21 | spdlog::level::level_enum level; 22 | }; 23 | 24 | /* 25 | * Model for Model-View 26 | */ 27 | class LoggerTreeModel : public QAbstractItemModel 28 | { 29 | Q_OBJECT 30 | 31 | public: 32 | LoggerTreeModel(QObject* parent = nullptr); 33 | ~LoggerTreeModel() override; 34 | 35 | QModelIndex index(int row, int column, const QModelIndex& parent) const override; 36 | QModelIndex parent(const QModelIndex& child) const override; 37 | int rowCount(const QModelIndex& parent) const override; 38 | int columnCount(const QModelIndex& parent) const override; 39 | QVariant data(const QModelIndex& index, int role) const override; 40 | QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 41 | 42 | public slots: // NOLINT(readability-redundant-access-specifiers) 43 | void sinkLog(const kemai::LoggerEntry& entry); 44 | 45 | private: 46 | std::list mEntries; 47 | QMutex mSinkMutex; 48 | }; 49 | 50 | /* 51 | * Sink to bridge between spdlog and Qt 52 | */ 53 | class LoggerTreeModelSink : public spdlog::sinks::base_sink 54 | { 55 | public: 56 | LoggerTreeModelSink(const std::shared_ptr& loggerTreeModel) : mLoggerTreeModel(loggerTreeModel) {} 57 | ~LoggerTreeModelSink() override = default; 58 | 59 | protected: 60 | void sink_it_(const spdlog::details::log_msg& msg) override 61 | { 62 | const auto timeInMSec = std::chrono::time_point_cast(msg.time); 63 | 64 | // clang-format off 65 | LoggerEntry entry{ 66 | QDateTime::fromMSecsSinceEpoch(timeInMSec.time_since_epoch().count(), Qt::UTC), 67 | QString::fromUtf8(msg.payload.data(), static_cast(msg.payload.size())).remove("<===").remove("===>").trimmed(), 68 | msg.level}; 69 | // clang-format on 70 | 71 | QMetaObject::invokeMethod(mLoggerTreeModel.get(), "sinkLog", Qt::AutoConnection, Q_ARG(kemai::LoggerEntry, entry)); 72 | } 73 | void flush_() override {} 74 | 75 | private: 76 | std::shared_ptr mLoggerTreeModel; 77 | }; 78 | 79 | } // namespace kemai 80 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 21, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "default", 11 | "hidden": true, 12 | "displayName": "Default Config", 13 | "description": "Default build using Ninja generator", 14 | "generator": "Ninja", 15 | "binaryDir": "${sourceDir}/cmake-build-${presetName}", 16 | "cacheVariables": { 17 | "CMAKE_INSTALL_PREFIX": "/usr" 18 | } 19 | }, 20 | { 21 | "name": "default-windows", 22 | "hidden": true, 23 | "cacheVariables": { 24 | "CMAKE_PREFIX_PATH": "c:/Qt/6.7.2/msvc2019_64", 25 | "OPENSSL_ROOT": "c:/Qt/Tools/OpenSSLv3/Win_x64", 26 | "CMAKE_INSTALL_PREFIX": "dist" 27 | } 28 | }, 29 | { 30 | "name": "default-macos", 31 | "hidden": true, 32 | "cacheVariables": { 33 | "CMAKE_PREFIX_PATH": "$env{HOME}/Qt/6.7.2/macos", 34 | "CMAKE_INSTALL_PREFIX": "dist" 35 | } 36 | }, 37 | { 38 | "name": "debug", 39 | "inherits": "default", 40 | "cacheVariables": { 41 | "CMAKE_BUILD_TYPE": "Debug" 42 | } 43 | }, 44 | { 45 | "name": "release", 46 | "inherits": "default", 47 | "cacheVariables": { 48 | "CMAKE_BUILD_TYPE": "Release" 49 | } 50 | }, 51 | { 52 | "name": "debug-win", 53 | "inherits": ["default-windows", "debug"] 54 | }, 55 | { 56 | "name": "release-win", 57 | "inherits": ["default-windows", "release"] 58 | }, 59 | { 60 | "name": "release-macos", 61 | "inherits": ["default-macos", "release"] 62 | }, 63 | { 64 | "name": "release-macos-noupdate", 65 | "inherits": ["default-macos", "release"], 66 | "cacheVariables": { 67 | "KEMAI_ENABLE_UPDATE_CHECK": "OFF" 68 | } 69 | } 70 | ], 71 | "buildPresets": [ 72 | { 73 | "name": "debug", 74 | "configurePreset": "debug" 75 | }, 76 | { 77 | "name": "release", 78 | "configurePreset": "release" 79 | }, 80 | { 81 | "name": "debug-win", 82 | "configurePreset": "debug-win" 83 | }, 84 | { 85 | "name": "release-win", 86 | "configurePreset": "release-win" 87 | }, 88 | { 89 | "name": "release-macos", 90 | "configurePreset": "release-macos" 91 | }, 92 | { 93 | "name": "release-macos-noupdate", 94 | "configurePreset": "release-macos-noupdate" 95 | } 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /src/gui/taskWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TaskWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 299 10 | 333 11 | 12 | 13 | 14 | TaskWidget 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Filter 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ... 36 | 37 | 38 | 39 | :/icons/refresh:/icons/refresh 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | QAbstractItemView::EditTrigger::NoEditTriggers 49 | 50 | 51 | true 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | false 61 | 62 | 63 | Start 64 | 65 | 66 | 67 | 68 | 69 | 70 | false 71 | 72 | 73 | Close 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | leFilter 83 | tbRefresh 84 | lvTasks 85 | btStartStop 86 | btClose 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/monitor/windowsDesktopEventsMonitor.cpp: -------------------------------------------------------------------------------- 1 | #include "windowsDesktopEventsMonitor.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | using namespace kemai; 9 | 10 | WindowsDesktopEventsMonitor::WindowsDesktopEventsMonitor(QWidget* widget) : DesktopEventsMonitor(true, true), mListenerWidget(widget) 11 | { 12 | connect(&mPollTimer, &QTimer::timeout, this, &WindowsDesktopEventsMonitor::onPollTimeout); 13 | 14 | qApp->installNativeEventFilter(this); 15 | } 16 | 17 | void WindowsDesktopEventsMonitor::initialize(const Settings::Events& eventsSettings) 18 | { 19 | stop(); 20 | 21 | mEventsSettings = eventsSettings; 22 | } 23 | 24 | void WindowsDesktopEventsMonitor::start() 25 | { 26 | if (mEventsSettings.stopOnIdle) 27 | { 28 | mPollTimer.start(std::chrono::seconds(1)); 29 | } 30 | 31 | if (mListenerWidget != nullptr) 32 | { 33 | if (mEventsSettings.stopOnLock) 34 | { 35 | WTSRegisterSessionNotification((HWND)mListenerWidget->winId(), NOTIFY_FOR_THIS_SESSION); 36 | } 37 | else 38 | { 39 | WTSUnRegisterSessionNotification((HWND)mListenerWidget->winId()); 40 | } 41 | } 42 | } 43 | 44 | void WindowsDesktopEventsMonitor::stop() 45 | { 46 | mPollTimer.stop(); 47 | if (mListenerWidget != nullptr) 48 | { 49 | WTSUnRegisterSessionNotification((HWND)mListenerWidget->winId()); 50 | } 51 | } 52 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 53 | bool WindowsDesktopEventsMonitor::nativeEventFilter(const QByteArray& eventType, void* message, long* /*result*/) 54 | #else 55 | bool WindowsDesktopEventsMonitor::nativeEventFilter(const QByteArray& eventType, void* message, qintptr* /*result*/) 56 | #endif // 57 | { 58 | if (eventType != "windows_generic_MSG" || !mEventsSettings.stopOnLock) 59 | { 60 | return false; 61 | } 62 | 63 | MSG* msg = static_cast(message); 64 | switch (msg->message) 65 | { 66 | case WM_WTSSESSION_CHANGE: 67 | if (msg->wParam == WTS_SESSION_LOCK || msg->wParam == WTS_SESSION_LOGOFF) 68 | { 69 | emit lockDetected(); 70 | } 71 | break; 72 | 73 | default: 74 | break; 75 | } 76 | return false; 77 | } 78 | 79 | void WindowsDesktopEventsMonitor::onPollTimeout() 80 | { 81 | LASTINPUTINFO lastInputInfo; 82 | lastInputInfo.cbSize = sizeof(LASTINPUTINFO); 83 | 84 | GetLastInputInfo(&lastInputInfo); 85 | auto idleSinceNMilliSecs = std::chrono::milliseconds(GetTickCount() - lastInputInfo.dwTime); 86 | 87 | if (mEventsSettings.stopOnIdle && mEventsSettings.idleDelayMinutes <= std::chrono::duration_cast(idleSinceNMilliSecs).count()) 88 | { 89 | emit idleDetected(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/client/kimaiClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "client/kimaiAPI.h" 9 | #include "client/kimaiReply.h" 10 | 11 | namespace kemai { 12 | 13 | using VersionRequestResult = KimaiApiResult*; 14 | using MeRequestResult = KimaiApiResult*; 15 | using TimeSheetConfigResult = KimaiApiResult*; 16 | using PluginsResult = KimaiApiResult*; 17 | using CustomersResult = KimaiApiResult*; 18 | using TimeSheetsResult = KimaiApiResult*; 19 | using ProjectsResult = KimaiApiResult*; 20 | using ActivitiesResult = KimaiApiResult*; 21 | using CustomerAddResult = KimaiApiResult*; 22 | using ProjectAddResult = KimaiApiResult*; 23 | using ActivityAddResult = KimaiApiResult*; 24 | using TimeSheetResult = KimaiApiResult*; 25 | using TaskResult = KimaiApiResult*; 26 | using TasksResult = KimaiApiResult*; 27 | 28 | class KimaiClient : public QObject 29 | { 30 | Q_OBJECT 31 | 32 | public: 33 | explicit KimaiClient(QObject* parent = nullptr); 34 | ~KimaiClient() override; 35 | 36 | void setHost(const QString& host); 37 | QString host() const; 38 | 39 | void setLegacyAuth(const QString& username, const QString& token); 40 | bool isUsingLegacyAuth() const; 41 | 42 | void setAPIToken(const QString& token); 43 | 44 | VersionRequestResult requestKimaiVersion(); 45 | MeRequestResult requestMeUserInfo(); 46 | TimeSheetConfigResult requestTimeSheetConfig(); 47 | PluginsResult requestPlugins(); 48 | CustomersResult requestCustomers(); 49 | TimeSheetsResult requestActiveTimeSheets(); 50 | TimeSheetsResult requestRecentTimeSheets(); 51 | ProjectsResult requestProjects(std::optional customerId = std::nullopt); 52 | ActivitiesResult requestActivities(std::optional projectId = std::nullopt); 53 | 54 | CustomerAddResult addCustomer(const Customer& customer); 55 | ProjectAddResult addProject(const Project& project); 56 | ActivityAddResult addActivity(const Activity& activity); 57 | 58 | TimeSheetResult startTimeSheet(const TimeSheet& timeSheet, TimeSheetConfig::TrackingMode trackingMode); 59 | TimeSheetResult updateTimeSheet(const TimeSheet& timeSheet, TimeSheetConfig::TrackingMode trackingMode); 60 | 61 | TasksResult requestTasks(); 62 | TaskResult startTask(int taskId); 63 | TaskResult closeTask(int taskId); 64 | 65 | static void addTrustedCertificates(const QStringList& trustedCertificates); 66 | 67 | signals: 68 | void requestError(const QString& errorMsg); 69 | void sslError(const QString& msg, const QByteArray& certSN, const QByteArray& certPem); 70 | 71 | private: 72 | class KimaiClientPrivate; 73 | QScopedPointer mD; 74 | }; 75 | 76 | } // namespace kemai 77 | -------------------------------------------------------------------------------- /src/monitor/kimaiEventsMonitor.cpp: -------------------------------------------------------------------------------- 1 | #include "kimaiEventsMonitor.h" 2 | 3 | #include 4 | 5 | #include "misc/customFmt.h" 6 | #include "settings/settings.h" 7 | 8 | using namespace kemai; 9 | 10 | KimaiEventsMonitor::KimaiEventsMonitor(std::shared_ptr kimaiClient) : mKimaiClient(std::move(kimaiClient)) 11 | { 12 | connect(&mSecondTimer, &QTimer::timeout, this, &KimaiEventsMonitor::onSecondTimeout); 13 | 14 | mSecondTimer.setInterval(std::chrono::seconds(1)); 15 | mSecondTimer.setTimerType(Qt::PreciseTimer); 16 | mSecondTimer.start(); 17 | } 18 | 19 | KimaiEventsMonitor::~KimaiEventsMonitor() = default; 20 | 21 | void KimaiEventsMonitor::refreshCurrentTimeSheet() 22 | { 23 | auto activeTimeSheetsResult = mKimaiClient->requestActiveTimeSheets(); 24 | connect(activeTimeSheetsResult, &KimaiApiBaseResult::ready, this, [this, activeTimeSheetsResult] { onActiveTimeSheetsReceived(activeTimeSheetsResult); }); 25 | connect(activeTimeSheetsResult, &KimaiApiBaseResult::error, this, [this, activeTimeSheetsResult]() { onClientError(activeTimeSheetsResult); }); 26 | } 27 | 28 | std::optional KimaiEventsMonitor::currentTimeSheet() const 29 | { 30 | return mCurrentTimeSheet; 31 | } 32 | 33 | bool KimaiEventsMonitor::hasCurrentTimeSheet() const 34 | { 35 | return mCurrentTimeSheet.has_value(); 36 | } 37 | 38 | void KimaiEventsMonitor::onSecondTimeout() 39 | { 40 | auto settings = SettingsHelper::load(); 41 | if (settings.events.autoRefreshCurrentTimeSheet && mLastTimeSheetUpdate.has_value()) 42 | { 43 | if (mLastTimeSheetUpdate->secsTo(QDateTime::currentDateTime()) >= settings.events.autoRefreshCurrentTimeSheetDelaySeconds) 44 | { 45 | refreshCurrentTimeSheet(); 46 | } 47 | } 48 | } 49 | 50 | void KimaiEventsMonitor::onClientError(KimaiApiBaseResult* apiBaseResult) 51 | { 52 | spdlog::error("Client error: {}", apiBaseResult->errorMessage()); 53 | apiBaseResult->deleteLater(); 54 | } 55 | 56 | void KimaiEventsMonitor::onActiveTimeSheetsReceived(TimeSheetsResult timeSheetsResult) 57 | { 58 | const auto& timeSheets = timeSheetsResult->getResult(); 59 | 60 | bool firstRun = !mLastTimeSheetUpdate.has_value(); 61 | bool isRunning = mCurrentTimeSheet.has_value(); 62 | 63 | if (timeSheets.empty()) 64 | { 65 | if (isRunning || firstRun) 66 | { 67 | mCurrentTimeSheet.reset(); 68 | emit currentTimeSheetChanged(); 69 | } 70 | } 71 | else 72 | { 73 | auto timeSheet = timeSheets.front(); 74 | bool isSame = isRunning && timeSheet.id == mCurrentTimeSheet->id; 75 | 76 | if (!isSame || firstRun || !isRunning) 77 | { 78 | mCurrentTimeSheet = timeSheet; 79 | emit currentTimeSheetChanged(); 80 | } 81 | } 82 | 83 | mLastTimeSheetUpdate = QDateTime::currentDateTime(); 84 | timeSheetsResult->deleteLater(); 85 | } 86 | -------------------------------------------------------------------------------- /cmake/WinDeployQt.cmake: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2017 Nathan Osman 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # Retrieve the absolute path to qmake and then use that path to find 24 | # the windeployqt binary 25 | get_target_property(_qmake_executable Qt::qmake IMPORTED_LOCATION) 26 | get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY) 27 | find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${_qt_bin_dir}") 28 | 29 | # Add commands that copy the Qt runtime to the target's output directory after 30 | # build and install the Qt runtime to the specified directory 31 | function(windeployqt target directory) 32 | 33 | # install(CODE ...) doesn't support generator expressions, but 34 | # file(GENERATE ...) does - store the path in a file 35 | file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${target}" 36 | CONTENT "$" 37 | ) 38 | 39 | # Before installation, run a series of commands that copy each of the Qt 40 | # runtime files to the appropriate directory for installation 41 | install(CODE 42 | " 43 | file(READ \"${CMAKE_CURRENT_BINARY_DIR}/${target}\" _file) 44 | execute_process( 45 | COMMAND \"${CMAKE_COMMAND}\" -E 46 | env PATH=\"${_qt_bin_dir}\" \"${WINDEPLOYQT_EXECUTABLE}\" 47 | --no-compiler-runtime 48 | --no-opengl-sw 49 | --dir \${CMAKE_INSTALL_PREFIX}/${directory} 50 | \${_file} 51 | OUTPUT_VARIABLE _output 52 | OUTPUT_STRIP_TRAILING_WHITESPACE 53 | ) 54 | " 55 | ) 56 | set(CMAKE_INSTALL_UCRT_LIBRARIES TRUE) 57 | set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION ${directory}) 58 | include(InstallRequiredSystemLibraries) 59 | 60 | endfunction() 61 | 62 | mark_as_advanced(WINDEPLOYQT_EXECUTABLE) 63 | -------------------------------------------------------------------------------- /src/gui/mainWindow.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "client/kimaiClient.h" 11 | #include "context/kemaiSession.h" 12 | #include "gui/loggerWidget.h" 13 | #include "monitor/desktopEventsMonitor.h" 14 | #include "settings/settings.h" 15 | #include "updater/kemaiUpdater.h" 16 | 17 | namespace Ui { 18 | class MainWindow; 19 | } 20 | 21 | namespace kemai { 22 | 23 | class ActivityWidget; 24 | class TaskWidget; 25 | 26 | class MainWindow : public QMainWindow 27 | { 28 | Q_OBJECT 29 | 30 | public: 31 | MainWindow(); 32 | ~MainWindow() override; 33 | 34 | void setLoggerTreeModel(const std::shared_ptr& model); 35 | 36 | protected: 37 | void closeEvent(QCloseEvent* event) override; 38 | void hideEvent(QHideEvent* event) override; 39 | 40 | private: 41 | void createKemaiSession(const Settings::Profile& profile); 42 | void showSelectedView(); 43 | void setViewActionsEnabled(bool enable); 44 | void updateProfilesMenu(); 45 | void processAutoConnect(); 46 | 47 | void onCurrentTimeSheetChanged(); 48 | void onPluginsChanged(); 49 | void onSessionVersionChanged(); 50 | void onActionSettingsTriggered(); 51 | void onActionCheckUpdateTriggered(); 52 | void onActionOpenHostTriggered(); 53 | void onActionRefreshCacheTriggered(); 54 | void onActionAboutKemaiTriggered(); 55 | void onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason); 56 | void onNewVersionCheckFinished(const VersionDetails& details); 57 | void onActivityChanged(bool started); 58 | void onProfilesActionGroupTriggered(QAction* action); 59 | void onDesktopIdleDetected(); 60 | void onDesktopLockDetected(); 61 | 62 | std::unique_ptr mUi; 63 | KemaiUpdater mUpdater; 64 | std::shared_ptr mSession; 65 | std::shared_ptr mDesktopEventsMonitor; 66 | 67 | // Stacked widget (ownership is transferred, don't try to delete them) 68 | ActivityWidget* mActivityWidget = nullptr; 69 | TaskWidget* mTaskWidget = nullptr; 70 | 71 | // Actions 72 | QAction* mActQuit = nullptr; 73 | QAction* mActSettings = nullptr; 74 | QAction* mActCheckUpdate = nullptr; 75 | QAction* mActOpenHost = nullptr; 76 | QAction* mActViewActivities = nullptr; 77 | QAction* mActViewTasks = nullptr; 78 | QAction* mActRefreshCache = nullptr; 79 | QAction* mActAboutKemai = nullptr; 80 | QAction* mActShowLogWidget = nullptr; 81 | QActionGroup* mActGroupView = nullptr; 82 | QActionGroup* mActGroupProfiles = nullptr; 83 | 84 | // Menus 85 | QMenuBar* mMenuBar = nullptr; 86 | QMenu* mProfileMenu = nullptr; 87 | 88 | // Tray 89 | QMenu* mTrayMenu = nullptr; 90 | QSystemTrayIcon* mSystemTrayIcon = nullptr; 91 | 92 | // Status bar 93 | QLabel mStatusInstanceLabel; 94 | 95 | // Logger view 96 | LoggerWidget mLoggerWidget; 97 | }; 98 | 99 | } // namespace kemai 100 | -------------------------------------------------------------------------------- /src/gui/taskWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "taskWidget.h" 2 | #include "ui_taskWidget.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | using namespace kemai; 10 | 11 | TaskWidget::TaskWidget(QWidget* parent) : QWidget(parent), mUi(std::make_unique()) 12 | { 13 | mUi->setupUi(this); 14 | 15 | mTaskProxyModel.setSourceModel(&mTaskModel); 16 | mUi->lvTasks->setModel(&mTaskProxyModel); 17 | 18 | auto clearTextAction = mUi->leFilter->addAction(QIcon(":/icons/backspace"), QLineEdit::TrailingPosition); 19 | 20 | connect(clearTextAction, &QAction::triggered, mUi->leFilter, &QLineEdit::clear); 21 | connect(mUi->leFilter, &QLineEdit::textChanged, [&](const QString& text) { mTaskProxyModel.setFilterRegularExpression(QString(".*%1.*").arg(text)); }); 22 | connect(mUi->lvTasks->selectionModel(), &QItemSelectionModel::currentChanged, this, &TaskWidget::onTaskItemChanged); 23 | connect(mUi->btStartStop, &QPushButton::clicked, this, &TaskWidget::onStartStopClicked); 24 | connect(mUi->btClose, &QPushButton::clicked, this, &TaskWidget::onCloseClicked); 25 | connect(mUi->tbRefresh, &QPushButton::clicked, this, &TaskWidget::updateTasks); 26 | } 27 | 28 | TaskWidget::~TaskWidget() = default; 29 | 30 | void TaskWidget::setKemaiSession(std::shared_ptr kemaiSession) 31 | { 32 | mSession = std::move(kemaiSession); 33 | if (mSession) 34 | { 35 | mTaskProxyModel.setUserId(mSession->me().id); 36 | updateTasks(); 37 | } 38 | 39 | setEnabled(mSession != nullptr); 40 | } 41 | 42 | void TaskWidget::updateTasks() 43 | { 44 | auto tasksResult = mSession->client()->requestTasks(); 45 | 46 | connect(tasksResult, &KimaiApiBaseResult::ready, this, [this, tasksResult] { 47 | mTaskModel.setTasks(tasksResult->getResult()); 48 | tasksResult->deleteLater(); 49 | }); 50 | connect(tasksResult, &KimaiApiBaseResult::error, [tasksResult] { tasksResult->deleteLater(); }); 51 | } 52 | 53 | void TaskWidget::onTaskItemChanged(const QModelIndex& current, const QModelIndex& /*previous*/) 54 | { 55 | mUi->btStartStop->setEnabled(current.isValid()); 56 | mUi->btClose->setEnabled(current.isValid()); 57 | } 58 | 59 | void TaskWidget::onStartStopClicked() 60 | { 61 | auto taskId = mTaskModel.data(mTaskProxyModel.mapToSource(mUi->lvTasks->currentIndex()), TaskListModel::TaskIDRole).toInt(); 62 | 63 | auto taskResult = mSession->client()->startTask(taskId); 64 | connect(taskResult, &KimaiApiBaseResult::ready, this, [this, taskResult] { 65 | updateTasks(); 66 | taskResult->deleteLater(); 67 | }); 68 | connect(taskResult, &KimaiApiBaseResult::error, [taskResult] { taskResult->deleteLater(); }); 69 | } 70 | 71 | void TaskWidget::onCloseClicked() 72 | { 73 | auto taskId = mTaskModel.data(mTaskProxyModel.mapToSource(mUi->lvTasks->currentIndex()), TaskListModel::TaskIDRole).toInt(); 74 | 75 | auto taskResult = mSession->client()->closeTask(taskId); 76 | connect(taskResult, &KimaiApiBaseResult::ready, this, [this, taskResult] { 77 | updateTasks(); 78 | taskResult->deleteLater(); 79 | }); 80 | connect(taskResult, &KimaiApiBaseResult::error, [taskResult] { taskResult->deleteLater(); }); 81 | } 82 | -------------------------------------------------------------------------------- /src/client/kimaiAPI.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace kemai { 11 | 12 | enum class ApiPlugin 13 | { 14 | Unknown, 15 | TaskManagement 16 | }; 17 | 18 | // reply structs 19 | struct KimaiVersion 20 | { 21 | QVersionNumber kimai; 22 | }; 23 | 24 | using Tags = QStringList; 25 | 26 | struct Customer 27 | { 28 | int id = 0; 29 | bool visible = true; 30 | QString name; 31 | QString number; 32 | QString comment; 33 | QString company; 34 | QString address; 35 | QString countryKey; 36 | QString currencyKey; 37 | QString phone; 38 | QString fax; 39 | QString mobile; 40 | QString email; 41 | QString homepage; 42 | QString timezone; 43 | QString color; 44 | double budget = 0.0; 45 | int timeBudget = 0; // seconds 46 | }; 47 | using Customers = std::vector; 48 | 49 | struct Project 50 | { 51 | int id = 0; 52 | bool visible = true; 53 | QString name; 54 | Customer customer; 55 | QString comment; 56 | QString orderNumber; 57 | QString orderDate; 58 | QString start; 59 | QString end; 60 | QString color; 61 | double budget = 0.0; 62 | int timeBudget = 0; 63 | }; 64 | using Projects = std::vector; 65 | 66 | struct Activity 67 | { 68 | int id = 0; 69 | bool visible = true; 70 | QString name; 71 | QString comment; 72 | QString color; 73 | double budget = 0.0; 74 | int timeBudget = 0; 75 | std::optional project; 76 | }; 77 | using Activities = std::vector; 78 | 79 | struct TimeSheet 80 | { 81 | int id = 0; 82 | int user = 0; 83 | Activity activity; 84 | Project project; 85 | QString description; 86 | QDateTime beginAt; 87 | QDateTime endAt; 88 | Tags tags; 89 | }; 90 | using TimeSheets = std::vector; 91 | 92 | struct User 93 | { 94 | int id = 0; 95 | QString username; 96 | QString language; 97 | QString timezone; 98 | }; 99 | 100 | struct Task 101 | { 102 | enum class Status 103 | { 104 | Undefined, 105 | Pending, 106 | Progress, 107 | Closed 108 | }; 109 | 110 | int id = 0; 111 | QString title; 112 | Status status = Status::Undefined; 113 | QString todo; 114 | QString description; 115 | Project project; 116 | Activity activity; 117 | User user; 118 | QDateTime endAt; 119 | int estimation = 0; 120 | TimeSheets activeTimeSheets; 121 | }; 122 | using Tasks = std::vector; 123 | 124 | struct Plugin 125 | { 126 | QString name; 127 | QVersionNumber version; 128 | ApiPlugin apiPlugin; 129 | }; 130 | using Plugins = std::vector; 131 | 132 | struct TimeSheetConfig 133 | { 134 | enum class TrackingMode 135 | { 136 | Default, 137 | Punch, 138 | DurationFixedBegin, 139 | DurationOnly 140 | }; 141 | 142 | TrackingMode trackingMode = TrackingMode::Default; 143 | }; 144 | 145 | ApiPlugin pluginByName(const QString& pluginName); 146 | 147 | } // namespace kemai 148 | -------------------------------------------------------------------------------- /src/models/loggerTreeModel.cpp: -------------------------------------------------------------------------------- 1 | #include "loggerTreeModel.h" 2 | 3 | #include 4 | 5 | using namespace kemai; 6 | 7 | const auto MaxLogEntries = 500; 8 | 9 | /* 10 | * Static helpers 11 | */ 12 | static QVariant getDisplay(const LoggerEntry& entry, int column) 13 | { 14 | switch (column) 15 | { 16 | case 0: 17 | return entry.level; 18 | 19 | case 1: 20 | return entry.dateTime; 21 | 22 | case 2: 23 | return entry.message; 24 | 25 | default: 26 | return {}; 27 | } 28 | } 29 | 30 | static QVariant getForeground(spdlog::level::level_enum level) 31 | { 32 | switch (level) 33 | { 34 | case spdlog::level::trace: 35 | case spdlog::level::debug: 36 | return QColor(Qt::blue); 37 | 38 | case spdlog::level::info: 39 | return QColor(Qt::darkGreen); 40 | 41 | case spdlog::level::warn: 42 | return QColor(Qt::darkYellow); 43 | 44 | case spdlog::level::err: 45 | case spdlog::level::critical: 46 | return QColor(Qt::darkRed); 47 | 48 | default: 49 | return {}; 50 | } 51 | } 52 | 53 | /* 54 | * Public impl 55 | */ 56 | LoggerTreeModel::LoggerTreeModel(QObject* parent) : QAbstractItemModel(parent) {} 57 | 58 | LoggerTreeModel::~LoggerTreeModel() = default; 59 | 60 | QModelIndex LoggerTreeModel::index(int row, int column, const QModelIndex& /*parent*/) const 61 | { 62 | return createIndex(row, column); 63 | } 64 | 65 | QModelIndex LoggerTreeModel::parent(const QModelIndex& /*child*/) const 66 | { 67 | return {}; 68 | } 69 | 70 | int LoggerTreeModel::rowCount(const QModelIndex& parent) const 71 | { 72 | if (parent.isValid()) 73 | { 74 | return 0; 75 | } 76 | return static_cast(mEntries.size()); 77 | } 78 | 79 | int LoggerTreeModel::columnCount(const QModelIndex& parent) const 80 | { 81 | return 3; 82 | } 83 | 84 | QVariant LoggerTreeModel::data(const QModelIndex& index, int role) const 85 | { 86 | if (index.row() >= mEntries.size() || !index.isValid()) 87 | { 88 | return {}; 89 | } 90 | 91 | auto it = std::next(mEntries.begin(), index.row()); 92 | switch (role) 93 | { 94 | case Qt::DisplayRole: 95 | return getDisplay(*it, index.column()); 96 | 97 | case Qt::ForegroundRole: 98 | return getForeground(it->level); 99 | 100 | default: 101 | return {}; 102 | } 103 | } 104 | 105 | QVariant LoggerTreeModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const 106 | { 107 | if (role != Qt::DisplayRole) 108 | { 109 | return {}; 110 | } 111 | 112 | switch (section) 113 | { 114 | case 0: 115 | return tr("Level"); 116 | 117 | case 1: 118 | return tr("Date"); 119 | 120 | case 2: 121 | return tr("Message"); 122 | } 123 | 124 | return {}; 125 | } 126 | 127 | void LoggerTreeModel::sinkLog(const LoggerEntry& entry) 128 | { 129 | QMutexLocker lock(&mSinkMutex); 130 | 131 | const auto size = static_cast(mEntries.size()); 132 | if (size > MaxLogEntries - 1) 133 | { 134 | beginMoveRows({}, 1, size - 1, {}, 0); 135 | mEntries.push_back(entry); 136 | mEntries.pop_front(); 137 | endMoveRows(); 138 | } 139 | else 140 | { 141 | beginInsertRows({}, size, size + 1); 142 | mEntries.push_back(entry); 143 | endInsertRows(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "client/kimaiClient.h" 12 | #include "gui/mainWindow.h" 13 | #include "kemaiConfig.h" 14 | #include "misc/customFmt.h" 15 | #include "misc/helpers.h" 16 | #include "models/loggerTreeModel.h" 17 | #include "settings/settings.h" 18 | 19 | using namespace kemai; 20 | 21 | static constinit const auto MaxLogFileSize = 1024 * 102 * 5; 22 | 23 | void myMessageOutput(QtMsgType type, const QMessageLogContext& /*context*/, const QString& msg) 24 | { 25 | switch (type) 26 | { 27 | case QtDebugMsg: 28 | spdlog::debug(msg); 29 | break; 30 | case QtInfoMsg: 31 | spdlog::info(msg); 32 | break; 33 | case QtWarningMsg: 34 | spdlog::warn(msg); 35 | break; 36 | case QtCriticalMsg: 37 | case QtFatalMsg: 38 | spdlog::critical(msg); 39 | break; 40 | } 41 | } 42 | 43 | int main(int argc, char* argv[]) 44 | { 45 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 46 | QApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); 47 | QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); 48 | #endif 49 | 50 | qInstallMessageHandler(myMessageOutput); 51 | 52 | QApplication app(argc, argv); 53 | QApplication::setApplicationName("Kemai"); 54 | QApplication::setOrganizationName("Kemai"); 55 | QApplication::setApplicationVersion(KEMAI_VERSION); 56 | 57 | // Get kemai data directory and log file path 58 | const auto& kemaiSettings = SettingsHelper::load(); 59 | 60 | // Create Qt logger model before spdlog sinks 61 | auto loggerTreeModel = std::make_shared(); 62 | 63 | // Create spdlog sinks : console, rotating file (3 x 5Mb) and Qt Object 64 | std::vector sinks; 65 | sinks.emplace_back(std::make_shared()); 66 | sinks.emplace_back(std::make_shared(loggerTreeModel)); 67 | 68 | if (!QDir(helpers::getLogDirPath()).mkpath(".")) // Ensure log dir exists before adding sink 69 | { 70 | sinks.emplace_back(std::make_shared(helpers::getLogFilePath().toStdString(), MaxLogFileSize, 3)); 71 | } 72 | 73 | auto logger = std::make_shared("kemai", sinks.begin(), sinks.end()); 74 | spdlog::register_logger(logger); 75 | spdlog::set_level(spdlog::level::debug); 76 | spdlog::set_default_logger(logger); 77 | 78 | logger->info("===== Starting Kemai {} =====", KEMAI_VERSION); 79 | 80 | // Setup Qt and app translations 81 | QTranslator qtTranslator; 82 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 83 | if (qtTranslator.load("qt_" + kemaiSettings.kemai.language.name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath))) 84 | #else 85 | if (qtTranslator.load("qt_" + kemaiSettings.kemai.language.name(), QLibraryInfo::path(QLibraryInfo::TranslationsPath))) 86 | #endif 87 | { 88 | QApplication::installTranslator(&qtTranslator); 89 | } 90 | 91 | QTranslator appTranslator; 92 | if (appTranslator.load(kemaiSettings.kemai.language, "kemai", "_", ":/l10n")) 93 | { 94 | QApplication::installTranslator(&appTranslator); 95 | } 96 | 97 | // Setup trusted certificates 98 | KimaiClient::addTrustedCertificates(kemaiSettings.trustedCertificates); 99 | 100 | // Startup 101 | MainWindow mainWindow; 102 | mainWindow.restoreGeometry(kemaiSettings.kemai.geometry); 103 | mainWindow.setLoggerTreeModel(loggerTreeModel); 104 | mainWindow.show(); 105 | 106 | return QApplication::exec(); 107 | } 108 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: Consecutive 7 | AlignConsecutiveDeclarations: None 8 | AlignEscapedNewlines: Right 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: Always 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: Inline 15 | AllowShortIfStatementsOnASingleLine: Never 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: false 19 | AlwaysBreakTemplateDeclarations: MultiLine 20 | BinPackArguments: true 21 | BinPackParameters: true 22 | BraceWrapping: 23 | AfterClass: true 24 | AfterControlStatement: Always 25 | AfterEnum: true 26 | AfterFunction: true 27 | AfterNamespace: false 28 | AfterObjCDeclaration: false 29 | AfterStruct: true 30 | AfterUnion: true 31 | AfterExternBlock: false 32 | BeforeCatch: true 33 | BeforeElse: true 34 | IndentBraces: false 35 | SplitEmptyFunction: false 36 | SplitEmptyRecord: false 37 | SplitEmptyNamespace: false 38 | BreakBeforeBinaryOperators: None 39 | BreakBeforeBraces: Custom 40 | BreakBeforeInheritanceComma: false 41 | BreakInheritanceList: BeforeColon 42 | BreakBeforeTernaryOperators: true 43 | BreakConstructorInitializersBeforeComma: false 44 | BreakConstructorInitializers: BeforeColon 45 | BreakAfterJavaFieldAnnotations: false 46 | BreakStringLiterals: true 47 | ColumnLimit: 160 48 | CommentPragmas: '^ IWYU pragma:' 49 | CompactNamespaces: false 50 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 51 | ConstructorInitializerIndentWidth: 4 52 | ContinuationIndentWidth: 4 53 | Cpp11BracedListStyle: true 54 | DerivePointerAlignment: false 55 | DisableFormat: false 56 | ExperimentalAutoDetectBinPacking: false 57 | FixNamespaceComments: true 58 | ForEachMacros: 59 | - foreach 60 | - Q_FOREACH 61 | - BOOST_FOREACH 62 | IncludeBlocks: Preserve 63 | IncludeCategories: 64 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 65 | Priority: 2 66 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 67 | Priority: 3 68 | - Regex: '.*' 69 | Priority: 1 70 | IncludeIsMainRegex: '(Test)?$' 71 | IndentCaseLabels: false 72 | IndentPPDirectives: AfterHash 73 | IndentWidth: 4 74 | IndentWrappedFunctionNames: false 75 | JavaScriptQuotes: Leave 76 | JavaScriptWrapImports: true 77 | KeepEmptyLinesAtTheStartOfBlocks: true 78 | MacroBlockBegin: '' 79 | MacroBlockEnd: '' 80 | MaxEmptyLinesToKeep: 1 81 | NamespaceIndentation: None 82 | ObjCBinPackProtocolList: Auto 83 | ObjCBlockIndentWidth: 2 84 | ObjCSpaceAfterProperty: false 85 | ObjCSpaceBeforeProtocolList: true 86 | PenaltyBreakAssignment: 2 87 | PenaltyBreakBeforeFirstCallParameter: 19 88 | PenaltyBreakComment: 300 89 | PenaltyBreakFirstLessLess: 120 90 | PenaltyBreakString: 1000 91 | PenaltyBreakTemplateDeclaration: 10 92 | PenaltyExcessCharacter: 1000000 93 | PenaltyReturnTypeOnItsOwnLine: 60 94 | PointerAlignment: Left 95 | ReflowComments: true 96 | SortIncludes: CaseSensitive 97 | SortUsingDeclarations: true 98 | SpaceAfterCStyleCast: false 99 | SpaceAfterTemplateKeyword: false 100 | SpaceBeforeAssignmentOperators: true 101 | SpaceBeforeCpp11BracedList: false 102 | SpaceBeforeCtorInitializerColon: true 103 | SpaceBeforeInheritanceColon: true 104 | SpaceBeforeParens: ControlStatements 105 | SpaceBeforeRangeBasedForLoopColon: true 106 | SpaceInEmptyParentheses: false 107 | SpacesBeforeTrailingComments: 1 108 | SpacesInAngles: false 109 | SpacesInContainerLiterals: false 110 | SpacesInCStyleCastParentheses: false 111 | SpacesInParentheses: false 112 | SpacesInSquareBrackets: false 113 | Standard: Latest 114 | StatementMacros: 115 | - Q_UNUSED 116 | - QT_REQUIRE_VERSION 117 | TabWidth: 4 118 | UseTab: Never 119 | ... 120 | -------------------------------------------------------------------------------- /src/client/kimaiClient_p.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "kimaiClient.h" 11 | #include "misc/customFmt.h" 12 | #include "parser.h" 13 | 14 | namespace kemai { 15 | 16 | // available requests 17 | enum class ApiMethod 18 | { 19 | Version, 20 | Customers, 21 | CustomerAdd, 22 | Projects, 23 | ProjectAdd, 24 | Activities, 25 | ActivityAdd, 26 | ActiveTimeSheets, 27 | RecentTimeSheets, 28 | TimeSheets, 29 | Users, 30 | MeUsers, 31 | Tags, 32 | Plugins, 33 | Tasks, 34 | TaskLog, 35 | TaskStart, 36 | TaskStop, 37 | TaskClose, 38 | TimeSheetConfig 39 | }; 40 | QString apiMethodToString(ApiMethod method); 41 | 42 | class KimaiClient::KimaiClientPrivate : public QObject 43 | { 44 | Q_OBJECT 45 | 46 | public: 47 | explicit KimaiClientPrivate(KimaiClient* c); 48 | 49 | QNetworkRequest prepareRequest(ApiMethod method, const std::map& parameters = {}, const QByteArray& data = {}, 50 | const QString& subPath = "") const; 51 | 52 | QNetworkReply* sendGetRequest(const QNetworkRequest& networkRequest) const; 53 | QNetworkReply* sendPostRequest(const QNetworkRequest& networkRequest, const QByteArray& data) const; 54 | QNetworkReply* sendPatchRequest(const QNetworkRequest& networkRequest, const QByteArray& data) const; 55 | 56 | template KimaiApiResult* processApiNetworkReplySingleObject(ApiMethod method, QNetworkReply* networkReply) 57 | { 58 | auto result = new KimaiApiResult; 59 | 60 | QObject::connect(networkReply, &QNetworkReply::finished, this, [networkReply, result, method]() { 61 | if (networkReply->error() == QNetworkReply::NoError) 62 | { 63 | try 64 | { 65 | spdlog::debug("[RECV] {}", apiMethodToString(method)); 66 | 67 | KimaiApiTypesParser parser(networkReply->readAll()); 68 | result->setResult(parser.getValueOf()); 69 | } 70 | catch (std::runtime_error& ex) 71 | { 72 | result->setError(ex.what()); 73 | } 74 | } 75 | else 76 | { 77 | auto error = tr("Error on request [%1]: %2\n%3").arg(apiMethodToString(method), networkReply->errorString(), networkReply->readAll()); 78 | result->setError(error); 79 | } 80 | networkReply->deleteLater(); 81 | }); 82 | 83 | return result; 84 | } 85 | 86 | template KimaiApiResult>* processApiNetworkReplyArray(ApiMethod method, QNetworkReply* networkReply) 87 | { 88 | auto result = new KimaiApiResult>; 89 | 90 | QObject::connect(networkReply, &QNetworkReply::finished, this, [networkReply, result, method]() { 91 | if (networkReply->error() == QNetworkReply::NoError) 92 | { 93 | try 94 | { 95 | spdlog::debug("[RECV] {}", apiMethodToString(method)); 96 | 97 | KimaiApiTypesParser parser(networkReply->readAll()); 98 | result->setResult(parser.getArrayOf()); 99 | } 100 | catch (std::runtime_error& ex) 101 | { 102 | result->setError(ex.what()); 103 | } 104 | } 105 | else 106 | { 107 | auto error = tr("Error on request [%1]: %2\n%3").arg(apiMethodToString(method), networkReply->errorString(), networkReply->readAll()); 108 | result->setError(error); 109 | } 110 | networkReply->deleteLater(); 111 | }); 112 | 113 | return result; 114 | } 115 | 116 | void onNamSslErrors(QNetworkReply* reply, const QList& errors); 117 | 118 | QString username, host, token, apiToken; 119 | QScopedPointer networkAccessManager; 120 | 121 | private: 122 | KimaiClient* const mQ; 123 | }; 124 | 125 | } // namespace kemai 126 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | project(KemaiProject VERSION 0.12.0 LANGUAGES CXX) 3 | 4 | # Point CMake to the custom modules 5 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/cmake) 6 | list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) 7 | 8 | # Common configuration 9 | set(CMAKE_CXX_STANDARD 20) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | # Options 13 | option(KEMAI_ENABLE_UPDATE_CHECK "Allow Kemai to check for update from Github releases" ON) 14 | option(KEMAI_BUILD_LOCAL_DEPENDENCIES "Download and build dependencies (except Qt6)" ON) 15 | 16 | # Install dependencies sources to build if required 17 | if (KEMAI_BUILD_LOCAL_DEPENDENCIES) 18 | set(SPDLOG_FMT_EXTERNAL ON) 19 | 20 | include(FetchContent) 21 | FetchContent_Declare(fmt 22 | GIT_REPOSITORY https://github.com/fmtlib/fmt 23 | GIT_TAG 10.2.1 24 | OVERRIDE_FIND_PACKAGE) 25 | FetchContent_Declare(magic_enum 26 | GIT_REPOSITORY https://github.com/Neargye/magic_enum.git 27 | GIT_TAG v0.9.5 28 | OVERRIDE_FIND_PACKAGE) 29 | FetchContent_Declare(spdlog 30 | GIT_REPOSITORY https://github.com/gabime/spdlog.git 31 | GIT_TAG v1.14.0 32 | OVERRIDE_FIND_PACKAGE) 33 | FetchContent_Declare(range-v3 34 | GIT_REPOSITORY https://github.com/ericniebler/range-v3.git 35 | GIT_TAG 0.12.0 36 | OVERRIDE_FIND_PACKAGE) 37 | endif (KEMAI_BUILD_LOCAL_DEPENDENCIES) 38 | 39 | # Find external dependencies 40 | find_package(fmt REQUIRED) 41 | find_package(magic_enum REQUIRED) 42 | find_package(spdlog REQUIRED) 43 | find_package(range-v3 REQUIRED) 44 | 45 | # Setup Qt 46 | set(CMAKE_AUTOMOC ON) 47 | set(CMAKE_AUTOUIC ON) 48 | set(CMAKE_AUTORCC ON) 49 | find_package(Qt6 QUIET COMPONENTS Widgets Network LinguistTools) 50 | if (NOT Qt6_FOUND) 51 | message(NOTICE "Qt6 not found. Fallback to Qt5") 52 | find_package(Qt5 COMPONENTS Widgets Network LinguistTools REQUIRED) 53 | add_compile_definitions(UNICODE _UNICODE) 54 | message(STATUS "Using Qt ${Qt5_VERSION}") 55 | else () 56 | message(STATUS "Using Qt ${Qt6_VERSION}") 57 | endif () 58 | 59 | # Write version to file to ease packaging 60 | file(WRITE ${CMAKE_BINARY_DIR}/version.txt ${PROJECT_VERSION}) 61 | 62 | # Common package configuration 63 | set(KEMAI_GUID "88815E44-85A0-469C-9740-B4887D456BAA") 64 | set(KEMAI_PROJECT_NAME "Kemai") 65 | set(CPACK_PACKAGE_NAME ${KEMAI_PROJECT_NAME}) 66 | set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) 67 | set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) 68 | set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) 69 | 70 | # OS Dependant options / configurations 71 | if (WIN32) 72 | # Hide console 73 | if (${CMAKE_BUILD_TYPE} STREQUAL "Release") 74 | set(OS_BUNDLE WIN32) 75 | endif () 76 | 77 | # Add Qt libs and installer for windows platform 78 | include(WinDeployQt) 79 | 80 | # Add OpenSSL binaries for installer 81 | include(WinDeployOpenSSL) 82 | 83 | # Package 84 | set(DEPLOY_DIR .) 85 | set(CPACK_GENERATOR WIX) 86 | set(CPACK_WIX_UPGRADE_GUID ${KEMAI_GUID}) 87 | set(CPACK_PACKAGE_INSTALL_DIRECTORY ${KEMAI_PROJECT_NAME}) 88 | set(CPACK_PACKAGE_EXECUTABLES Kemai;${KEMAI_PROJECT_NAME}) 89 | set(CPACK_CREATE_DESKTOP_LINKS Kemai;${KEMAI_PROJECT_NAME}) 90 | set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_SOURCE_DIR}/LICENSE.txt) 91 | elseif (APPLE) 92 | set(OS_BUNDLE MACOSX_BUNDLE) 93 | # set(CMAKE_OSX_DEPLOYMENT_TARGET 10.12) 94 | set(DEPLOY_DIR .) 95 | set(CPACK_GENERATOR DragNDrop) 96 | install(FILES LICENSE.txt DESTINATION ${DEPLOY_DIR}) 97 | else () 98 | include(GNUInstallDirs) 99 | set(OS_BUNDLE) 100 | set(DEPLOY_DIR bin) 101 | # install(FILES share/kemai.desktop DESTINATION ${) 102 | install(FILES share/kemai.desktop 103 | DESTINATION ${CMAKE_INSTALL_DATADIR}/applications 104 | ) 105 | install(FILES src/resources/icons/kimai.png 106 | DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/256x256/apps/ 107 | ) 108 | install(FILES LICENSE.txt DESTINATION ${CMAKE_INSTALL_DOCDIR}/) 109 | endif () 110 | 111 | # Project code 112 | add_subdirectory(src) 113 | 114 | # Install targets and files 115 | install(TARGETS Kemai DESTINATION ${DEPLOY_DIR}) 116 | 117 | if (WIN32) 118 | windeployqt(Kemai ${DEPLOY_DIR}) 119 | windeployopenssl(${DEPLOY_DIR}) 120 | endif () 121 | 122 | include(CPack) 123 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, workflow_dispatch] 3 | 4 | jobs: 5 | LinuxJob: 6 | name: Ubuntu Qt-${{ matrix.qt_version }} 7 | runs-on: ubuntu-latest 8 | env: 9 | CC: gcc-12 10 | CXX: g++-12 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | qt_version: [6.7.2, 5.15.2] 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v3 18 | 19 | - name: Install CMake and Ninja 20 | uses: lukka/get-cmake@latest 21 | 22 | - name: Install Qt ${{ matrix.qt_version }} 23 | uses: jurplel/install-qt-action@v3 24 | with: 25 | version: ${{ matrix.qt_version }} 26 | tools: 'tools_opensslv3_src' 27 | cache: true 28 | aqtversion: '==3.1.*' 29 | 30 | - name: Install dev dependencies 31 | run: sudo apt install libxss-dev libxkbcommon-x11-dev libxcb-cursor0 32 | 33 | - if: startsWith(matrix.qt_version, '6') 34 | name: Build OpenSSLv3 35 | run: | 36 | cd $IQTA_TOOLS/OpenSSLv3/src 37 | ./Configure && make -j3 38 | 39 | - name: Configure 40 | run: | 41 | cmake --preset release 42 | 43 | - name: Build 44 | run: | 45 | cmake --build --preset release 46 | 47 | - if: startsWith(matrix.qt_version, '6') 48 | name: Create Linux package 49 | run: | 50 | sudo add-apt-repository universe 51 | sudo apt install libfuse2 52 | chmod +x bundle/linux/create_appimage.sh 53 | ./bundle/linux/create_appimage.sh -q $Qt6_DIR -b cmake-build-release -o $IQTA_TOOLS/OpenSSLv3/src 54 | 55 | # Create release if on built on a tag 56 | - if: startsWith(matrix.qt_version, '6') && startsWith(github.ref, 'refs/tags/') 57 | name: Create/Update release 58 | uses: ncipollo/release-action@v1 59 | with: 60 | allowUpdates: true 61 | draft: true 62 | artifacts: 'Kemai-*.AppImage' 63 | 64 | WindowsJob: 65 | name: Windows Qt-6.7.2 66 | runs-on: windows-latest 67 | steps: 68 | - name: Checkout repo 69 | uses: actions/checkout@v3 70 | 71 | - name: Install CMake and Ninja 72 | uses: lukka/get-cmake@latest 73 | 74 | - name: Install Qt 75 | uses: jurplel/install-qt-action@v3 76 | with: 77 | version: '6.7.2' 78 | tools: 'tools_opensslv3_x64' 79 | cache: true 80 | aqtversion: '==3.1.*' 81 | 82 | - name: Setup MSVC 83 | uses: ilammy/msvc-dev-cmd@v1 84 | 85 | - name: Configure 86 | run: | 87 | cmake --preset release-win 88 | 89 | - name: Build 90 | run: | 91 | cmake --build --preset release-win 92 | 93 | - name: Create Windows package 94 | run: | 95 | cmake --build --preset release-win --target package 96 | 97 | # Create release if on built on a tag 98 | - if: startsWith(github.ref, 'refs/tags/') 99 | name: Create/Update release 100 | uses: ncipollo/release-action@v1 101 | with: 102 | allowUpdates: true 103 | draft: true 104 | artifacts: 'cmake-build-release-win/Kemai-*.msi' 105 | 106 | MacOSJob: 107 | name: MacOS Qt-6.7.2 108 | runs-on: macos-latest 109 | strategy: 110 | matrix: 111 | preset: ["release-macos", "release-macos-noupdate"] 112 | steps: 113 | - name: Checkout repo 114 | uses: actions/checkout@v3 115 | 116 | - name: Install CMake and Ninja 117 | uses: lukka/get-cmake@latest 118 | 119 | - name: Install Qt 120 | uses: jurplel/install-qt-action@v3 121 | with: 122 | version: '6.7.2' 123 | cache: true 124 | aqtversion: '==3.1.*' 125 | 126 | - name: Configure 127 | run: | 128 | cmake --preset ${{ matrix.preset }} 129 | 130 | - name: Build 131 | run: | 132 | cmake --build --preset ${{ matrix.preset }} 133 | 134 | - name: Create MacOS package 135 | run: | 136 | chmod +x bundle/macos/create_dmg.sh 137 | ./bundle/macos/create_dmg.sh --qt_path $Qt6_DIR --build_path cmake-build-${{ matrix.preset }} 138 | 139 | # Create release if on built on a tag 140 | - if: startsWith(github.ref, 'refs/tags/') 141 | name: Create/Update release 142 | uses: ncipollo/release-action@v1 143 | with: 144 | allowUpdates: true 145 | draft: true 146 | artifacts: 'dist/Kemai-*.dmg' 147 | -------------------------------------------------------------------------------- /src/context/kemaiSession.cpp: -------------------------------------------------------------------------------- 1 | #include "kemaiSession.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "client/kimaiFeatures.h" 8 | #include "misc/customFmt.h" 9 | 10 | using namespace kemai; 11 | 12 | /* 13 | * Class impl 14 | */ 15 | KemaiSession::KemaiSession(const std::shared_ptr& kimaiClient) : mKimaiClient(kimaiClient), mKimaiMonitor(kimaiClient) 16 | { 17 | connect(&mKimaiMonitor, &KimaiEventsMonitor::currentTimeSheetChanged, this, &KemaiSession::currentTimeSheetChanged); 18 | } 19 | 20 | KemaiSession::~KemaiSession() = default; 21 | 22 | std::shared_ptr KemaiSession::client() const 23 | { 24 | return mKimaiClient; 25 | } 26 | 27 | const KimaiCache& KemaiSession::cache() const 28 | { 29 | return mKimaiCache; 30 | } 31 | 32 | void KemaiSession::refreshSessionInfos() 33 | { 34 | requestMe(); 35 | requestVersion(); 36 | requestTimeSheetConfig(); 37 | } 38 | 39 | void KemaiSession::refreshCurrentTimeSheet() 40 | { 41 | mKimaiMonitor.refreshCurrentTimeSheet(); 42 | } 43 | 44 | void KemaiSession::refreshCache(KimaiCache::Category category) 45 | { 46 | if (mKimaiClient) 47 | { 48 | mKimaiCache.synchronize(mKimaiClient, {category}); 49 | } 50 | } 51 | 52 | void KemaiSession::refreshCache(const std::set& categories) 53 | { 54 | if (mKimaiClient) 55 | { 56 | mKimaiCache.synchronize(mKimaiClient, categories); 57 | } 58 | } 59 | 60 | bool KemaiSession::hasPlugin(ApiPlugin apiPlugin) const 61 | { 62 | return std::any_of(mPlugins.begin(), mPlugins.end(), [apiPlugin](const Plugin& plugin) { return plugin.apiPlugin == apiPlugin; }); 63 | } 64 | 65 | QVersionNumber KemaiSession::kimaiVersion() const 66 | { 67 | return mKimaiVersion; 68 | } 69 | 70 | User KemaiSession::me() const 71 | { 72 | return mMe; 73 | } 74 | 75 | TimeSheetConfig KemaiSession::timeSheetConfig() const 76 | { 77 | return mTimeSheetConfig; 78 | } 79 | 80 | std::optional KemaiSession::currentTimeSheet() const 81 | { 82 | return mKimaiMonitor.currentTimeSheet(); 83 | } 84 | 85 | bool KemaiSession::hasCurrentTimeSheet() const 86 | { 87 | return mKimaiMonitor.hasCurrentTimeSheet(); 88 | } 89 | 90 | QDateTime KemaiSession::computeTZDateTime(const QDateTime& dateTime) const 91 | { 92 | // Be sure to use expected timezone 93 | auto timeZone = QTimeZone(mMe.timezone.toLocal8Bit()); 94 | if (timeZone.isValid()) 95 | { 96 | return dateTime.toTimeZone(timeZone); 97 | } 98 | return dateTime; 99 | } 100 | 101 | void KemaiSession::onClientError(KimaiApiBaseResult* apiBaseResult) 102 | { 103 | spdlog::error("Client error: {}", apiBaseResult->errorMessage()); 104 | apiBaseResult->deleteLater(); 105 | } 106 | 107 | void KemaiSession::requestMe() 108 | { 109 | auto meResult = mKimaiClient->requestMeUserInfo(); 110 | 111 | connect(meResult, &KimaiApiBaseResult::ready, this, [this, meResult]() { 112 | mMe = meResult->getResult(); 113 | emit meChanged(); 114 | meResult->deleteLater(); 115 | }); 116 | 117 | connect(meResult, &KimaiApiBaseResult::error, this, [this, meResult]() { onClientError(meResult); }); 118 | } 119 | 120 | void KemaiSession::requestVersion() 121 | { 122 | auto versionResult = mKimaiClient->requestKimaiVersion(); 123 | 124 | connect(versionResult, &KimaiApiBaseResult::ready, this, [this, versionResult]() { 125 | mKimaiVersion = versionResult->getResult().kimai; 126 | emit versionChanged(); 127 | if (KimaiFeatures::canRequestPlugins(mKimaiVersion)) 128 | { 129 | requestPlugins(); 130 | } 131 | versionResult->deleteLater(); 132 | }); 133 | 134 | connect(versionResult, &KimaiApiBaseResult::error, this, [this, versionResult]() { onClientError(versionResult); }); 135 | } 136 | 137 | void KemaiSession::requestTimeSheetConfig() 138 | { 139 | auto timeSheetConfigResult = mKimaiClient->requestTimeSheetConfig(); 140 | 141 | connect(timeSheetConfigResult, &KimaiApiBaseResult::ready, this, [this, timeSheetConfigResult]() { 142 | mTimeSheetConfig = timeSheetConfigResult->getResult(); 143 | emit timeSheetConfigChanged(); 144 | timeSheetConfigResult->deleteLater(); 145 | }); 146 | 147 | connect(timeSheetConfigResult, &KimaiApiBaseResult::error, this, [this, timeSheetConfigResult]() { onClientError(timeSheetConfigResult); }); 148 | } 149 | 150 | void KemaiSession::requestPlugins() 151 | { 152 | auto pluginsResult = mKimaiClient->requestPlugins(); 153 | 154 | connect(pluginsResult, &KimaiApiBaseResult::ready, this, [this, pluginsResult]() { 155 | mPlugins = pluginsResult->getResult(); 156 | emit pluginsChanged(); 157 | pluginsResult->deleteLater(); 158 | }); 159 | 160 | connect(pluginsResult, &KimaiApiBaseResult::error, this, [this, pluginsResult]() { onClientError(pluginsResult); }); 161 | } 162 | -------------------------------------------------------------------------------- /src/gui/projectDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ProjectDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 300 10 | 101 11 | 12 | 13 | 14 | Project 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Name 26 | 27 | 28 | 29 | 30 | 31 | 32 | Budget 33 | 34 | 35 | 36 | 37 | 38 | 39 | Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 40 | 41 | 42 | QAbstractSpinBox::ButtonSymbols::NoButtons 43 | 44 | 45 | 46 | 47 | 48 | 999999999.000000000000000 49 | 50 | 51 | 52 | 53 | 54 | 55 | Qt::Orientation::Horizontal 56 | 57 | 58 | 59 | 40 60 | 20 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Time Budget 69 | 70 | 71 | 72 | 73 | 74 | 75 | 0 76 | 77 | 78 | Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 79 | 80 | 81 | 82 | 83 | 84 | 85 | 150 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | Qt::Orientation::Vertical 95 | 96 | 97 | 98 | 20 99 | 2 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | Qt::Orientation::Horizontal 108 | 109 | 110 | QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | kemai::DurationEdit 119 | QLineEdit 120 |
gui/durationEdit.h
121 |
122 |
123 | 124 | leName 125 | sbBudget 126 | leTimeBudget 127 | 128 | 129 | 130 | 131 | buttonBox 132 | accepted() 133 | ProjectDialog 134 | accept() 135 | 136 | 137 | 248 138 | 254 139 | 140 | 141 | 157 142 | 274 143 | 144 | 145 | 146 | 147 | buttonBox 148 | rejected() 149 | ProjectDialog 150 | reject() 151 | 152 | 153 | 316 154 | 260 155 | 156 | 157 | 286 158 | 274 159 | 160 | 161 | 162 | 163 |
164 | -------------------------------------------------------------------------------- /src/gui/activityDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ActivityDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 300 10 | 101 11 | 12 | 13 | 14 | Activity 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 0 26 | 27 | 28 | Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 29 | 30 | 31 | 32 | 33 | 34 | 35 | Budget 36 | 37 | 38 | 39 | 40 | 41 | 42 | Qt::Orientation::Horizontal 43 | 44 | 45 | 46 | 40 47 | 20 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 150 56 | 57 | 58 | 59 | 60 | 61 | 62 | Name 63 | 64 | 65 | 66 | 67 | 68 | 69 | Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 70 | 71 | 72 | QAbstractSpinBox::ButtonSymbols::NoButtons 73 | 74 | 75 | 76 | 77 | 78 | 999999999.000000000000000 79 | 80 | 81 | 82 | 83 | 84 | 85 | Time Budget 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | Qt::Orientation::Vertical 95 | 96 | 97 | 98 | 20 99 | 1 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | Qt::Orientation::Horizontal 108 | 109 | 110 | QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | kemai::DurationEdit 119 | QLineEdit 120 |
gui/durationEdit.h
121 |
122 |
123 | 124 | leName 125 | sbBudget 126 | leTimeBudget 127 | 128 | 129 | 130 | 131 | buttonBox 132 | accepted() 133 | ActivityDialog 134 | accept() 135 | 136 | 137 | 248 138 | 254 139 | 140 | 141 | 157 142 | 274 143 | 144 | 145 | 146 | 147 | buttonBox 148 | rejected() 149 | ActivityDialog 150 | reject() 151 | 152 | 153 | 316 154 | 260 155 | 156 | 157 | 286 158 | 274 159 | 160 | 161 | 162 | 163 |
164 | -------------------------------------------------------------------------------- /src/gui/aboutDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AboutDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 375 10 | 258 11 | 12 | 13 | 14 | About Kemai 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 128 29 | 128 30 | 31 | 32 | 33 | 34 | 128 35 | 128 36 | 37 | 38 | 39 | 40 | 41 | 42 | :/icons/kemai 43 | 44 | 45 | true 46 | 47 | 48 | 49 | 50 | 51 | 52 | Qt::Orientation::Vertical 53 | 54 | 55 | 56 | 20 57 | 40 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 0 72 | 73 | 74 | 75 | 76 | 18 77 | true 78 | 79 | 80 | 81 | Kemai 82 | 83 | 84 | 85 | 86 | 87 | 88 | Desktop client for Kimai 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | Qt::Orientation::Horizontal 103 | 104 | 105 | 106 | 107 | 108 | 109 | Proudly using: 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | Qt::Orientation::Horizontal 127 | 128 | 129 | QDialogButtonBox::StandardButton::Close 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | buttonBox 141 | accepted() 142 | AboutDialog 143 | accept() 144 | 145 | 146 | 248 147 | 254 148 | 149 | 150 | 157 151 | 274 152 | 153 | 154 | 155 | 156 | buttonBox 157 | rejected() 158 | AboutDialog 159 | reject() 160 | 161 | 162 | 316 163 | 260 164 | 165 | 166 | 286 167 | 274 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(Kemai) 2 | 3 | add_executable(${PROJECT_NAME} ${OS_BUNDLE}) 4 | 5 | configure_file(kemaiConfig.h.in ${PROJECT_BINARY_DIR}/kemaiConfig.h) 6 | 7 | # Sources 8 | set(SRCS 9 | client/kimaiAPI.cpp 10 | client/kimaiCache.cpp 11 | client/kimaiClient.cpp 12 | client/kimaiFeatures.cpp 13 | client/kimaiReply.cpp 14 | client/parser.cpp 15 | context/kemaiSession.cpp 16 | gui/aboutDialog.cpp 17 | gui/activityDialog.cpp 18 | gui/activityWidget.cpp 19 | gui/autoCompleteComboBox.cpp 20 | gui/customerDialog.cpp 21 | gui/durationEdit.cpp 22 | gui/loggerWidget.cpp 23 | gui/mainWindow.cpp 24 | gui/projectDialog.cpp 25 | gui/settingsDialog.cpp 26 | gui/taskWidget.cpp 27 | gui/timeSheetListWidgetItem.cpp 28 | main.cpp 29 | misc/dataReader.cpp 30 | misc/helpers.cpp 31 | models/kimaiDataListModel.cpp 32 | models/kimaiDataSortFilterProxyModel.cpp 33 | models/loggerTreeModel.cpp 34 | models/taskFilterProxyModel.cpp 35 | models/taskListModel.cpp 36 | monitor/desktopEventsMonitor.cpp 37 | monitor/kimaiEventsMonitor.cpp 38 | settings/settings.cpp 39 | updater/kemaiUpdater.cpp) 40 | 41 | set(HDRS 42 | client/kimaiAPI.h 43 | client/kimaiCache.h 44 | client/kimaiClient.h 45 | client/kimaiClient_p.h 46 | client/kimaiFeatures.h 47 | client/kimaiReply.h 48 | client/parser.h 49 | context/kemaiSession.h 50 | gui/aboutDialog.h 51 | gui/activityDialog.h 52 | gui/activityWidget.h 53 | gui/autoCompleteComboBox.h 54 | gui/customerDialog.h 55 | gui/durationEdit.h 56 | gui/loggerWidget.h 57 | gui/mainWindow.h 58 | gui/projectDialog.h 59 | gui/settingsDialog.h 60 | gui/taskWidget.h 61 | gui/timeSheetListWidgetItem.h 62 | misc/customFmt.h 63 | misc/dataReader.h 64 | misc/helpers.h 65 | models/kimaiDataListModel.h 66 | models/kimaiDataSortFilterProxyModel.h 67 | models/loggerTreeModel.h 68 | models/taskFilterProxyModel.h 69 | models/taskListModel.h 70 | monitor/desktopEventsMonitor.h 71 | monitor/kimaiEventsMonitor.h 72 | settings/settings.h 73 | updater/kemaiUpdater.h 74 | updater/kemaiUpdater_p.h) 75 | 76 | # Forms 77 | set(UIS 78 | gui/aboutDialog.ui 79 | gui/activityDialog.ui 80 | gui/activityWidget.ui 81 | gui/customerDialog.ui 82 | gui/loggerWidget.ui 83 | gui/mainWindow.ui 84 | gui/projectDialog.ui 85 | gui/settingsDialog.ui 86 | gui/taskWidget.ui 87 | gui/timeSheetListWidgetItem.ui) 88 | 89 | 90 | # Localization 91 | include(LocalizationTools) 92 | set(KEMAI_TS_FILES 93 | resources/l10n/kemai_cs.ts 94 | resources/l10n/kemai_de.ts 95 | resources/l10n/kemai_el.ts 96 | resources/l10n/kemai_fr.ts 97 | resources/l10n/kemai_hr.ts 98 | resources/l10n/kemai_hu.ts 99 | resources/l10n/kemai_it.ts 100 | resources/l10n/kemai_nb_NO.ts 101 | resources/l10n/kemai_nl.ts 102 | resources/l10n/kemai_tr.ts) 103 | qt_add_translation(KEMAI_QM_FILES ${KEMAI_TS_FILES}) 104 | add_qm_files_to_qrc(KEMAI_L10N_QRC ${KEMAI_QM_FILES}) 105 | 106 | # Update .ts files on demand 107 | get_target_property(_lupdate_executable Qt::lupdate IMPORTED_LOCATION) 108 | add_custom_target(kemai-update-ts COMMAND 109 | ${_lupdate_executable} -no-obsolete ${CMAKE_CURRENT_SOURCE_DIR} -ts ${KEMAI_TS_FILES} 110 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) 111 | 112 | # Resources 113 | set(RESX 114 | resources/kemai.qrc 115 | ${KEMAI_L10N_QRC}) 116 | 117 | qt_add_resources(${PROJECT_NAME} ${RESX}) 118 | 119 | # OS Specifics 120 | if (WIN32) 121 | list(APPEND SRCS 122 | ${CMAKE_SOURCE_DIR}/bundle/windows/kemai.rc 123 | monitor/windowsDesktopEventsMonitor.cpp) 124 | list(APPEND HDRS 125 | monitor/windowsDesktopEventsMonitor.h) 126 | target_link_libraries(${PROJECT_NAME} Wtsapi32) 127 | elseif (APPLE) 128 | list(APPEND SRCS 129 | monitor/macDesktopEventsMonitor.mm) 130 | list(APPEND HDRS 131 | monitor/macDesktopEventsMonitor.h) 132 | set(KEMAI_ICNS "${CMAKE_SOURCE_DIR}/bundle/macos/kemai.icns") 133 | set_source_files_properties(${KEMAI_ICNS} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") 134 | set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/bundle/macos/Info.plist) 135 | target_link_libraries(${PROJECT_NAME} "-framework CoreGraphics") 136 | elseif (UNIX) 137 | list(APPEND SRCS 138 | monitor/linuxDesktopEventsMonitor.cpp) 139 | list(APPEND HDRS 140 | monitor/linuxDesktopEventsMonitor.h) 141 | target_link_libraries(${PROJECT_NAME} Xss X11) 142 | endif () 143 | 144 | # Target configuration 145 | target_sources(${PROJECT_NAME} PRIVATE ${SRCS} ${HDRS} ${UIS} ${RESX} ${KEMAI_ICNS}) 146 | target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) 147 | target_link_libraries(${PROJECT_NAME} Qt::Widgets Qt::Network spdlog::spdlog magic_enum::magic_enum range-v3::range-v3) 148 | -------------------------------------------------------------------------------- /src/client/kimaiCache.cpp: -------------------------------------------------------------------------------- 1 | #include "kimaiCache.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace kemai; 8 | 9 | void KimaiCache::synchronize(const std::shared_ptr& client, const std::set& categories) 10 | { 11 | if (!mSyncMutex.try_lock()) 12 | { 13 | spdlog::error("Sync already in progress"); 14 | return; 15 | } 16 | 17 | mStatus = KimaiCache::Status::SyncPending; 18 | 19 | // Fill what to sync 20 | if (categories.empty()) 21 | { 22 | auto categoryArray = magic_enum::enum_values(); 23 | mPendingSync = {categoryArray.begin(), categoryArray.end()}; 24 | } 25 | else 26 | { 27 | mPendingSync = categories; 28 | } 29 | 30 | // Notify before running sync requests 31 | emit synchronizeStarted(); 32 | 33 | for (const auto& category : mPendingSync) 34 | { 35 | switch (category) 36 | { 37 | case Category::Customers: { 38 | mCustomers.clear(); 39 | auto customersResult = client->requestCustomers(); 40 | connect(customersResult, &KimaiApiBaseResult::ready, this, [this, customersResult] { processCustomersResult(customersResult); }); 41 | connect(customersResult, &KimaiApiBaseResult::error, this, [this, customersResult] { processCustomersResult(customersResult); }); 42 | } 43 | break; 44 | 45 | case Category::Projects: { 46 | mProjects.clear(); 47 | auto projectsResult = client->requestProjects(); 48 | connect(projectsResult, &KimaiApiBaseResult::ready, this, [this, projectsResult] { processProjectsResult(projectsResult); }); 49 | connect(projectsResult, &KimaiApiBaseResult::error, this, [this, projectsResult] { processProjectsResult(projectsResult); }); 50 | } 51 | break; 52 | 53 | case Category::Activities: { 54 | mActivities.clear(); 55 | auto activitiesResult = client->requestActivities(); 56 | connect(activitiesResult, &KimaiApiBaseResult::ready, this, [this, activitiesResult] { processActivitiesResult(activitiesResult); }); 57 | connect(activitiesResult, &KimaiApiBaseResult::error, this, [this, activitiesResult] { processActivitiesResult(activitiesResult); }); 58 | } 59 | break; 60 | 61 | case Category::RecentTimeSheets: { 62 | mRecentTimeSheets.clear(); 63 | auto timeSheetsResult = client->requestRecentTimeSheets(); 64 | connect(timeSheetsResult, &KimaiApiBaseResult::ready, this, [this, timeSheetsResult] { processRecentTimeSheetsResult(timeSheetsResult); }); 65 | connect(timeSheetsResult, &KimaiApiBaseResult::error, this, [this, timeSheetsResult] { processRecentTimeSheetsResult(timeSheetsResult); }); 66 | } 67 | break; 68 | } 69 | } 70 | } 71 | 72 | KimaiCache::Status KimaiCache::status() const 73 | { 74 | return mStatus; 75 | } 76 | 77 | Customers KimaiCache::customers() const 78 | { 79 | return mCustomers; 80 | } 81 | 82 | Projects KimaiCache::projects(std::optional customerId) const 83 | { 84 | if (customerId.has_value()) 85 | { 86 | auto its = mProjects | ranges::views::filter([id = customerId.value()](auto const& project) { return project.customer.id == id; }); 87 | return {its.begin(), its.end()}; 88 | } 89 | return mProjects; 90 | } 91 | 92 | Activities KimaiCache::activities(std::optional projectId) const 93 | { 94 | if (projectId.has_value()) 95 | { 96 | auto its = mActivities | ranges::views::filter([id = projectId.value()](auto const& activity) { 97 | if (!activity.project.has_value()) 98 | { 99 | return true; 100 | } 101 | return activity.project->id == id; 102 | }); 103 | return {its.begin(), its.end()}; 104 | } 105 | return mActivities; 106 | } 107 | 108 | TimeSheets KimaiCache::recentTimeSheets() const 109 | { 110 | return mRecentTimeSheets; 111 | } 112 | 113 | void KimaiCache::updateSyncProgress(Category finishedCategory) 114 | { 115 | const std::lock_guard lockGuard(mProgressMutex); 116 | 117 | mPendingSync.erase(finishedCategory); 118 | 119 | if (mPendingSync.empty()) 120 | { 121 | mStatus = KimaiCache::Status::Ready; 122 | mSyncMutex.unlock(); 123 | emit synchronizeFinished(); 124 | } 125 | } 126 | 127 | void KimaiCache::processCustomersResult(CustomersResult customersResult) 128 | { 129 | if (!customersResult->hasError()) 130 | { 131 | mCustomers = customersResult->takeResult(); 132 | } 133 | customersResult->deleteLater(); 134 | updateSyncProgress(Category::Customers); 135 | } 136 | 137 | void KimaiCache::processProjectsResult(ProjectsResult projectsResult) 138 | { 139 | if (!projectsResult->hasError()) 140 | { 141 | mProjects = projectsResult->takeResult(); 142 | } 143 | projectsResult->deleteLater(); 144 | updateSyncProgress(Category::Projects); 145 | } 146 | 147 | void KimaiCache::processActivitiesResult(ActivitiesResult activitiesResult) 148 | { 149 | if (!activitiesResult->hasError()) 150 | { 151 | mActivities = activitiesResult->takeResult(); 152 | } 153 | activitiesResult->deleteLater(); 154 | updateSyncProgress(Category::Activities); 155 | } 156 | 157 | void KimaiCache::processRecentTimeSheetsResult(TimeSheetsResult timeSheetsResult) 158 | { 159 | if (!timeSheetsResult->hasError()) 160 | { 161 | mRecentTimeSheets = timeSheetsResult->takeResult(); 162 | } 163 | timeSheetsResult->deleteLater(); 164 | updateSyncProgress(Category::RecentTimeSheets); 165 | } 166 | -------------------------------------------------------------------------------- /src/resources/data/iso3166-alpha2.json: -------------------------------------------------------------------------------- 1 | { 2 | "AF": "Afghanistan", 3 | "AX": "Aland Islands", 4 | "AL": "Albania", 5 | "DZ": "Algeria", 6 | "AS": "American Samoa", 7 | "AD": "Andorra", 8 | "AO": "Angola", 9 | "AI": "Anguilla", 10 | "AQ": "Antarctica", 11 | "AG": "Antigua And Barbuda", 12 | "AR": "Argentina", 13 | "AM": "Armenia", 14 | "AW": "Aruba", 15 | "AU": "Australia", 16 | "AT": "Austria", 17 | "AZ": "Azerbaijan", 18 | "BS": "Bahamas", 19 | "BH": "Bahrain", 20 | "BD": "Bangladesh", 21 | "BB": "Barbados", 22 | "BY": "Belarus", 23 | "BE": "Belgium", 24 | "BZ": "Belize", 25 | "BJ": "Benin", 26 | "BM": "Bermuda", 27 | "BT": "Bhutan", 28 | "BO": "Bolivia", 29 | "BA": "Bosnia And Herzegovina", 30 | "BW": "Botswana", 31 | "BV": "Bouvet Island", 32 | "BR": "Brazil", 33 | "IO": "British Indian Ocean Territory", 34 | "BN": "Brunei Darussalam", 35 | "BG": "Bulgaria", 36 | "BF": "Burkina Faso", 37 | "BI": "Burundi", 38 | "KH": "Cambodia", 39 | "CM": "Cameroon", 40 | "CA": "Canada", 41 | "CV": "Cape Verde", 42 | "KY": "Cayman Islands", 43 | "CF": "Central African Republic", 44 | "TD": "Chad", 45 | "CL": "Chile", 46 | "CN": "China", 47 | "CX": "Christmas Island", 48 | "CC": "Cocos (Keeling) Islands", 49 | "CO": "Colombia", 50 | "KM": "Comoros", 51 | "CG": "Congo", 52 | "CD": "Congo, Democratic Republic", 53 | "CK": "Cook Islands", 54 | "CR": "Costa Rica", 55 | "CI": "Cote D\"Ivoire", 56 | "HR": "Croatia", 57 | "CU": "Cuba", 58 | "CY": "Cyprus", 59 | "CZ": "Czech Republic", 60 | "DK": "Denmark", 61 | "DJ": "Djibouti", 62 | "DM": "Dominica", 63 | "DO": "Dominican Republic", 64 | "EC": "Ecuador", 65 | "EG": "Egypt", 66 | "SV": "El Salvador", 67 | "GQ": "Equatorial Guinea", 68 | "ER": "Eritrea", 69 | "EE": "Estonia", 70 | "ET": "Ethiopia", 71 | "FK": "Falkland Islands (Malvinas)", 72 | "FO": "Faroe Islands", 73 | "FJ": "Fiji", 74 | "FI": "Finland", 75 | "FR": "France", 76 | "GF": "French Guiana", 77 | "PF": "French Polynesia", 78 | "TF": "French Southern Territories", 79 | "GA": "Gabon", 80 | "GM": "Gambia", 81 | "GE": "Georgia", 82 | "DE": "Germany", 83 | "GH": "Ghana", 84 | "GI": "Gibraltar", 85 | "GR": "Greece", 86 | "GL": "Greenland", 87 | "GD": "Grenada", 88 | "GP": "Guadeloupe", 89 | "GU": "Guam", 90 | "GT": "Guatemala", 91 | "GG": "Guernsey", 92 | "GN": "Guinea", 93 | "GW": "Guinea-Bissau", 94 | "GY": "Guyana", 95 | "HT": "Haiti", 96 | "HM": "Heard Island & Mcdonald Islands", 97 | "VA": "Holy See (Vatican City State)", 98 | "HN": "Honduras", 99 | "HK": "Hong Kong", 100 | "HU": "Hungary", 101 | "IS": "Iceland", 102 | "IN": "India", 103 | "ID": "Indonesia", 104 | "IR": "Iran, Islamic Republic Of", 105 | "IQ": "Iraq", 106 | "IE": "Ireland", 107 | "IM": "Isle Of Man", 108 | "IL": "Israel", 109 | "IT": "Italy", 110 | "JM": "Jamaica", 111 | "JP": "Japan", 112 | "JE": "Jersey", 113 | "JO": "Jordan", 114 | "KZ": "Kazakhstan", 115 | "KE": "Kenya", 116 | "KI": "Kiribati", 117 | "KR": "Korea", 118 | "KW": "Kuwait", 119 | "KG": "Kyrgyzstan", 120 | "LA": "Lao People\"s Democratic Republic", 121 | "LV": "Latvia", 122 | "LB": "Lebanon", 123 | "LS": "Lesotho", 124 | "LR": "Liberia", 125 | "LY": "Libyan Arab Jamahiriya", 126 | "LI": "Liechtenstein", 127 | "LT": "Lithuania", 128 | "LU": "Luxembourg", 129 | "MO": "Macao", 130 | "MK": "Macedonia", 131 | "MG": "Madagascar", 132 | "MW": "Malawi", 133 | "MY": "Malaysia", 134 | "MV": "Maldives", 135 | "ML": "Mali", 136 | "MT": "Malta", 137 | "MH": "Marshall Islands", 138 | "MQ": "Martinique", 139 | "MR": "Mauritania", 140 | "MU": "Mauritius", 141 | "YT": "Mayotte", 142 | "MX": "Mexico", 143 | "FM": "Micronesia, Federated States Of", 144 | "MD": "Moldova", 145 | "MC": "Monaco", 146 | "MN": "Mongolia", 147 | "ME": "Montenegro", 148 | "MS": "Montserrat", 149 | "MA": "Morocco", 150 | "MZ": "Mozambique", 151 | "MM": "Myanmar", 152 | "NA": "Namibia", 153 | "NR": "Nauru", 154 | "NP": "Nepal", 155 | "NL": "Netherlands", 156 | "AN": "Netherlands Antilles", 157 | "NC": "New Caledonia", 158 | "NZ": "New Zealand", 159 | "NI": "Nicaragua", 160 | "NE": "Niger", 161 | "NG": "Nigeria", 162 | "NU": "Niue", 163 | "NF": "Norfolk Island", 164 | "MP": "Northern Mariana Islands", 165 | "NO": "Norway", 166 | "OM": "Oman", 167 | "PK": "Pakistan", 168 | "PW": "Palau", 169 | "PS": "Palestinian Territory, Occupied", 170 | "PA": "Panama", 171 | "PG": "Papua New Guinea", 172 | "PY": "Paraguay", 173 | "PE": "Peru", 174 | "PH": "Philippines", 175 | "PN": "Pitcairn", 176 | "PL": "Poland", 177 | "PT": "Portugal", 178 | "PR": "Puerto Rico", 179 | "QA": "Qatar", 180 | "RE": "Reunion", 181 | "RO": "Romania", 182 | "RU": "Russian Federation", 183 | "RW": "Rwanda", 184 | "BL": "Saint Barthelemy", 185 | "SH": "Saint Helena", 186 | "KN": "Saint Kitts And Nevis", 187 | "LC": "Saint Lucia", 188 | "MF": "Saint Martin", 189 | "PM": "Saint Pierre And Miquelon", 190 | "VC": "Saint Vincent And Grenadines", 191 | "WS": "Samoa", 192 | "SM": "San Marino", 193 | "ST": "Sao Tome And Principe", 194 | "SA": "Saudi Arabia", 195 | "SN": "Senegal", 196 | "RS": "Serbia", 197 | "SC": "Seychelles", 198 | "SL": "Sierra Leone", 199 | "SG": "Singapore", 200 | "SK": "Slovakia", 201 | "SI": "Slovenia", 202 | "SB": "Solomon Islands", 203 | "SO": "Somalia", 204 | "ZA": "South Africa", 205 | "GS": "South Georgia And Sandwich Isl.", 206 | "ES": "Spain", 207 | "LK": "Sri Lanka", 208 | "SD": "Sudan", 209 | "SR": "Suriname", 210 | "SJ": "Svalbard And Jan Mayen", 211 | "SZ": "Swaziland", 212 | "SE": "Sweden", 213 | "CH": "Switzerland", 214 | "SY": "Syrian Arab Republic", 215 | "TW": "Taiwan", 216 | "TJ": "Tajikistan", 217 | "TZ": "Tanzania", 218 | "TH": "Thailand", 219 | "TL": "Timor-Leste", 220 | "TG": "Togo", 221 | "TK": "Tokelau", 222 | "TO": "Tonga", 223 | "TT": "Trinidad And Tobago", 224 | "TN": "Tunisia", 225 | "TR": "Turkey", 226 | "TM": "Turkmenistan", 227 | "TC": "Turks And Caicos Islands", 228 | "TV": "Tuvalu", 229 | "UG": "Uganda", 230 | "UA": "Ukraine", 231 | "AE": "United Arab Emirates", 232 | "GB": "United Kingdom", 233 | "US": "United States", 234 | "UM": "United States Outlying Islands", 235 | "UY": "Uruguay", 236 | "UZ": "Uzbekistan", 237 | "VU": "Vanuatu", 238 | "VE": "Venezuela", 239 | "VN": "Vietnam", 240 | "VG": "Virgin Islands, British", 241 | "VI": "Virgin Islands, U.S.", 242 | "WF": "Wallis And Futuna", 243 | "EH": "Western Sahara", 244 | "YE": "Yemen", 245 | "ZM": "Zambia", 246 | "ZW": "Zimbabwe" 247 | } -------------------------------------------------------------------------------- /src/gui/customerDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | CustomerDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 300 10 | 179 11 | 12 | 13 | 14 | Customer 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Country 26 | 27 | 28 | 29 | 30 | 31 | 32 | Qt::Orientation::Horizontal 33 | 34 | 35 | 36 | 40 37 | 20 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 0 47 | 0 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Currency 56 | 57 | 58 | 59 | 60 | 61 | 62 | Name 63 | 64 | 65 | 66 | 67 | 68 | 69 | Time Budget 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 0 78 | 0 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 150 87 | 88 | 89 | 90 | 91 | 92 | 93 | Timezone 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | Budget 104 | 105 | 106 | 107 | 108 | 109 | 110 | Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 111 | 112 | 113 | QAbstractSpinBox::ButtonSymbols::NoButtons 114 | 115 | 116 | 117 | 118 | 119 | 999999999.000000000000000 120 | 121 | 122 | 123 | 124 | 125 | 126 | 0 127 | 128 | 129 | Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | Qt::Orientation::Vertical 139 | 140 | 141 | 142 | 20 143 | 38 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | Qt::Orientation::Horizontal 152 | 153 | 154 | QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | kemai::DurationEdit 163 | QLineEdit 164 |
gui/durationEdit.h
165 |
166 |
167 | 168 | leName 169 | cbCountry 170 | cbTimezone 171 | cbCurrency 172 | sbBudget 173 | leTimeBudget 174 | 175 | 176 | 177 | 178 | buttonBox 179 | accepted() 180 | CustomerDialog 181 | accept() 182 | 183 | 184 | 248 185 | 254 186 | 187 | 188 | 157 189 | 274 190 | 191 | 192 | 193 | 194 | buttonBox 195 | rejected() 196 | CustomerDialog 197 | reject() 198 | 199 | 200 | 316 201 | 260 202 | 203 | 204 | 286 205 | 274 206 | 207 | 208 | 209 | 210 |
211 | -------------------------------------------------------------------------------- /src/gui/timeSheetListWidgetItem.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TimeSheetListWidgetItem 4 | 5 | 6 | 7 | 0 8 | 0 9 | 116 10 | 84 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | 21 | 2 22 | 23 | 24 | 2 25 | 26 | 27 | 2 28 | 29 | 30 | 2 31 | 32 | 33 | 34 | 35 | 36 | 0 37 | 0 38 | 39 | 40 | 41 | QFrame::Shape::NoFrame 42 | 43 | 44 | QFrame::Shadow::Plain 45 | 46 | 47 | 1 48 | 49 | 50 | 51 | QLayout::SizeConstraint::SetNoConstraint 52 | 53 | 54 | 2 55 | 56 | 57 | 2 58 | 59 | 60 | 2 61 | 62 | 63 | 2 64 | 65 | 66 | 67 | 68 | 69 | 70 | StartedAt 71 | 72 | 73 | 74 | 75 | 76 | 77 | Qt::Orientation::Horizontal 78 | 79 | 80 | 81 | 40 82 | 20 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | duration 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 0 101 | 0 102 | 103 | 104 | 105 | Description 106 | 107 | 108 | true 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 0 119 | 0 120 | 121 | 122 | 123 | 124 | true 125 | 126 | 127 | 128 | Activity 129 | 130 | 131 | true 132 | 133 | 134 | 135 | 136 | 137 | 138 | Qt::Orientation::Horizontal 139 | 140 | 141 | 142 | 40 143 | 20 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | Use as template 152 | 153 | 154 | 155 | 156 | 157 | 158 | :/icons/refresh:/icons/refresh 159 | 160 | 161 | 162 | 24 163 | 24 164 | 165 | 166 | 167 | false 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 32 176 | 32 177 | 178 | 179 | 180 | Start again 181 | 182 | 183 | 184 | :/icons/play:/icons/play 185 | 186 | 187 | 188 | 24 189 | 24 190 | 191 | 192 | 193 | false 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | 10 | ## [0.11.1] - 2024-06-30 11 | 12 | ### Fixed 13 | - Fix missing log directory at first startup (Thanks to @ensemblebd) 14 | - Settings management 15 | 16 | 17 | ## [0.11.0] - 2024-05-22 18 | 19 | ### Fixed 20 | - Fix combo selection after model reset (Thanks to @virtulis) 21 | 22 | ## Added 23 | - Template button to reload previous timesheet without starting it (Thanks to @poelzi) 24 | - Allow usage of API Token for kimai>=2.14.0 25 | 26 | 27 | ## [0.10.0] - 2023-07-17 28 | 29 | ### Fixed 30 | - Bad MacOSX layout resizing [#96](https://github.com/AlexandrePTJ/kemai/issues/96) 31 | 32 | ### Added 33 | - About dialog [#95](https://github.com/AlexandrePTJ/kemai/issues/95) 34 | - Recent time sheets history [#86](https://github.com/AlexandrePTJ/kemai/issues/86) 35 | - Log journal widget [#58](https://github.com/AlexandrePTJ/kemai/issues/58) 36 | 37 | 38 | ## [0.9.4] - 2023-05-10 39 | 40 | ### Changed 41 | - Force gcc-10 for linux build to have better compatibility over linux distros 42 | 43 | 44 | ## [0.9.3] - 2023-05-08 45 | 46 | ### Fixed 47 | - Sort regression [#84](https://github.com/AlexandrePTJ/kemai/issues/84). 48 | - SSL problems on some linux distro [#65](https://github.com/AlexandrePTJ/kemai/issues/65). 49 | - Profile name not updated in menu [#82](https://github.com/AlexandrePTJ/kemai/issues/82). 50 | - List projects with same name [#83](https://github.com/AlexandrePTJ/kemai/issues/83). 51 | 52 | ### Changed 53 | - Replace conan for dependencies management with CMake built-in FetchContent 54 | 55 | 56 | ## [0.9.2] - 2023-04-04 57 | 58 | ### Fixed 59 | - Profiles not saved [#81](https://github.com/AlexandrePTJ/kemai/issues/81). 60 | 61 | 62 | ## [0.9.1] - 2023-03-28 63 | 64 | ### Added 65 | - Add 'refresh cache' menu item 66 | 67 | ### Fixed 68 | - Fix filter on Customer / Projects / Activities 69 | 70 | 71 | ## [0.9.0] - 2023-03-27 72 | 73 | Special thanks to @shrippen for its support through [sponsorship](https://github.com/sponsors/AlexandrePTJ). 74 | 75 | ### Added 76 | - Periodic check of current timesheet [#80](https://github.com/AlexandrePTJ/kemai/issues/80). 77 | 78 | ### Fixed 79 | - Regression resetting dropdown fields when stopping timesheet [#77](https://github.com/AlexandrePTJ/kemai/issues/77). 80 | 81 | ### Changed 82 | - Allow selection of project without first selecting customer [#74](https://github.com/AlexandrePTJ/kemai/issues/74). 83 | - Improve Loading of Customer / Projects / Activities [#44](https://github.com/AlexandrePTJ/kemai/issues/44). 84 | - Improve filter on Customer / Projects / Activities [#75](https://github.com/AlexandrePTJ/kemai/issues/75). 85 | 86 | 87 | ## [0.8.0] - 2023-03-14 88 | 89 | ### Added 90 | - TimeSheet description and tags are saved on stop [#43](https://github.com/AlexandrePTJ/kemai/issues/43). 91 | - Option to enable/disable Kemai's update check [#68](https://github.com/AlexandrePTJ/kemai/issues/68). 92 | - Can stop current timesheet when idle is detected [#63](https://github.com/AlexandrePTJ/kemai/issues/63). 93 | - Czech translation (Thanks to @office256) 94 | - Dutch translation (Thanks to Thomas) 95 | - Turkish translation (Thanks to Mert Saraç) 96 | 97 | ### Fixed 98 | - Enhance MacOSX generated dmg [#54](https://github.com/AlexandrePTJ/kemai/issues/54). 99 | 100 | ### Changed 101 | - Migrate to GitHub Actions 102 | - Changelog format 103 | - Kimai API client refactoring 104 | 105 | 106 | ## [0.7.1] - 2022-06-21 107 | 108 | ### Fixed 109 | - Settings not saved [#51](https://github.com/AlexandrePTJ/kemai/issues/51). 110 | - Missing translations [#52](https://github.com/AlexandrePTJ/kemai/issues/52). 111 | 112 | 113 | ## [0.7.0] - 2022-06-19 114 | 115 | ### Added 116 | - Use connection profiles to allow several kimai endpoints [#46](https://github.com/AlexandrePTJ/kemai/issues/46). 117 | - Minimize to tray [#47](https://github.com/AlexandrePTJ/kemai/issues/47). 118 | - Add greek translations (Thanks to @dkstiler). 119 | - 120 | ### Fixed 121 | - Fix tab order (Thanks to @sdreilinger) [#50](https://github.com/AlexandrePTJ/kemai/issues/50). 122 | 123 | 124 | ## [0.6.0] - 2021-11-11 125 | 126 | ### Added 127 | - Add translations (de, fr, hr, nb_NO) [#23](https://github.com/AlexandrePTJ/kemai/issues/23). 128 | - Setup connection to Kimai without the need to restart [#21](https://github.com/AlexandrePTJ/kemai/issues/21). 129 | - Support 'timetracking' mode [#37](https://github.com/AlexandrePTJ/kemai/issues/37). 130 | - Add preliminary support to Kimai TaskManagement plugin [#25](https://github.com/AlexandrePTJ/kemai/issues/25). 131 | 132 | ### Changed 133 | - Migrate to Qt 6 [#27](https://github.com/AlexandrePTJ/kemai/issues/27). 134 | 135 | 136 | ## [0.5.0] - 2020-10-23 137 | 138 | ### Added 139 | - Provide AppImage for Linux [#17](https://github.com/AlexandrePTJ/kemai/issues/17). 140 | - Save / Restore window geometry [#16](https://github.com/AlexandrePTJ/kemai/issues/16). 141 | - Add description and tags to activity widget [#15](https://github.com/AlexandrePTJ/kemai/issues/15). 142 | 143 | 144 | ## [0.4.0] - 2020-08-20 145 | 146 | ### Changed 147 | - Set token edit field as password [#14](https://github.com/AlexandrePTJ/kemai/issues/14). 148 | - Better handling of SSL errors [#13](https://github.com/AlexandrePTJ/kemai/issues/13). 149 | - Change icon when timer is running (Thanks to @maxguru) [#12](https://github.com/AlexandrePTJ/kemai/pull/12). 150 | 151 | ### Fixed 152 | - Fix sizes policies (Thanks to @maxguru) [#12](https://github.com/AlexandrePTJ/kemai/pull/12). 153 | 154 | 155 | ## [0.3.1] - 2020-06-11 156 | 157 | ### Changed 158 | - Allow usage of CMake >= 3.11 [#10](https://github.com/AlexandrePTJ/kemai/issues/10) 159 | 160 | 161 | ## [0.3.0] - 2020-06-09 162 | 163 | ### Added 164 | - Add budget and timeBudget to activity, project and customer dialogs 165 | - Add rotating file logger 166 | 167 | ### Fixed 168 | - Fix 'Not acceptable' [#9](https://github.com/AlexandrePTJ/kemai/issues/9) 169 | 170 | 171 | ## [0.2.1] - 2020-05-20 172 | 173 | ### Fixed 174 | - Add OpenSSL binaries to Windows installer [#8](https://github.com/AlexandrePTJ/kemai/issues/8). 175 | - Fix Windows installer upgrade process [#7](https://github.com/AlexandrePTJ/kemai/issues/7). 176 | 177 | 178 | ## [0.2.0] - 2020-05-14 179 | 180 | ### Added 181 | - Add updater [#3](https://github.com/AlexandrePTJ/kemai/issues/3). 182 | 183 | ### Fixed 184 | - Handle user timezone correctly [#6](https://github.com/AlexandrePTJ/kemai/issues/6). 185 | - Fix host with subpath [#5](https://github.com/AlexandrePTJ/kemai/issues/5). 186 | 187 | 188 | ## [0.1.1] - 2020-05-09 189 | 190 | - Fix minimal OSX version [#2](https://github.com/AlexandrePTJ/kemai/issues/2). 191 | 192 | 193 | ## [0.1.0] - 2020-05-04 194 | 195 | Initial version. 196 | 197 | 198 | [Unreleased]: https://github.com/AlexandrePTJ/kemai/compare/0.11.1...HEAD 199 | [0.11.1]: https://github.com/AlexandrePTJ/kemai/compare/0.11.0...0.11.1 200 | [0.11.0]: https://github.com/AlexandrePTJ/kemai/compare/0.10.0...0.11.0 201 | [0.10.0]: https://github.com/AlexandrePTJ/kemai/compare/0.9.4...0.10.0 202 | [0.9.4]: https://github.com/AlexandrePTJ/kemai/compare/0.9.3...0.9.4 203 | [0.9.3]: https://github.com/AlexandrePTJ/kemai/compare/0.9.2...0.9.3 204 | [0.9.2]: https://github.com/AlexandrePTJ/kemai/compare/0.9.1...0.9.2 205 | [0.9.1]: https://github.com/AlexandrePTJ/kemai/compare/0.9.0...0.9.1 206 | [0.9.0]: https://github.com/AlexandrePTJ/kemai/compare/0.8.0...0.9.0 207 | [0.8.0]: https://github.com/AlexandrePTJ/kemai/compare/0.7.1...0.8.0 208 | [0.7.1]: https://github.com/AlexandrePTJ/kemai/compare/0.7.0...0.7.1 209 | [0.7.0]: https://github.com/AlexandrePTJ/kemai/compare/0.6.0...0.7.0 210 | [0.6.0]: https://github.com/AlexandrePTJ/kemai/compare/0.5.0...0.6.0 211 | [0.5.0]: https://github.com/AlexandrePTJ/kemai/compare/0.4.0...0.5.0 212 | [0.4.0]: https://github.com/AlexandrePTJ/kemai/compare/0.3.1...0.4.0 213 | [0.3.1]: https://github.com/AlexandrePTJ/kemai/compare/0.3.0...0.3.1 214 | [0.3.0]: https://github.com/AlexandrePTJ/kemai/compare/0.2.1...0.3.0 215 | [0.2.1]: https://github.com/AlexandrePTJ/kemai/compare/0.2.0...0.2.1 216 | [0.2.0]: https://github.com/AlexandrePTJ/kemai/compare/0.1.1...0.2.0 217 | [0.1.1]: https://github.com/AlexandrePTJ/kemai/compare/0.1.0...0.1.1 218 | [0.1.0]: https://github.com/AlexandrePTJ/kemai/releases/tag/0.1.0 219 | -------------------------------------------------------------------------------- /src/settings/settings.cpp: -------------------------------------------------------------------------------- 1 | #include "settings.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace kemai; 14 | 15 | /* 16 | * Config file version 17 | */ 18 | static const auto gCfgVersion_0 = 0; // Ini format 19 | static const auto gCfgVersion_1 = 1; // Migrate to JSON 20 | static const auto gCfgVersion_2 = 2; // Add AutoRefreshCurrentTimeSheet setting 21 | static const auto gCfgVersion_3 = 3; // Add API Token to profile 22 | static const auto gCurrentCfgVersion = gCfgVersion_3; 23 | 24 | /* 25 | * Static helpers 26 | */ 27 | QSettings getQSettingsInstance() 28 | { 29 | return {QSettings::IniFormat, QSettings::UserScope, qApp->organizationName(), qApp->applicationName()}; 30 | } 31 | 32 | QString getJsonSettingsPath() 33 | { 34 | return QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)).absoluteFilePath("settings.json"); 35 | } 36 | 37 | Settings loadFromLegacyIni(const QSettings& qset) 38 | { 39 | Settings settings; 40 | 41 | QString kimaiGrpPrefix; 42 | QString kemaiGrpPrefix; 43 | 44 | if (qset.childGroups().contains("kimai")) 45 | { 46 | kimaiGrpPrefix = "kimai/"; 47 | } 48 | if (qset.childGroups().contains("kemai")) 49 | { 50 | kemaiGrpPrefix = "kemai/"; 51 | } 52 | 53 | Settings::Profile defaultProfile; 54 | defaultProfile.id = QUuid::createUuid(); 55 | defaultProfile.name = "default"; 56 | defaultProfile.host = qset.value(kimaiGrpPrefix + "host").toString(); 57 | defaultProfile.username = qset.value(kimaiGrpPrefix + "username").toString(); 58 | defaultProfile.token = qset.value(kimaiGrpPrefix + "token").toString(); 59 | settings.profiles.push_back(defaultProfile); 60 | 61 | settings.trustedCertificates = qset.value(kimaiGrpPrefix + "trustedCertificates", {}).toStringList(); 62 | 63 | settings.kemai.closeToSystemTray = qset.value(kemaiGrpPrefix + "closeToSystemTray", false).toBool(); 64 | settings.kemai.minimizeToSystemTray = qset.value(kemaiGrpPrefix + "minimizeToSystemTray", false).toBool(); 65 | settings.kemai.ignoredVersion = qset.value(kemaiGrpPrefix + "ignoredVersion", "0.0.0").toString(); 66 | settings.kemai.geometry = qset.value(kemaiGrpPrefix + "geometry").toByteArray(); 67 | settings.kemai.language = qset.value(kemaiGrpPrefix + "language", QLocale::system()).toLocale(); 68 | 69 | return settings; 70 | } 71 | 72 | /* 73 | * Class impl 74 | */ 75 | Settings SettingsHelper::load() 76 | { 77 | auto settings = Settings{}; 78 | 79 | auto qset = getQSettingsInstance(); 80 | auto jsonSettingsPath = getJsonSettingsPath(); 81 | 82 | // Migrate from ini settings to json 83 | if (QFile::exists(qset.fileName()) && !QFile::exists(jsonSettingsPath)) 84 | { 85 | save(loadFromLegacyIni(qset)); 86 | } 87 | 88 | // Load from previous save 89 | if (QFile::exists(jsonSettingsPath)) 90 | { 91 | QFile jsonFile(jsonSettingsPath); 92 | jsonFile.open(QIODevice::ReadOnly | QIODevice::Text); 93 | auto jsonDocument = QJsonDocument::fromJson(jsonFile.readAll()); 94 | auto root = jsonDocument.object(); 95 | jsonFile.close(); 96 | 97 | const auto cfgVersion = root.value("version").toInt(); 98 | 99 | for (const auto& certificationValue : root.value("trustedCertificates").toArray()) 100 | { 101 | settings.trustedCertificates.append(certificationValue.toString()); 102 | } 103 | 104 | auto kemaiObject = root.value("kemai").toObject(); 105 | settings.kemai.closeToSystemTray = kemaiObject.value("closeToSystemTray").toBool(); 106 | settings.kemai.minimizeToSystemTray = kemaiObject.value("minimizeToSystemTray").toBool(); 107 | settings.kemai.checkUpdateAtStartup = kemaiObject.value("checkUpdateAtStartup").toBool(); 108 | settings.kemai.ignoredVersion = kemaiObject.value("ignoredVersion").toString(); 109 | settings.kemai.geometry = QByteArray::fromBase64(kemaiObject.value("geometry").toString().toLocal8Bit()); 110 | settings.kemai.language = QLocale(kemaiObject.value("language").toString()); 111 | if (kemaiObject.contains("lastConnectedProfile")) 112 | { 113 | settings.kemai.lastConnectedProfile = QUuid(kemaiObject.value("lastConnectedProfile").toString()); 114 | } 115 | 116 | for (const auto& profileValue : root.value("profiles").toArray()) 117 | { 118 | const auto profileObject = profileValue.toObject(); 119 | 120 | Settings::Profile profile; 121 | profile.id = QUuid(profileObject.value("id").toString()); 122 | profile.name = profileObject.value("name").toString(); 123 | profile.host = profileObject.value("host").toString(); 124 | profile.username = profileObject.value("username").toString(); 125 | profile.token = profileObject.value("token").toString(); 126 | 127 | if (cfgVersion >= gCfgVersion_3) 128 | { 129 | profile.apiToken = profileObject.value("apiToken").toString(); 130 | } 131 | 132 | settings.profiles.push_back(profile); 133 | } 134 | 135 | if (root.contains("events")) 136 | { 137 | auto eventsObject = root.value("events").toObject(); 138 | settings.events.stopOnLock = eventsObject.value("stopOnLock").toBool(); 139 | settings.events.stopOnIdle = eventsObject.value("stopOnIdle").toBool(); 140 | settings.events.idleDelayMinutes = eventsObject.value("idleDelayMinutes").toInt(); 141 | 142 | if (cfgVersion >= gCfgVersion_2) 143 | { 144 | settings.events.autoRefreshCurrentTimeSheet = eventsObject.value("autoRefreshCurrentTimeSheet").toBool(); 145 | settings.events.autoRefreshCurrentTimeSheetDelaySeconds = eventsObject.value("autoRefreshCurrentTimeSheetDelaySeconds").toInt(); 146 | } 147 | } 148 | } 149 | return settings; 150 | } 151 | 152 | void SettingsHelper::save(const Settings& settings) 153 | { 154 | QJsonArray profilesArray; 155 | for (const auto& profile : settings.profiles) 156 | { 157 | QJsonObject profileObject; 158 | profileObject["id"] = profile.id.toString(); 159 | profileObject["name"] = profile.name; 160 | profileObject["host"] = profile.host; 161 | profileObject["username"] = profile.username; 162 | profileObject["token"] = profile.token; 163 | profileObject["apiToken"] = profile.apiToken; 164 | profilesArray.append(profileObject); 165 | } 166 | 167 | QJsonObject kemaiObject; 168 | kemaiObject["closeToSystemTray"] = settings.kemai.closeToSystemTray; 169 | kemaiObject["minimizeToSystemTray"] = settings.kemai.minimizeToSystemTray; 170 | kemaiObject["checkUpdateAtStartup"] = settings.kemai.checkUpdateAtStartup; 171 | kemaiObject["ignoredVersion"] = settings.kemai.ignoredVersion; 172 | kemaiObject["geometry"] = QString(settings.kemai.geometry.toBase64()); 173 | kemaiObject["language"] = settings.kemai.language.name(); 174 | kemaiObject["lastConnectedProfile"] = settings.kemai.lastConnectedProfile.toString(); 175 | 176 | QJsonObject eventsObject; 177 | eventsObject["stopOnLock"] = settings.events.stopOnLock; 178 | eventsObject["stopOnIdle"] = settings.events.stopOnIdle; 179 | eventsObject["idleDelayMinutes"] = settings.events.idleDelayMinutes; 180 | eventsObject["autoRefreshCurrentTimeSheet"] = settings.events.autoRefreshCurrentTimeSheet; 181 | eventsObject["autoRefreshCurrentTimeSheetDelaySeconds"] = settings.events.autoRefreshCurrentTimeSheetDelaySeconds; 182 | 183 | QJsonObject root; 184 | root["version"] = gCurrentCfgVersion; 185 | root["profiles"] = profilesArray; 186 | root["trustedCertificates"] = QJsonArray::fromStringList(settings.trustedCertificates); 187 | root["kemai"] = kemaiObject; 188 | root["events"] = eventsObject; 189 | 190 | QFileInfo jsonFileInfo(getJsonSettingsPath()); 191 | if (!jsonFileInfo.exists()) 192 | { 193 | if (!jsonFileInfo.absoluteDir().exists()) 194 | { 195 | jsonFileInfo.absoluteDir().mkpath("."); 196 | } 197 | } 198 | 199 | QFile jsonFile(jsonFileInfo.filePath()); 200 | jsonFile.open(QIODevice::WriteOnly | QIODevice::Text); 201 | 202 | QTextStream testStream(&jsonFile); 203 | testStream << QJsonDocument(root).toJson(); 204 | 205 | jsonFile.close(); 206 | } 207 | -------------------------------------------------------------------------------- /src/resources/data/iso4217.json: -------------------------------------------------------------------------------- 1 | { 2 | "AFN": "Afghan Afghani", 3 | "AFA": "Afghan Afghani (1927–2002)", 4 | "ALL": "Albanian Lek", 5 | "ALK": "Albanian Lek (1946–1965)", 6 | "DZD": "Algerian Dinar", 7 | "ADP": "Andorran Peseta", 8 | "AOA": "Angolan Kwanza", 9 | "AOK": "Angolan Kwanza (1977–1991)", 10 | "AON": "Angolan New Kwanza (1990–2000)", 11 | "AOR": "Angolan Readjusted Kwanza (1995–1999)", 12 | "ARA": "Argentine Austral", 13 | "ARS": "Argentine Peso", 14 | "ARM": "Argentine Peso (1881–1970)", 15 | "ARP": "Argentine Peso (1983–1985)", 16 | "ARL": "Argentine Peso Ley (1970–1983)", 17 | "AMD": "Armenian Dram", 18 | "AWG": "Aruban Florin", 19 | "AUD": "Australian Dollar", 20 | "ATS": "Austrian Schilling", 21 | "AZN": "Azerbaijani Manat", 22 | "AZM": "Azerbaijani Manat (1993–2006)", 23 | "BSD": "Bahamian Dollar", 24 | "BHD": "Bahraini Dinar", 25 | "BDT": "Bangladeshi Taka", 26 | "BBD": "Barbadian Dollar", 27 | "BYN": "Belarusian Ruble", 28 | "BYB": "Belarusian Ruble (1994–1999)", 29 | "BYR": "Belarusian Ruble (2000–2016)", 30 | "BEF": "Belgian Franc", 31 | "BEC": "Belgian Franc (convertible)", 32 | "BEL": "Belgian Franc (financial)", 33 | "BZD": "Belize Dollar", 34 | "BMD": "Bermudan Dollar", 35 | "BTN": "Bhutanese Ngultrum", 36 | "BOB": "Bolivian Boliviano", 37 | "BOL": "Bolivian Boliviano (1863–1963)", 38 | "BOV": "Bolivian Mvdol", 39 | "BOP": "Bolivian Peso", 40 | "BAM": "Bosnia-Herzegovina Convertible Mark", 41 | "BAD": "Bosnia-Herzegovina Dinar (1992–1994)", 42 | "BAN": "Bosnia-Herzegovina New Dinar (1994–1997)", 43 | "BWP": "Botswanan Pula", 44 | "BRC": "Brazilian Cruzado (1986–1989)", 45 | "BRZ": "Brazilian Cruzeiro (1942–1967)", 46 | "BRE": "Brazilian Cruzeiro (1990–1993)", 47 | "BRR": "Brazilian Cruzeiro (1993–1994)", 48 | "BRN": "Brazilian New Cruzado (1989–1990)", 49 | "BRB": "Brazilian New Cruzeiro (1967–1986)", 50 | "BRL": "Brazilian Real", 51 | "GBP": "British Pound", 52 | "BND": "Brunei Dollar", 53 | "BGL": "Bulgarian Hard Lev", 54 | "BGN": "Bulgarian Lev", 55 | "BGO": "Bulgarian Lev (1879–1952)", 56 | "BGM": "Bulgarian Socialist Lev", 57 | "BUK": "Burmese Kyat", 58 | "BIF": "Burundian Franc", 59 | "KHR": "Cambodian Riel", 60 | "CAD": "Canadian Dollar", 61 | "CVE": "Cape Verdean Escudo", 62 | "KYD": "Cayman Islands Dollar", 63 | "XAF": "Central African CFA Franc", 64 | "XPF": "CFP Franc", 65 | "CLE": "Chilean Escudo", 66 | "CLP": "Chilean Peso", 67 | "CLF": "Chilean Unit of Account (UF)", 68 | "CNX": "Chinese People’s Bank Dollar", 69 | "CNY": "Chinese Yuan", 70 | "CNH": "Chinese Yuan (offshore)", 71 | "COP": "Colombian Peso", 72 | "COU": "Colombian Real Value Unit", 73 | "KMF": "Comorian Franc", 74 | "CDF": "Congolese Franc", 75 | "CRC": "Costa Rican Colón", 76 | "HRD": "Croatian Dinar", 77 | "HRK": "Croatian Kuna", 78 | "CUC": "Cuban Convertible Peso", 79 | "CUP": "Cuban Peso", 80 | "CYP": "Cypriot Pound", 81 | "CZK": "Czech Koruna", 82 | "CSK": "Czechoslovak Hard Koruna", 83 | "DKK": "Danish Krone", 84 | "DJF": "Djiboutian Franc", 85 | "DOP": "Dominican Peso", 86 | "NLG": "Dutch Guilder", 87 | "XCD": "East Caribbean Dollar", 88 | "DDM": "East German Mark", 89 | "ECS": "Ecuadorian Sucre", 90 | "ECV": "Ecuadorian Unit of Constant Value", 91 | "EGP": "Egyptian Pound", 92 | "GQE": "Equatorial Guinean Ekwele", 93 | "ERN": "Eritrean Nakfa", 94 | "EEK": "Estonian Kroon", 95 | "ETB": "Ethiopian Birr", 96 | "EUR": "Euro", 97 | "XEU": "European Currency Unit", 98 | "FKP": "Falkland Islands Pound", 99 | "FJD": "Fijian Dollar", 100 | "FIM": "Finnish Markka", 101 | "FRF": "French Franc", 102 | "XFO": "French Gold Franc", 103 | "XFU": "French UIC-Franc", 104 | "GMD": "Gambian Dalasi", 105 | "GEK": "Georgian Kupon Larit", 106 | "GEL": "Georgian Lari", 107 | "DEM": "German Mark", 108 | "GHS": "Ghanaian Cedi", 109 | "GHC": "Ghanaian Cedi (1979–2007)", 110 | "GIP": "Gibraltar Pound", 111 | "GRD": "Greek Drachma", 112 | "GTQ": "Guatemalan Quetzal", 113 | "GWP": "Guinea-Bissau Peso", 114 | "GNF": "Guinean Franc", 115 | "GNS": "Guinean Syli", 116 | "GYD": "Guyanaese Dollar", 117 | "HTG": "Haitian Gourde", 118 | "HNL": "Honduran Lempira", 119 | "HKD": "Hong Kong Dollar", 120 | "HUF": "Hungarian Forint", 121 | "ISK": "Icelandic Króna", 122 | "ISJ": "Icelandic Króna (1918–1981)", 123 | "INR": "Indian Rupee", 124 | "IDR": "Indonesian Rupiah", 125 | "IRR": "Iranian Rial", 126 | "IQD": "Iraqi Dinar", 127 | "IEP": "Irish Pound", 128 | "ILS": "Israeli New Shekel", 129 | "ILP": "Israeli Pound", 130 | "ILR": "Israeli Shekel (1980–1985)", 131 | "ITL": "Italian Lira", 132 | "JMD": "Jamaican Dollar", 133 | "JPY": "Japanese Yen", 134 | "JOD": "Jordanian Dinar", 135 | "KZT": "Kazakhstani Tenge", 136 | "KES": "Kenyan Shilling", 137 | "KWD": "Kuwaiti Dinar", 138 | "KGS": "Kyrgystani Som", 139 | "LAK": "Laotian Kip", 140 | "LVL": "Latvian Lats", 141 | "LVR": "Latvian Ruble", 142 | "LBP": "Lebanese Pound", 143 | "LSL": "Lesotho Loti", 144 | "LRD": "Liberian Dollar", 145 | "LYD": "Libyan Dinar", 146 | "LTL": "Lithuanian Litas", 147 | "LTT": "Lithuanian Talonas", 148 | "LUL": "Luxembourg Financial Franc", 149 | "LUC": "Luxembourgian Convertible Franc", 150 | "LUF": "Luxembourgian Franc", 151 | "MOP": "Macanese Pataca", 152 | "MKD": "Macedonian Denar", 153 | "MKN": "Macedonian Denar (1992–1993)", 154 | "MGA": "Malagasy Ariary", 155 | "MGF": "Malagasy Franc", 156 | "MWK": "Malawian Kwacha", 157 | "MYR": "Malaysian Ringgit", 158 | "MVR": "Maldivian Rufiyaa", 159 | "MVP": "Maldivian Rupee (1947–1981)", 160 | "MLF": "Malian Franc", 161 | "MTL": "Maltese Lira", 162 | "MTP": "Maltese Pound", 163 | "MRU": "Mauritanian Ouguiya", 164 | "MRO": "Mauritanian Ouguiya (1973–2017)", 165 | "MUR": "Mauritian Rupee", 166 | "MXV": "Mexican Investment Unit", 167 | "MXN": "Mexican Peso", 168 | "MXP": "Mexican Silver Peso (1861–1992)", 169 | "MDC": "Moldovan Cupon", 170 | "MDL": "Moldovan Leu", 171 | "MCF": "Monegasque Franc", 172 | "MNT": "Mongolian Tugrik", 173 | "MAD": "Moroccan Dirham", 174 | "MAF": "Moroccan Franc", 175 | "MZE": "Mozambican Escudo", 176 | "MZN": "Mozambican Metical", 177 | "MZM": "Mozambican Metical (1980–2006)", 178 | "MMK": "Myanmar Kyat", 179 | "NAD": "Namibian Dollar", 180 | "NPR": "Nepalese Rupee", 181 | "ANG": "Netherlands Antillean Guilder", 182 | "TWD": "New Taiwan Dollar", 183 | "NZD": "New Zealand Dollar", 184 | "NIO": "Nicaraguan Córdoba", 185 | "NIC": "Nicaraguan Córdoba (1988–1991)", 186 | "NGN": "Nigerian Naira", 187 | "KPW": "North Korean Won", 188 | "NOK": "Norwegian Krone", 189 | "OMR": "Omani Rial", 190 | "PKR": "Pakistani Rupee", 191 | "PAB": "Panamanian Balboa", 192 | "PGK": "Papua New Guinean Kina", 193 | "PYG": "Paraguayan Guarani", 194 | "PEI": "Peruvian Inti", 195 | "PEN": "Peruvian Sol", 196 | "PES": "Peruvian Sol (1863–1965)", 197 | "PHP": "Philippine Piso", 198 | "PLN": "Polish Zloty", 199 | "PLZ": "Polish Zloty (1950–1995)", 200 | "PTE": "Portuguese Escudo", 201 | "GWE": "Portuguese Guinea Escudo", 202 | "QAR": "Qatari Rial", 203 | "RHD": "Rhodesian Dollar", 204 | "XRE": "RINET Funds", 205 | "RON": "Romanian Leu", 206 | "ROL": "Romanian Leu (1952–2006)", 207 | "RUB": "Russian Ruble", 208 | "RUR": "Russian Ruble (1991–1998)", 209 | "RWF": "Rwandan Franc", 210 | "SVC": "Salvadoran Colón", 211 | "WST": "Samoan Tala", 212 | "STN": "São Tomé & Príncipe Dobra", 213 | "STD": "São Tomé & Príncipe Dobra (1977–2017)", 214 | "SAR": "Saudi Riyal", 215 | "RSD": "Serbian Dinar", 216 | "CSD": "Serbian Dinar (2002–2006)", 217 | "SCR": "Seychellois Rupee", 218 | "SLL": "Sierra Leonean Leone", 219 | "SGD": "Singapore Dollar", 220 | "SKK": "Slovak Koruna", 221 | "SIT": "Slovenian Tolar", 222 | "SBD": "Solomon Islands Dollar", 223 | "SOS": "Somali Shilling", 224 | "ZAR": "South African Rand", 225 | "ZAL": "South African Rand (financial)", 226 | "KRH": "South Korean Hwan (1953–1962)", 227 | "KRW": "South Korean Won", 228 | "KRO": "South Korean Won (1945–1953)", 229 | "SSP": "South Sudanese Pound", 230 | "SUR": "Soviet Rouble", 231 | "ESP": "Spanish Peseta", 232 | "ESA": "Spanish Peseta (A account)", 233 | "ESB": "Spanish Peseta (convertible account)", 234 | "LKR": "Sri Lankan Rupee", 235 | "SHP": "St. Helena Pound", 236 | "SDD": "Sudanese Dinar (1992–2007)", 237 | "SDG": "Sudanese Pound", 238 | "SDP": "Sudanese Pound (1957–1998)", 239 | "SRD": "Surinamese Dollar", 240 | "SRG": "Surinamese Guilder", 241 | "SZL": "Swazi Lilangeni", 242 | "SEK": "Swedish Krona", 243 | "CHF": "Swiss Franc", 244 | "SYP": "Syrian Pound", 245 | "TJR": "Tajikistani Ruble", 246 | "TJS": "Tajikistani Somoni", 247 | "TZS": "Tanzanian Shilling", 248 | "THB": "Thai Baht", 249 | "TPE": "Timorese Escudo", 250 | "TOP": "Tongan Paʻanga", 251 | "TTD": "Trinidad & Tobago Dollar", 252 | "TND": "Tunisian Dinar", 253 | "TRY": "Turkish Lira", 254 | "TRL": "Turkish Lira (1922–2005)", 255 | "TMT": "Turkmenistani Manat", 256 | "TMM": "Turkmenistani Manat (1993–2009)", 257 | "UGX": "Ugandan Shilling", 258 | "UGS": "Ugandan Shilling (1966–1987)", 259 | "UAH": "Ukrainian Hryvnia", 260 | "UAK": "Ukrainian Karbovanets", 261 | "AED": "United Arab Emirates Dirham", 262 | "UYW": "Uruguayan Nominal Wage Index Unit", 263 | "UYU": "Uruguayan Peso", 264 | "UYP": "Uruguayan Peso (1975–1993)", 265 | "UYI": "Uruguayan Peso (Indexed Units)", 266 | "USD": "US Dollar", 267 | "USN": "US Dollar (Next day)", 268 | "USS": "US Dollar (Same day)", 269 | "UZS": "Uzbekistani Som", 270 | "VUV": "Vanuatu Vatu", 271 | "VES": "Venezuelan Bolívar", 272 | "VEB": "Venezuelan Bolívar (1871–2008)", 273 | "VEF": "Venezuelan Bolívar (2008–2018)", 274 | "VND": "Vietnamese Dong", 275 | "VNN": "Vietnamese Dong (1978–1985)", 276 | "XOF": "West African CFA Franc", 277 | "CHE": "WIR Euro", 278 | "CHW": "WIR Franc", 279 | "YDD": "Yemeni Dinar", 280 | "YER": "Yemeni Rial", 281 | "YUN": "Yugoslavian Convertible Dinar (1990–1992)", 282 | "YUD": "Yugoslavian Hard Dinar (1966–1990)", 283 | "YUM": "Yugoslavian New Dinar (1994–2002)", 284 | "YUR": "Yugoslavian Reformed Dinar (1992–1993)", 285 | "ZRN": "Zairean New Zaire (1993–1998)", 286 | "ZRZ": "Zairean Zaire (1971–1993)", 287 | "ZMW": "Zambian Kwacha", 288 | "ZMK": "Zambian Kwacha (1968–2012)", 289 | "ZWD": "Zimbabwean Dollar (1980–2008)", 290 | "ZWR": "Zimbabwean Dollar (2008)", 291 | "ZWL": "Zimbabwean Dollar (2009)" 292 | } --------------------------------------------------------------------------------