Compare commits
14 Commits
f2bff8d0e2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c5ae5294ca | |||
| eb43364f0f | |||
| 3241dd674d | |||
| 8a10b6013b | |||
| d998b16ae4 | |||
| f055ad9cc4 | |||
| 955395345e | |||
| c7d921f774 | |||
| 69d0c9543a | |||
| 53384337bd | |||
| 9b51e3f3e7 | |||
| 46504c1b06 | |||
| 60090062dc | |||
| 65d44ed5f8 |
@@ -6,6 +6,8 @@ set(CMAKE_AUTOMOC ON)
|
|||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
|
||||||
find_package(Qt5 COMPONENTS
|
find_package(Qt5 COMPONENTS
|
||||||
Core
|
Core
|
||||||
Gui
|
Gui
|
||||||
@@ -19,6 +21,8 @@ file(GLOB
|
|||||||
"src/*"
|
"src/*"
|
||||||
"src/MainWidget/*"
|
"src/MainWidget/*"
|
||||||
"src/LicenseModel/*"
|
"src/LicenseModel/*"
|
||||||
|
"src/EditClientDialog/*"
|
||||||
|
"src/LicenseGenerator/*"
|
||||||
"deps/deps.qrc"
|
"deps/deps.qrc"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,6 +43,8 @@ target_link_libraries(
|
|||||||
Qt5::Gui
|
Qt5::Gui
|
||||||
Qt5::Sql
|
Qt5::Sql
|
||||||
Qt5::Widgets
|
Qt5::Widgets
|
||||||
|
OpenSSL::Crypto
|
||||||
|
OpenSSL::SSL
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(Git QUIET)
|
find_package(Git QUIET)
|
||||||
|
|||||||
145
README.md
Normal file
145
README.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# LicenseManager — пользовательская документация
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
LicenseManager — настольное приложение на Qt5 для ведения базы клиентов и выпуска файлов лицензий на основе аппаратного идентификатора.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
- хранение клиентов в локальной базе SQLite;
|
||||||
|
- добавление, редактирование и удаление записей;
|
||||||
|
- импорт данных клиента из JSON-файла (drag-and-drop или выбор файла);
|
||||||
|
- выдача файла лицензии по выбранной записи;
|
||||||
|
- отображение статуса базы и информации о сборке.
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
- ОС: Linux/Windows/macOS (Qt5);
|
||||||
|
- зависимости при сборке: Qt5 (Core, Gui, Widgets, Sql), OpenSSL;
|
||||||
|
- для SQLite: системный драйвер Qt `QSQLITE` (например, пакет `libqt5sql5-sqlite` в Debian/Ubuntu).
|
||||||
|
|
||||||
|
## Сборка из исходников
|
||||||
|
```bash
|
||||||
|
cmake -S . -B build
|
||||||
|
cmake --build build -j
|
||||||
|
./build/LicenseManager
|
||||||
|
```
|
||||||
|
|
||||||
|
## Установка как .deb (опционально)
|
||||||
|
```bash
|
||||||
|
./deb/build.sh /path/to/LicenseManager
|
||||||
|
sudo dpkg -i licensemanager_*.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
## Первое использование и база данных
|
||||||
|
- Приложение хранит данные в `./db.sqlite` относительно текущей рабочей директории.
|
||||||
|
- При первом запуске, если структура базы отсутствует, приложение предложит создать базу.
|
||||||
|
- Если нужен фиксированный путь, запускайте приложение из нужной папки или измените `DB_PATH` в коде.
|
||||||
|
|
||||||
|
## Интерфейс
|
||||||
|
### Таблица клиентов
|
||||||
|
Столбцы:
|
||||||
|
- Фамилия, Имя, Отчество
|
||||||
|
- Email, Телефон
|
||||||
|
- Компания, Город
|
||||||
|
- Срок лицензии (если пусто — отображается `Permanent`)
|
||||||
|
- Комментарий
|
||||||
|
|
||||||
|
### Панель инструментов
|
||||||
|
- Add client — добавить запись;
|
||||||
|
- Edit client — редактировать выбранную запись;
|
||||||
|
- Delete client — удалить выбранные записи;
|
||||||
|
- Get info — информация о сборке.
|
||||||
|
|
||||||
|
### Контекстное меню таблицы
|
||||||
|
- Reload table — обновить список;
|
||||||
|
- Add/Edit/Delete client — действия с записями;
|
||||||
|
- Get license file — сформировать файл лицензии.
|
||||||
|
|
||||||
|
### Иконки действий
|
||||||
|
| Действие | Иконка | Файл |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| App icon | <img src="deps/icon.png" width="28" height="28" alt="App"> | `deps/icon.png` |
|
||||||
|
| Reload table | <img src="deps/reload.png" width="28" height="28" alt="Reload"> | `deps/reload.png` |
|
||||||
|
| Add client | <img src="deps/add.png" width="28" height="28" alt="Add"> | `deps/add.png` |
|
||||||
|
| Edit client | <img src="deps/edit.png" width="28" height="28" alt="Edit"> | `deps/edit.png` |
|
||||||
|
| Delete client | <img src="deps/delete.png" width="28" height="28" alt="Delete"> | `deps/delete.png` |
|
||||||
|
| Get info | <img src="deps/info.png" width="28" height="28" alt="Info"> | `deps/info.png` |
|
||||||
|
| Get license file | <img src="deps/getLicenseFile.png" width="28" height="28" alt="License file"> | `deps/getLicenseFile.png` |
|
||||||
|
| Drop files (автозаполнение) | <img src="deps/dropFiles.png" width="28" height="28" alt="Drop files"> | `deps/dropFiles.png` |
|
||||||
|
|
||||||
|
### Строка статуса
|
||||||
|
Показывает состояние базы (готова/ошибка/в работе).
|
||||||
|
|
||||||
|
## Работа с клиентами
|
||||||
|
### Добавление
|
||||||
|
1. Нажмите `Add client`.
|
||||||
|
2. Вкладка `File autofilled`:
|
||||||
|
- перетащите JSON-файл на область или нажмите и выберите файл;
|
||||||
|
- данные будут подставлены автоматически.
|
||||||
|
3. Вкладка `Main info`:
|
||||||
|
- заполните поля вручную.
|
||||||
|
4. Вкладка `License time`:
|
||||||
|
- включите `License is temporally`, чтобы выбрать дату окончания.
|
||||||
|
5. Нажмите `Save`.
|
||||||
|
|
||||||
|
Важно: поле `Hardware hash` можно редактировать только при добавлении записи.
|
||||||
|
|
||||||
|
### Редактирование
|
||||||
|
1. Выберите одну запись.
|
||||||
|
2. Нажмите `Edit client`.
|
||||||
|
3. Измените данные и нажмите `Save`.
|
||||||
|
|
||||||
|
Если нужно изменить `Hardware hash`, удалите запись и создайте новую.
|
||||||
|
|
||||||
|
### Удаление
|
||||||
|
Выберите записи и нажмите `Delete client` (или через контекстное меню). Подтвердите удаление.
|
||||||
|
|
||||||
|
### Обновление списка
|
||||||
|
Используйте `Reload table` для перечитывания данных из базы.
|
||||||
|
|
||||||
|
## Генерация файла лицензии
|
||||||
|
1. Выберите одну запись в таблице.
|
||||||
|
2. Нажмите `Get license file`.
|
||||||
|
3. Укажите место сохранения (по умолчанию — `Documents/` или `Home/`).
|
||||||
|
4. Файл сохраняется с расширением `.dat` (если расширение не указано, оно добавляется автоматически).
|
||||||
|
|
||||||
|
Внутри файла содержится зашифрованный (RSA) CBOR/JSON с полями:
|
||||||
|
- `machineId` — аппаратный идентификатор (`hardwareHash`);
|
||||||
|
- `validUntil` — дата окончания лицензии в ISO-формате (опционально).
|
||||||
|
|
||||||
|
## Формат JSON для авто‑заполнения
|
||||||
|
Пример:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lastName": "Иванов",
|
||||||
|
"firstName": "Иван",
|
||||||
|
"patronymic": "Иванович",
|
||||||
|
"email": "ivan@example.com",
|
||||||
|
"phone": "+7(777)777-77-77",
|
||||||
|
"city": "Москва",
|
||||||
|
"yourCompany": "ООО Ромашка",
|
||||||
|
"hardwareHash": "abcdef0123456789"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Любое поле можно опустить — оно останется пустым. Для выдачи лицензии важно заполнить `hardwareHash`.
|
||||||
|
|
||||||
|
## Хранилище данных
|
||||||
|
SQLite-файл: `db.sqlite` в рабочей директории.
|
||||||
|
|
||||||
|
Таблица `clients`:
|
||||||
|
- `id` (INTEGER, PK, AUTOINCREMENT)
|
||||||
|
- `city`, `email`, `firstName`, `hardwareHash`, `lastName`, `patronymic`, `phone`, `yourCompany` (TEXT, NOT NULL)
|
||||||
|
- `licenseTime` (TEXT, nullable)
|
||||||
|
- `comment` (TEXT, nullable)
|
||||||
|
|
||||||
|
## Настройки
|
||||||
|
Размеры окон сохраняются через `QSettings` (организация `LicenseManager`).
|
||||||
|
Расположение конфигурации зависит от ОС (на Linux обычно `~/.config/LicenseManager/`).
|
||||||
|
|
||||||
|
## Технический обзор (для сопровождения)
|
||||||
|
- Стек: C++20, Qt5 Widgets/Sql, OpenSSL, SQLite.
|
||||||
|
- Основные модули:
|
||||||
|
- `MainWidget` — главное окно, таблица, меню, выдача лицензии.
|
||||||
|
- `LicenseModel` — модель таблицы и работа с SQLite (в асинхронных задачах).
|
||||||
|
- `EditClientDialog` — диалог добавления/редактирования, импорт JSON.
|
||||||
|
- `LicenseGenerator`/`Crypt` — утилиты шифрования и генерации файла (часть функций не привязана к UI).
|
||||||
|
- Ресурсы Qt (`deps.qrc`) включают иконки, `tables.ddl` для инициализации БД и приватный ключ `Key`.
|
||||||
|
- Для смены ключа шифрования замените `deps/Key` и пересоберите проект.
|
||||||
9
deb/LicenseManager.desktop
Normal file
9
deb/LicenseManager.desktop
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=License Manager
|
||||||
|
Comment=License management application
|
||||||
|
Exec=LicenseManager
|
||||||
|
Icon=licensemanager
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;
|
||||||
|
StartupNotify=true
|
||||||
55
deb/build.sh
Executable file
55
deb/build.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ===== Проверка аргументов =====
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage: $0 /path/to/LicenseManager"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BIN_PATH="$(realpath "$1")"
|
||||||
|
|
||||||
|
if [ ! -f "$BIN_PATH" ]; then
|
||||||
|
echo "Error: binary not found: $BIN_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Параметры пакета =====
|
||||||
|
APP_NAME="licensemanager"
|
||||||
|
BIN_NAME="LicenseManager"
|
||||||
|
VERSION="0.1"
|
||||||
|
ARCH="$(dpkg --print-architecture)"
|
||||||
|
|
||||||
|
PKG_DIR="${APP_NAME}_${VERSION}_${ARCH}"
|
||||||
|
|
||||||
|
# ===== Сборка =====
|
||||||
|
echo "==> Cleaning"
|
||||||
|
rm -rf "$PKG_DIR" *.deb
|
||||||
|
|
||||||
|
echo "==> Creating package structure"
|
||||||
|
mkdir -p "$PKG_DIR/DEBIAN"
|
||||||
|
mkdir -p "$PKG_DIR/usr/bin"
|
||||||
|
mkdir -p "$PKG_DIR/usr/share/applications"
|
||||||
|
mkdir -p "$PKG_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||||
|
|
||||||
|
echo "==> Copy control file"
|
||||||
|
cp control "$PKG_DIR/DEBIAN/control"
|
||||||
|
|
||||||
|
echo "==> Copy binary"
|
||||||
|
cp "$BIN_PATH" "$PKG_DIR/usr/bin/$BIN_NAME"
|
||||||
|
chmod 0755 "$PKG_DIR/usr/bin/$BIN_NAME"
|
||||||
|
|
||||||
|
echo "==> Copy desktop file"
|
||||||
|
cp LicenseManager.desktop \
|
||||||
|
"$PKG_DIR/usr/share/applications/LicenseManager.desktop"
|
||||||
|
|
||||||
|
echo "==> Copy icon"
|
||||||
|
cp icon.png \
|
||||||
|
"$PKG_DIR/usr/share/icons/hicolor/256x256/apps/licensemanager.png"
|
||||||
|
|
||||||
|
echo "==> Building deb package"
|
||||||
|
dpkg-deb --build "$PKG_DIR"
|
||||||
|
|
||||||
|
echo "==> Done:"
|
||||||
|
ls -lh *.deb
|
||||||
|
|
||||||
10
deb/control
Normal file
10
deb/control
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Package: licensemanager
|
||||||
|
Version: 0.1
|
||||||
|
Section: utils
|
||||||
|
Priority: optional
|
||||||
|
Architecture: amd64
|
||||||
|
Maintainer: TrombonIp
|
||||||
|
Depends: libqt5core5a, libqt5gui5, libqt5widgets5, libqt5sql5, libqt5sql5-sqlite
|
||||||
|
Description: License Manager (Qt5)
|
||||||
|
Simple Qt5 application for managing licenses.
|
||||||
|
|
||||||
BIN
deb/icon.png
Normal file
BIN
deb/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
3
deps/deps.qrc
vendored
3
deps/deps.qrc
vendored
@@ -7,6 +7,9 @@
|
|||||||
<file alias="edit.png">edit.png</file>
|
<file alias="edit.png">edit.png</file>
|
||||||
<file alias="delete.png">delete.png</file>
|
<file alias="delete.png">delete.png</file>
|
||||||
<file alias="info.png">info.png</file>
|
<file alias="info.png">info.png</file>
|
||||||
|
<file alias="dropFiles.png">dropFiles.png</file>
|
||||||
|
<file alias="getLicenseFile.png">getLicenseFile.png</file>
|
||||||
<file alias="tables.ddl">tables.ddl</file>
|
<file alias="tables.ddl">tables.ddl</file>
|
||||||
|
<file alias="Key">Key</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
BIN
deps/dropFiles.png
vendored
Normal file
BIN
deps/dropFiles.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
deps/getLicenseFile.png
vendored
Normal file
BIN
deps/getLicenseFile.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 520 KiB |
BIN
deps/info.png
vendored
BIN
deps/info.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 500 KiB |
1
deps/tables.ddl
vendored
1
deps/tables.ddl
vendored
@@ -8,5 +8,6 @@ CREATE TABLE IF NOT EXISTS clients (
|
|||||||
patronymic TEXT NOT NULL,
|
patronymic TEXT NOT NULL,
|
||||||
phone TEXT NOT NULL,
|
phone TEXT NOT NULL,
|
||||||
yourCompany TEXT NOT NULL,
|
yourCompany TEXT NOT NULL,
|
||||||
|
licenseTime TEXT,
|
||||||
comment TEXT
|
comment TEXT
|
||||||
);
|
);
|
||||||
|
|||||||
85
src/EditClientDialog/DropFileWidget.cpp
Normal file
85
src/EditClientDialog/DropFileWidget.cpp
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#include "DropFileWidget.h"
|
||||||
|
|
||||||
|
// Qt
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QDropEvent>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMimeData>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
DropFileWidget::DropFileWidget(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setAcceptDrops(true);
|
||||||
|
|
||||||
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||||
|
setLayout(layout);
|
||||||
|
|
||||||
|
QIcon icon(":/deps/dropFiles.png");
|
||||||
|
QLabel *iconLabel = new QLabel(this);
|
||||||
|
iconLabel->setPixmap(icon.pixmap(300, 300));
|
||||||
|
iconLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(iconLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropFileWidget::dragEnterEvent(QDragEnterEvent *e)
|
||||||
|
{
|
||||||
|
if (e->mimeData()->hasUrls())
|
||||||
|
e->acceptProposedAction();
|
||||||
|
else
|
||||||
|
e->ignore();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropFileWidget::dragMoveEvent(QDragMoveEvent *e)
|
||||||
|
{
|
||||||
|
if (e->mimeData()->hasUrls())
|
||||||
|
e->acceptProposedAction();
|
||||||
|
else
|
||||||
|
e->ignore();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropFileWidget::dropEvent(QDropEvent *e)
|
||||||
|
{
|
||||||
|
if (e->mimeData()->hasUrls())
|
||||||
|
{
|
||||||
|
QStringList paths;
|
||||||
|
const auto urls = e->mimeData()->urls();
|
||||||
|
for (const QUrl &url : urls)
|
||||||
|
{
|
||||||
|
if (url.isLocalFile())
|
||||||
|
{
|
||||||
|
const QString path = url.toLocalFile();
|
||||||
|
if (!path.isEmpty())
|
||||||
|
paths << path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!paths.isEmpty())
|
||||||
|
{
|
||||||
|
emit filesDropped(paths);
|
||||||
|
e->acceptProposedAction();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
e->ignore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropFileWidget::mousePressEvent(QMouseEvent *e)
|
||||||
|
{
|
||||||
|
if (e->button() == Qt::LeftButton)
|
||||||
|
selectFiles();
|
||||||
|
QWidget::mousePressEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropFileWidget::selectFiles()
|
||||||
|
{
|
||||||
|
auto files = QFileDialog::getOpenFileNames(
|
||||||
|
this
|
||||||
|
, tr("Select config file")
|
||||||
|
, QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
|
||||||
|
);
|
||||||
|
emit filesDropped(files);
|
||||||
|
}
|
||||||
55
src/EditClientDialog/DropFileWidget.h
Normal file
55
src/EditClientDialog/DropFileWidget.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#ifndef LICENSEMANAGER_DROPFILEWIDGET_H
|
||||||
|
#define LICENSEMANAGER_DROPFILEWIDGET_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file DropFileWidget.h
|
||||||
|
* @brief Виджет для перетаскивания файлов.
|
||||||
|
*
|
||||||
|
* @details
|
||||||
|
* Предоставляет drag-and-drop и выбор файлов через диалог.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Qt
|
||||||
|
#include <QWidget>
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Виджет для приёма файлов через drag-and-drop.
|
||||||
|
*/
|
||||||
|
class DropFileWidget : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Создаёт виджет.
|
||||||
|
*/
|
||||||
|
explicit DropFileWidget(QWidget *parent = nullptr);
|
||||||
|
signals:
|
||||||
|
/**
|
||||||
|
* @brief Сигнал о том, что файлы выбраны или перетащены.
|
||||||
|
*/
|
||||||
|
void filesDropped(const QStringList &paths);
|
||||||
|
protected:
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает начало перетаскивания.
|
||||||
|
*/
|
||||||
|
void dragEnterEvent(QDragEnterEvent *e) override;
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает перемещение в области виджета.
|
||||||
|
*/
|
||||||
|
void dragMoveEvent(QDragMoveEvent *e) override;
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает сброс файлов.
|
||||||
|
*/
|
||||||
|
void dropEvent(QDropEvent *e) override;
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает клик для открытия диалога выбора.
|
||||||
|
*/
|
||||||
|
void mousePressEvent(QMouseEvent *e) override;
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief Открывает диалог выбора файлов.
|
||||||
|
*/
|
||||||
|
void selectFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //LICENSEMANAGER_DROPFILEWIDGET_H
|
||||||
281
src/EditClientDialog/EditClientDialog.cpp
Normal file
281
src/EditClientDialog/EditClientDialog.cpp
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
#include "EditClientDialog.h"
|
||||||
|
|
||||||
|
// Qt
|
||||||
|
#include <QCalendarWidget>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QTabWidget>
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
// Self
|
||||||
|
#include "../def.h"
|
||||||
|
#include "DropFileWidget.h"
|
||||||
|
#include "LicenseModel/LicenseModel.h"
|
||||||
|
|
||||||
|
#define WINDOW_SIZE "WindowSize"
|
||||||
|
|
||||||
|
EditClientDialog::EditClientDialog(LicenseModel* model, QWidget *parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
, m_model(model)
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("Edit Client"));
|
||||||
|
setModal(true);
|
||||||
|
setAcceptDrops(true);
|
||||||
|
|
||||||
|
// Ui
|
||||||
|
{
|
||||||
|
auto vLayout = new QVBoxLayout;
|
||||||
|
setLayout(vLayout);
|
||||||
|
m_tabWidget = new QTabWidget(this);
|
||||||
|
vLayout->addWidget(m_tabWidget);
|
||||||
|
|
||||||
|
// Auto filled widget
|
||||||
|
{
|
||||||
|
auto autoFilledWidget = new QWidget(m_tabWidget);
|
||||||
|
auto layout = new QVBoxLayout;
|
||||||
|
autoFilledWidget->setLayout(layout);
|
||||||
|
|
||||||
|
auto dropWidget = new DropFileWidget(autoFilledWidget);
|
||||||
|
layout->addWidget(dropWidget);
|
||||||
|
layout->addStretch();
|
||||||
|
m_configPathLabel = new QLabel(tr("Drop file here"));
|
||||||
|
m_configPathLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(m_configPathLabel);
|
||||||
|
m_tabWidget->addTab(autoFilledWidget, tr("File autofilled"));
|
||||||
|
|
||||||
|
connect(dropWidget, &DropFileWidget::filesDropped, this, &EditClientDialog::onFilesChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual user add widget
|
||||||
|
{
|
||||||
|
m_manualWidget = new QWidget(m_tabWidget);
|
||||||
|
auto gridLayout = new QGridLayout();
|
||||||
|
m_manualWidget->setLayout(gridLayout);
|
||||||
|
|
||||||
|
gridLayout->setColumnStretch(0, 0);
|
||||||
|
gridLayout->setColumnStretch(1, 1);
|
||||||
|
|
||||||
|
gridLayout->addWidget(makeLabel(tr("First name")), 0, 0);
|
||||||
|
m_firstNameLineEdit = new QLineEdit(m_manualWidget);
|
||||||
|
gridLayout->addWidget(m_firstNameLineEdit, 0, 1);
|
||||||
|
gridLayout->addWidget(makeLabel(tr("Last name")), 1, 0);
|
||||||
|
m_lastNameLineEdit = new QLineEdit(m_manualWidget);
|
||||||
|
gridLayout->addWidget(m_lastNameLineEdit, 1, 1);
|
||||||
|
gridLayout->addWidget(makeLabel(tr("Patronymic")), 2, 0);
|
||||||
|
m_patronymicLineEdit = new QLineEdit(m_manualWidget);
|
||||||
|
gridLayout->addWidget(m_patronymicLineEdit, 2, 1);
|
||||||
|
|
||||||
|
gridLayout->addWidget(makeLabel(tr("Email")), 3, 0);
|
||||||
|
m_emailLineEdit = new QLineEdit(m_manualWidget);
|
||||||
|
m_emailLineEdit->setPlaceholderText("examplebox@email.com");
|
||||||
|
gridLayout->addWidget(m_emailLineEdit, 3, 1);
|
||||||
|
|
||||||
|
gridLayout->addWidget(makeLabel(tr("Phone")), 4, 0);
|
||||||
|
m_phoneLineEdit = new QLineEdit(m_manualWidget);
|
||||||
|
m_phoneLineEdit->setPlaceholderText("+7(777)777-77-77");
|
||||||
|
gridLayout->addWidget(m_phoneLineEdit, 4, 1);
|
||||||
|
|
||||||
|
gridLayout->addWidget(makeLabel(tr("City")), 5, 0);
|
||||||
|
m_cityLineEdit = new QLineEdit(m_manualWidget);
|
||||||
|
gridLayout->addWidget(m_cityLineEdit, 5, 1);
|
||||||
|
|
||||||
|
gridLayout->addWidget(makeLabel(tr("You company name")), 6, 0);
|
||||||
|
m_yourCompanyNameTextEdit = new QLineEdit(m_manualWidget);
|
||||||
|
gridLayout->addWidget(m_yourCompanyNameTextEdit, 6, 1);
|
||||||
|
|
||||||
|
gridLayout->addWidget(makeLabel(tr("Hardware hash")), 9, 0);
|
||||||
|
m_hardwareHashLineEdit = new QLineEdit(m_manualWidget);
|
||||||
|
gridLayout->addWidget(m_hardwareHashLineEdit, 9, 1);
|
||||||
|
|
||||||
|
gridLayout->addWidget(makeLabel(tr("Comment")), 10, 0);
|
||||||
|
m_commentTextEdit = new QTextEdit(m_manualWidget);
|
||||||
|
gridLayout->addWidget(m_commentTextEdit, 11, 0, 1, 2);
|
||||||
|
m_commentTextEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||||
|
gridLayout->setRowStretch(11, 1);
|
||||||
|
|
||||||
|
m_tabWidget->addTab(m_manualWidget, tr("Main info"));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto layout = new QVBoxLayout();
|
||||||
|
auto licenseTimeWidget = new QWidget(this);
|
||||||
|
licenseTimeWidget->setLayout(layout);
|
||||||
|
|
||||||
|
m_useTemporaryLicenseCheckBox = new QCheckBox(tr("License is temporally"), licenseTimeWidget);
|
||||||
|
layout->addWidget(m_useTemporaryLicenseCheckBox);
|
||||||
|
|
||||||
|
m_calendarWidget = new QCalendarWidget(licenseTimeWidget);
|
||||||
|
layout->addWidget(m_calendarWidget);
|
||||||
|
m_calendarWidget->setEnabled(false);
|
||||||
|
m_calendarWidget->setGridVisible(true);
|
||||||
|
m_calendarWidget->showToday();
|
||||||
|
|
||||||
|
connect(m_useTemporaryLicenseCheckBox, &QCheckBox::stateChanged, [&](int state) {
|
||||||
|
m_calendarWidget->setEnabled(state == Qt::Checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_tabWidget->addTab(licenseTimeWidget, tr("License time"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto hLayout = new QHBoxLayout;
|
||||||
|
hLayout->setSpacing(vLayout->spacing());
|
||||||
|
hLayout->addStretch();
|
||||||
|
m_saveButton = new QPushButton(tr("Save"), this);
|
||||||
|
hLayout->addWidget(m_saveButton);
|
||||||
|
auto cancelButton = new QPushButton(tr("Cancel"), this);
|
||||||
|
hLayout->addWidget(cancelButton);
|
||||||
|
vLayout->addItem(hLayout);
|
||||||
|
|
||||||
|
connect(m_saveButton, &QPushButton::clicked, this, &QDialog::accept);
|
||||||
|
connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditClientDialog::~EditClientDialog()
|
||||||
|
{
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditClientDialog::loadSettings()
|
||||||
|
{
|
||||||
|
QSettings settings(ORGANIZATION_NAME, metaObject()->className());
|
||||||
|
if (settings.contains(WINDOW_SIZE))
|
||||||
|
resize(settings.value(WINDOW_SIZE).toSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditClientDialog::saveSettings()
|
||||||
|
{
|
||||||
|
QSettings settings(ORGANIZATION_NAME, metaObject()->className());
|
||||||
|
settings.setValue(WINDOW_SIZE, size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditClientDialog::onFilesChanged(const QStringList &paths)
|
||||||
|
{
|
||||||
|
if (paths.isEmpty())
|
||||||
|
return;
|
||||||
|
QFile file(paths.at(0));
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_filesPath = paths.at(0);
|
||||||
|
m_configPathLabel->setText(paths.at(0).split("/").last());
|
||||||
|
|
||||||
|
auto configBody = QJsonDocument::fromJson(file.readAll()).object();
|
||||||
|
if (configBody.contains("lastName"))
|
||||||
|
m_lastNameLineEdit->setText(configBody.value("lastName").toString());
|
||||||
|
if (configBody.contains("firstName"))
|
||||||
|
m_firstNameLineEdit->setText(configBody.value("firstName").toString());
|
||||||
|
if (configBody.contains("patronymic"))
|
||||||
|
m_patronymicLineEdit->setText(configBody.value("patronymic").toString());
|
||||||
|
if (configBody.contains("email"))
|
||||||
|
m_emailLineEdit->setText(configBody.value("email").toString());
|
||||||
|
if (configBody.contains("phone"))
|
||||||
|
m_phoneLineEdit->setText(configBody.value("phone").toString());
|
||||||
|
if (configBody.contains("city"))
|
||||||
|
m_cityLineEdit->setText(configBody.value("city").toString());
|
||||||
|
if (configBody.contains("yourCompany"))
|
||||||
|
m_yourCompanyNameTextEdit->setText(configBody.value("yourCompany").toString());
|
||||||
|
if (configBody.contains("hardwareHash"))
|
||||||
|
m_hardwareHashLineEdit->setText(configBody.value("hardwareHash").toString());
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel* EditClientDialog::makeLabel(const QString &text)
|
||||||
|
{
|
||||||
|
auto *label = new QLabel(text, m_manualWidget);
|
||||||
|
label->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
|
void EditClientDialog::setType(Type type)
|
||||||
|
{
|
||||||
|
m_type = type;
|
||||||
|
|
||||||
|
m_hardwareHashLineEdit->setEnabled(m_type == Type::Add);
|
||||||
|
switch (m_type)
|
||||||
|
{
|
||||||
|
case Type::Add:
|
||||||
|
m_tabWidget->setCurrentIndex(0);
|
||||||
|
break;
|
||||||
|
case Type::Edit:
|
||||||
|
m_tabWidget->setCurrentIndex(1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditClientDialog::Type EditClientDialog::getType() const
|
||||||
|
{
|
||||||
|
return m_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
LicenseModel::LicenseItem EditClientDialog::getLicenseItem() const
|
||||||
|
{
|
||||||
|
LicenseModel::LicenseItem item;
|
||||||
|
item.lastName = m_lastNameLineEdit->text().trimmed();
|
||||||
|
item.firstName = m_firstNameLineEdit->text().trimmed();
|
||||||
|
item.patronymic = m_patronymicLineEdit->text().trimmed();
|
||||||
|
item.email = m_emailLineEdit->text().trimmed();
|
||||||
|
item.phone = m_phoneLineEdit->text().trimmed();
|
||||||
|
item.city = m_cityLineEdit->text().trimmed();
|
||||||
|
item.yourCompany = m_yourCompanyNameTextEdit->text().trimmed();
|
||||||
|
item.hardwareHash = m_hardwareHashLineEdit->text().trimmed();
|
||||||
|
if (m_useTemporaryLicenseCheckBox->isChecked())
|
||||||
|
item.licenseTime = m_calendarWidget->selectedDate().toString();
|
||||||
|
item.comment = m_commentTextEdit->toPlainText().trimmed();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditClientDialog::clear()
|
||||||
|
{
|
||||||
|
m_firstNameLineEdit->clear();
|
||||||
|
m_lastNameLineEdit->clear();
|
||||||
|
m_patronymicLineEdit->clear();
|
||||||
|
m_emailLineEdit->clear();
|
||||||
|
m_phoneLineEdit->clear();
|
||||||
|
m_cityLineEdit->clear();
|
||||||
|
m_yourCompanyNameTextEdit->clear();
|
||||||
|
m_hardwareHashLineEdit->clear();
|
||||||
|
m_commentTextEdit->clear();
|
||||||
|
m_useTemporaryLicenseCheckBox->setChecked(false);
|
||||||
|
m_calendarWidget->showToday();
|
||||||
|
m_configPathLabel->setText(tr("Drop file here"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditClientDialog::setClientInfo(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_model->rowCount())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto item = m_model->getItem(index);
|
||||||
|
m_lastNameLineEdit->setText(item.lastName);
|
||||||
|
m_firstNameLineEdit->setText(item.firstName);
|
||||||
|
m_patronymicLineEdit->setText(item.patronymic);
|
||||||
|
m_emailLineEdit->setText(item.email);
|
||||||
|
m_phoneLineEdit->setText(item.phone);
|
||||||
|
m_cityLineEdit->setText(item.city);
|
||||||
|
m_yourCompanyNameTextEdit->setText(item.yourCompany);
|
||||||
|
m_hardwareHashLineEdit->setText(item.hardwareHash);
|
||||||
|
if (item.licenseTime.isEmpty())
|
||||||
|
{
|
||||||
|
m_useTemporaryLicenseCheckBox->setChecked(false);
|
||||||
|
m_calendarWidget->showToday();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_useTemporaryLicenseCheckBox->setChecked(true);
|
||||||
|
m_calendarWidget->setSelectedDate(QDate::fromString(item.licenseTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_configPathLabel->setText(tr("Drop file here"));
|
||||||
|
}
|
||||||
117
src/EditClientDialog/EditClientDialog.h
Normal file
117
src/EditClientDialog/EditClientDialog.h
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#ifndef LICENSEMANAGER_EDITCLIENTDIALOG_H
|
||||||
|
#define LICENSEMANAGER_EDITCLIENTDIALOG_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file EditClientDialog.h
|
||||||
|
* @brief Диалог добавления/редактирования клиента.
|
||||||
|
*
|
||||||
|
* @details
|
||||||
|
* Позволяет заполнять данные вручную или автозаполнением из файла,
|
||||||
|
* а также задавать срок действия лицензии.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Qt
|
||||||
|
#include <QDate>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDragEnterEvent>
|
||||||
|
#include <QLabel>
|
||||||
|
|
||||||
|
#include "LicenseModel/LicenseModel.h"
|
||||||
|
class QCalendarWidget;
|
||||||
|
class QCheckBox;
|
||||||
|
class QLineEdit;
|
||||||
|
class QTabWidget;
|
||||||
|
class QTextEdit;
|
||||||
|
|
||||||
|
// Self
|
||||||
|
class LicenseModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Диалог редактирования данных клиента.
|
||||||
|
*/
|
||||||
|
class EditClientDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Режим работы диалога.
|
||||||
|
*/
|
||||||
|
enum class Type
|
||||||
|
{
|
||||||
|
None = 0, //!< Режим не выбран.
|
||||||
|
Add, //!< Добавление нового клиента.
|
||||||
|
Edit, //!< Редактирование существующего клиента.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Создаёт диалог для заданной модели.
|
||||||
|
*/
|
||||||
|
EditClientDialog(LicenseModel* model, QWidget *parent = nullptr);
|
||||||
|
/**
|
||||||
|
* @brief Деструктор.
|
||||||
|
*/
|
||||||
|
~EditClientDialog();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Устанавливает режим диалога.
|
||||||
|
*/
|
||||||
|
void setType(Type type);
|
||||||
|
/**
|
||||||
|
* @brief Возвращает текущий режим диалога.
|
||||||
|
*/
|
||||||
|
Type getType() const;
|
||||||
|
/**
|
||||||
|
* @brief Формирует структуру клиента из полей формы.
|
||||||
|
*/
|
||||||
|
LicenseModel::LicenseItem getLicenseItem() const;
|
||||||
|
/**
|
||||||
|
* @brief Очищает поля ввода.
|
||||||
|
*/
|
||||||
|
void clear();
|
||||||
|
/**
|
||||||
|
* @brief Заполняет поля данными клиента по индексу строки.
|
||||||
|
*/
|
||||||
|
void setClientInfo(int index);
|
||||||
|
private slots:
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает выбор/перетаскивание файла с конфигурацией.
|
||||||
|
*/
|
||||||
|
void onFilesChanged(const QStringList &paths);
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief Загружает настройки окна (размер).
|
||||||
|
*/
|
||||||
|
void loadSettings();
|
||||||
|
/**
|
||||||
|
* @brief Сохраняет настройки окна (размер).
|
||||||
|
*/
|
||||||
|
void saveSettings();
|
||||||
|
/**
|
||||||
|
* @brief Создаёт стандартную подпись для поля ввода.
|
||||||
|
*/
|
||||||
|
QLabel* makeLabel(const QString &text);
|
||||||
|
private:
|
||||||
|
LicenseModel* m_model{nullptr}; //!< Модель данных клиентов.
|
||||||
|
QTabWidget* m_tabWidget{nullptr}; //!< Набор вкладок диалога.
|
||||||
|
QLabel* m_configPathLabel{nullptr}; //!< Отображение пути к файлу.
|
||||||
|
QWidget* m_manualWidget{nullptr}; //!< Вкладка ручного ввода.
|
||||||
|
QLineEdit* m_firstNameLineEdit{nullptr}; //!< Поле ввода имени пользователя.
|
||||||
|
QLineEdit* m_lastNameLineEdit{nullptr}; //!< Поле ввода фамилии пользователя.
|
||||||
|
QLineEdit* m_patronymicLineEdit{nullptr}; //!< Поле ввода отчества пользователя.
|
||||||
|
QLineEdit* m_emailLineEdit{nullptr}; //!< Поле ввода электронной почты.
|
||||||
|
QLineEdit* m_phoneLineEdit{nullptr}; //!< Поле ввода номера телефона.
|
||||||
|
QLineEdit* m_yourCompanyNameTextEdit{nullptr}; //!< Поле ввода названия компании пользователя.
|
||||||
|
QLineEdit* m_sellerNameTextEdit{nullptr}; //!< Поле ввода названия продавца (если используется).
|
||||||
|
QLineEdit* m_cityLineEdit{nullptr}; //!< Поле ввода города.
|
||||||
|
QLineEdit* m_hardwareHashLineEdit{nullptr}; //!< Поле для отображения хеша оборудования.
|
||||||
|
QCheckBox* m_useTemporaryLicenseCheckBox{nullptr}; //!< Признак временной лицензии.
|
||||||
|
QCalendarWidget* m_calendarWidget{nullptr}; //!< Календарь для выбора даты.
|
||||||
|
QTextEdit* m_commentTextEdit{nullptr}; //!< Поле ввода комментария.
|
||||||
|
QPushButton* m_saveButton{nullptr}; //!< Кнопка сохранения.
|
||||||
|
QString m_filesPath; //!< Последний выбранный файл.
|
||||||
|
Type m_type = Type::None; //!< Текущий режим диалога.
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //LICENSEMANAGER_EDITCLIENTDIALOG_H
|
||||||
36
src/LicenseGenerator/Util.cpp
Normal file
36
src/LicenseGenerator/Util.cpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#include "Util.h"
|
||||||
|
|
||||||
|
#include <QCborValue>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonValue>
|
||||||
|
|
||||||
|
QByteArray fileReadAll(const QString& path)
|
||||||
|
{
|
||||||
|
QFile f(path);
|
||||||
|
QByteArray buf;
|
||||||
|
if (f.open(QIODevice::ReadOnly))
|
||||||
|
buf = f.readAll();
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray toCbor(const QJsonObject &jsonObject)
|
||||||
|
{
|
||||||
|
QCborValue cborValue = QCborValue::fromJsonValue(QJsonValue(jsonObject));
|
||||||
|
return cborValue.toCbor();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray toCbor(const QJsonArray &jsonArray)
|
||||||
|
{
|
||||||
|
QCborValue cborValue = QCborValue::fromJsonValue(QJsonValue(jsonArray));
|
||||||
|
return cborValue.toCbor();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray toCbor(const QJsonDocument &doc)
|
||||||
|
{
|
||||||
|
if (doc.isObject()) {
|
||||||
|
return toCbor(doc.object());
|
||||||
|
} else if (doc.isArray()) {
|
||||||
|
return toCbor(doc.array());
|
||||||
|
}
|
||||||
|
return {}; // Return an empty QByteArray if the document is invalid
|
||||||
|
}
|
||||||
29
src/LicenseGenerator/Util.h
Normal file
29
src/LicenseGenerator/Util.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
/**
|
||||||
|
* @file Util.h
|
||||||
|
* @brief Базовые вспомогательные функции проекта.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Считывает файл целиком.
|
||||||
|
*/
|
||||||
|
QByteArray fileReadAll(const QString& path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Конвертирует QJsonObject в CBOR.
|
||||||
|
*/
|
||||||
|
QByteArray toCbor(const QJsonObject &jsonObject);
|
||||||
|
/**
|
||||||
|
* @brief Конвертирует QJsonArray в CBOR.
|
||||||
|
*/
|
||||||
|
QByteArray toCbor(const QJsonArray &jsonArray);
|
||||||
|
/**
|
||||||
|
* @brief Конвертирует QJsonDocument в CBOR.
|
||||||
|
*/
|
||||||
|
QByteArray toCbor(const QJsonDocument &doc);
|
||||||
95
src/LicenseGenerator/crypt.cpp
Normal file
95
src/LicenseGenerator/crypt.cpp
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#include "crypt.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <openssl/pem.h>
|
||||||
|
#include <openssl/bio.h>
|
||||||
|
#include <openssl/rsa.h>
|
||||||
|
#include <openssl/err.h>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
QByteArray Crypt::rsaEncrypt(const QByteArray &data, const QByteArray &privateKey)
|
||||||
|
{
|
||||||
|
EVP_PKEY *pkey = loadKeyFromDER(privateKey, false);
|
||||||
|
if (!pkey) {
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
|
||||||
|
if (!ctx) {
|
||||||
|
qDebug() << "Failed to create EVP_PKEY_CTX for encryption.";
|
||||||
|
EVP_PKEY_free(pkey);
|
||||||
|
ERR_print_errors_fp(stderr);
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
const int keySize =
|
||||||
|
#if OPENSSL_VERSION_NUMBER < 0x30000000L
|
||||||
|
EVP_PKEY_size(pkey);
|
||||||
|
#else
|
||||||
|
EVP_PKEY_get_size(pkey);
|
||||||
|
#endif
|
||||||
|
const int chunkSize = keySize - RSA_PKCS1_PADDING_SIZE;
|
||||||
|
EVP_PKEY_free(pkey);
|
||||||
|
if (chunkSize <= 0) {
|
||||||
|
qDebug() << "Invalid RSA key size for PKCS1 padding.";
|
||||||
|
EVP_PKEY_CTX_free(ctx);
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EVP_PKEY_sign_init(ctx) <= 0) {
|
||||||
|
qDebug() << "Failed to initialize signing context.";
|
||||||
|
ERR_print_errors_fp(stderr);
|
||||||
|
EVP_PKEY_CTX_free(ctx);
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) {
|
||||||
|
qDebug() << "Failed to set RSA padding.";
|
||||||
|
ERR_print_errors_fp(stderr);
|
||||||
|
EVP_PKEY_CTX_free(ctx);
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray ret;
|
||||||
|
for (int i = 0; i < ceil(data.size() * 1. / chunkSize); ++i) {
|
||||||
|
int offset = i * chunkSize;
|
||||||
|
QByteArray chunk = data.mid(offset, chunkSize);
|
||||||
|
size_t out_len = 0;
|
||||||
|
if (EVP_PKEY_sign(ctx, nullptr, &out_len, reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()) <= 0) {
|
||||||
|
qDebug() << "Failed to determine signature size.";
|
||||||
|
ERR_print_errors_fp(stderr);
|
||||||
|
EVP_PKEY_CTX_free(ctx);
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
QByteArray encryptedChunk(out_len, 0);
|
||||||
|
if (EVP_PKEY_sign(ctx, reinterpret_cast<unsigned char *>(encryptedChunk.data()), &out_len, reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()) <= 0) {
|
||||||
|
qDebug() << "Signing (encryption) failed.\n";
|
||||||
|
ERR_print_errors_fp(stderr);
|
||||||
|
EVP_PKEY_CTX_free(ctx);
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
encryptedChunk.resize(out_len);
|
||||||
|
ret.append(encryptedChunk);
|
||||||
|
}
|
||||||
|
EVP_PKEY_CTX_free(ctx);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
EVP_PKEY *Crypt::loadKeyFromDER(const QByteArray &derKey, bool isPublicKey) {
|
||||||
|
// Create a memory BIO from the QByteArray
|
||||||
|
BIO *bio = BIO_new_mem_buf(derKey.constData(), derKey.size());
|
||||||
|
if (!bio) {
|
||||||
|
qWarning("Failed to create BIO");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the key based on whether it's a public or private key
|
||||||
|
EVP_PKEY *key = isPublicKey ? d2i_PUBKEY_bio(bio, nullptr) : d2i_PrivateKey_bio(bio, nullptr);
|
||||||
|
BIO_free(bio); // Free the BIO after loading
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
qDebug() << "Failed to load key. SSL error:";
|
||||||
|
ERR_print_errors_fp(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
27
src/LicenseGenerator/crypt.h
Normal file
27
src/LicenseGenerator/crypt.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#ifndef CRYPT_H
|
||||||
|
#define CRYPT_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file crypt.h
|
||||||
|
* @brief Криптографические операции (RSA) для генерации лицензий.
|
||||||
|
*
|
||||||
|
* @details
|
||||||
|
* Оставлены только операции, используемые в проекте.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
|
class Crypt
|
||||||
|
{
|
||||||
|
static EVP_PKEY *loadKeyFromDER(const QByteArray &derPrivateKey, bool isPublicKey);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief RSA-шифрование приватным ключом (sign).
|
||||||
|
*/
|
||||||
|
static QByteArray rsaEncrypt(const QByteArray &data, const QByteArray &privateKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CRYPT_H
|
||||||
@@ -1,70 +1,70 @@
|
|||||||
#include "LicenseModel.h"
|
#include "LicenseModel.h"
|
||||||
|
|
||||||
// Qt
|
// Qt
|
||||||
|
#include <QtConcurrent/QtConcurrent>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <algorithm>
|
||||||
#include <QtSql/QSqlDatabase>
|
#include <QtSql/QSqlDatabase>
|
||||||
#include <QtSql/QSqlQuery>
|
#include <QtSql/QSqlQuery>
|
||||||
#include <QtSql/QSqlError>
|
#include <QtSql/QSqlError>
|
||||||
#include <QDebug>
|
#include <QtSql/QSqlRecord>
|
||||||
|
|
||||||
// Self
|
// Self
|
||||||
#include <qfile.h>
|
|
||||||
|
|
||||||
#include "../def.h"
|
#include "../def.h"
|
||||||
|
|
||||||
const static int COLUMN_COUNT = 9;
|
const static int COLUMN_COUNT = 9;
|
||||||
|
|
||||||
LicenseModel::LicenseModel(QObject* parent)
|
LicenseModel::LicenseModel(QObject* parent)
|
||||||
: QAbstractTableModel(parent)
|
: QAbstractItemModel(parent)
|
||||||
{
|
{
|
||||||
m_db = QSqlDatabase::addDatabase("QSQLITE");
|
checkTables();
|
||||||
m_db.setDatabaseName(DB_PATH);
|
|
||||||
|
|
||||||
if (!m_db.open())
|
|
||||||
{
|
|
||||||
m_status = Status::DbExistError;
|
|
||||||
m_errors.append("Database connection failed: " + m_db.lastError().text());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!checkTables())
|
|
||||||
{
|
|
||||||
m_status = Status::DbStructError;
|
|
||||||
m_errors.append("Database tables are not valid");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_status = Status::Ok;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LicenseModel::~LicenseModel()
|
|
||||||
{
|
|
||||||
m_db.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int LicenseModel::rowCount(const QModelIndex &parent) const
|
int LicenseModel::rowCount(const QModelIndex &parent) const
|
||||||
{
|
{
|
||||||
return 0;
|
return m_data.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
int LicenseModel::columnCount(const QModelIndex &parent) const
|
int LicenseModel::columnCount(const QModelIndex &parent) const
|
||||||
{
|
{
|
||||||
return COLUMN_COUNT;
|
return COLUMN_COUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant LicenseModel::data(const QModelIndex &index, int role) const
|
QVariant LicenseModel::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
switch (role)
|
if (!index.isValid() || index.row() < 0 || index.row() >= m_data.size())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (role == Qt::ToolTipRole && index.column() == 0)
|
||||||
|
return m_data[index.row()].id;
|
||||||
|
|
||||||
|
if (role != Qt::DisplayRole)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
switch (index.column())
|
||||||
{
|
{
|
||||||
case Qt::DisplayRole:
|
case 0:
|
||||||
break;
|
return m_data[index.row()].lastName ;
|
||||||
case Qt::ToolTipRole:
|
case 1:
|
||||||
return {}; // TODO: return client id
|
return m_data[index.row()].firstName ;
|
||||||
|
case 2:
|
||||||
|
return m_data[index.row()].patronymic ;
|
||||||
|
case 3:
|
||||||
|
return m_data[index.row()].email ;
|
||||||
|
case 4:
|
||||||
|
return m_data[index.row()].phone ;
|
||||||
|
case 5:
|
||||||
|
return m_data[index.row()].yourCompany ;
|
||||||
|
case 6:
|
||||||
|
return m_data[index.row()].city ;
|
||||||
|
case 7:
|
||||||
|
return m_data[index.row()].licenseTime.isEmpty() ? tr("Permanent") : m_data[index.row()].licenseTime ;
|
||||||
|
case 8:
|
||||||
|
return m_data[index.row()].comment ;
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant LicenseModel::headerData(int section, Qt::Orientation orientation, int role) const
|
QVariant LicenseModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||||
@@ -92,7 +92,7 @@ QVariant LicenseModel::headerData(int section, Qt::Orientation orientation, int
|
|||||||
case 6:
|
case 6:
|
||||||
return tr("City");
|
return tr("City");
|
||||||
case 7:
|
case 7:
|
||||||
return tr("Created date");
|
return tr("License time");
|
||||||
case 8:
|
case 8:
|
||||||
return tr("Comment");
|
return tr("Comment");
|
||||||
default:
|
default:
|
||||||
@@ -100,6 +100,23 @@ QVariant LicenseModel::headerData(int section, Qt::Orientation orientation, int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QModelIndex LicenseModel::parent(const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(index)
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex LicenseModel::index(int row, int column, const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= m_data.size() || column < 0 || column >= COLUMN_COUNT)
|
||||||
|
return QModelIndex();
|
||||||
|
|
||||||
|
if (parent.isValid())
|
||||||
|
return QModelIndex();
|
||||||
|
|
||||||
|
return createIndex(row, column, reinterpret_cast<quintptr>(&(m_data[row])));
|
||||||
|
}
|
||||||
|
|
||||||
LicenseModel::Status LicenseModel::getStatus()
|
LicenseModel::Status LicenseModel::getStatus()
|
||||||
{
|
{
|
||||||
return m_status;
|
return m_status;
|
||||||
@@ -112,18 +129,52 @@ QString LicenseModel::getStatusText()
|
|||||||
|
|
||||||
bool LicenseModel::checkTables()
|
bool LicenseModel::checkTables()
|
||||||
{
|
{
|
||||||
bool clienttableExist = false;
|
auto db = QSqlDatabase::addDatabase("QSQLITE", "checkTablesConnection");
|
||||||
for (const auto &table : m_db.tables())
|
db.setDatabaseName(DB_PATH);
|
||||||
|
|
||||||
|
if (!db.open())
|
||||||
|
{
|
||||||
|
m_status = Status::DbExistError;
|
||||||
|
m_errors.append("Database connection failed: " + db.lastError().text());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool clientTableExist = false;
|
||||||
|
for (const auto &table : db.tables())
|
||||||
{
|
{
|
||||||
if (table == "clients")
|
if (table == "clients")
|
||||||
|
{
|
||||||
|
m_status = Status::Ok;
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!clienttableExist)
|
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
if (!clientTableExist)
|
||||||
|
{
|
||||||
|
m_status = Status::DbStructError;
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_status = Status::Ok;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LicenseModel::prepareDatabase()
|
bool LicenseModel::prepareDatabase()
|
||||||
{
|
{
|
||||||
|
auto db = QSqlDatabase::addDatabase("QSQLITE", "prepareDatabaseConnection");
|
||||||
|
db.setDatabaseName(DB_PATH);
|
||||||
|
|
||||||
|
if (!db.open())
|
||||||
|
{
|
||||||
|
m_status = Status::DbExistError;
|
||||||
|
m_errors << tr("Database not exist");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
QFile tablesFile(QStringLiteral(":/deps/tables.ddl"));
|
QFile tablesFile(QStringLiteral(":/deps/tables.ddl"));
|
||||||
if (!tablesFile.open(QIODevice::ReadOnly | QIODevice::Text))
|
if (!tablesFile.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
return false;
|
return false;
|
||||||
@@ -135,15 +186,452 @@ bool LicenseModel::prepareDatabase()
|
|||||||
{
|
{
|
||||||
if (item.trimmed().isEmpty())
|
if (item.trimmed().isEmpty())
|
||||||
continue;
|
continue;
|
||||||
QSqlQuery query(item.trimmed() + ";", m_db);
|
QSqlQuery query(db);
|
||||||
if (!query.exec())
|
if (!query.exec(item.trimmed()))
|
||||||
{
|
{
|
||||||
m_status = Status::DbStructError;
|
m_status = Status::DbStructError;
|
||||||
qDebug() << item.trimmed() + ";" << query.lastError().text();
|
|
||||||
m_errors.append(query.lastError().text());
|
m_errors.append(query.lastError().text());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
m_status = Status::Ok;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LicenseModel::addClient(const LicenseItem &item)
|
||||||
|
{
|
||||||
|
m_status = Status::Working;
|
||||||
|
emit statusChanged();
|
||||||
|
|
||||||
|
auto* watcher = new QFutureWatcher<Result>(this);
|
||||||
|
connect(watcher, &QFutureWatcher<Result>::finished, this, [this, watcher]() {
|
||||||
|
const Result result = watcher->result();
|
||||||
|
if (result.status != Status::Ok)
|
||||||
|
{
|
||||||
|
m_status = result.status;
|
||||||
|
if (!result.error.isEmpty())
|
||||||
|
m_errors.append(result.error);
|
||||||
|
emit statusChanged();
|
||||||
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data.isEmpty())
|
||||||
|
{
|
||||||
|
m_status = Status::DbStructError;
|
||||||
|
m_errors.append("Inserted client not found");
|
||||||
|
emit statusChanged();
|
||||||
|
qDebug() << m_errors;
|
||||||
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int row = 0;
|
||||||
|
beginInsertRows({}, row, row);
|
||||||
|
m_data.insert(row, result.data.first());
|
||||||
|
endInsertRows();
|
||||||
|
|
||||||
|
m_status = Status::Ok;
|
||||||
|
emit statusChanged();
|
||||||
|
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher->setFuture(QtConcurrent::run([item]() {
|
||||||
|
Result result;
|
||||||
|
const QString connectionName = QString("license_add_%1").arg(QUuid::createUuid().toString(QUuid::Id128));
|
||||||
|
{
|
||||||
|
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
|
||||||
|
db.setDatabaseName(DB_PATH);
|
||||||
|
if (!db.open())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbExistError;
|
||||||
|
result.error = "Database connection failed: " + db.lastError().text();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QSqlQuery queryInsert(db);
|
||||||
|
queryInsert.prepare(QString(
|
||||||
|
"INSERT INTO clients(lastName, firstName, patronymic, phone, email, city, yourCompany, hardwareHash, licenseTime, comment) "
|
||||||
|
"VALUES (:lastName, :firstName, :patronymic, :phone, :email, :city, :yourCompany, :hardwareHash, :licenseTime, :comment);"
|
||||||
|
));
|
||||||
|
queryInsert.bindValue(":lastName", item.lastName);
|
||||||
|
queryInsert.bindValue(":firstName", item.firstName);
|
||||||
|
queryInsert.bindValue(":patronymic", item.patronymic);
|
||||||
|
queryInsert.bindValue(":phone", item.phone);
|
||||||
|
queryInsert.bindValue(":email", item.email);
|
||||||
|
queryInsert.bindValue(":city", item.city);
|
||||||
|
queryInsert.bindValue(":yourCompany", item.yourCompany);
|
||||||
|
queryInsert.bindValue(":hardwareHash", item.hardwareHash);
|
||||||
|
queryInsert.bindValue(":licenseTime", item.licenseTime);
|
||||||
|
queryInsert.bindValue(":comment", item.comment);
|
||||||
|
|
||||||
|
if (!queryInsert.exec())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbStructError;
|
||||||
|
result.error = queryInsert.lastError().text();
|
||||||
|
qDebug() << result.error;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const QString itemId = QString::number(queryInsert.lastInsertId().toLongLong());
|
||||||
|
|
||||||
|
QSqlQuery querySelect(db);
|
||||||
|
querySelect.prepare(QString("SELECT * FROM clients WHERE id=:id;"));
|
||||||
|
querySelect.bindValue(":id", itemId);
|
||||||
|
|
||||||
|
if (!querySelect.exec())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbStructError;
|
||||||
|
result.error = querySelect.lastError().text();
|
||||||
|
}
|
||||||
|
else if (!querySelect.next())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbStructError;
|
||||||
|
result.error = "Inserted client not found: " + itemId;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LicenseItem output;
|
||||||
|
output.id = itemId;
|
||||||
|
output.lastName = querySelect.value("lastName").toString();
|
||||||
|
output.firstName = querySelect.value("firstName").toString();
|
||||||
|
output.patronymic = querySelect.value("patronymic").toString();
|
||||||
|
output.phone = querySelect.value("phone").toString();
|
||||||
|
output.email = querySelect.value("email").toString();
|
||||||
|
output.city = querySelect.value("city").toString();
|
||||||
|
output.yourCompany = querySelect.value("yourCompany").toString();
|
||||||
|
output.hardwareHash = querySelect.value("hardwareHash").toString();
|
||||||
|
output.licenseTime = querySelect.value("licenseTime").toString();
|
||||||
|
output.comment = querySelect.value("comment").toString();
|
||||||
|
result.data.append(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QSqlDatabase::removeDatabase(connectionName);
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LicenseModel::deleteClient(const QList<int> &rows)
|
||||||
|
{
|
||||||
|
m_status = Status::Working;
|
||||||
|
emit statusChanged();
|
||||||
|
|
||||||
|
QList<QString> ids;
|
||||||
|
ids.reserve(rows.size());
|
||||||
|
for (const auto row : rows)
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= m_data.size())
|
||||||
|
continue;
|
||||||
|
const QString id = m_data[row].id;
|
||||||
|
if (!id.isEmpty())
|
||||||
|
ids.append(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* watcher = new QFutureWatcher<Result>(this);
|
||||||
|
connect(watcher, &QFutureWatcher<Result>::finished, this, [this, watcher]() {
|
||||||
|
const Result result = watcher->result();
|
||||||
|
if (result.status != Status::Ok)
|
||||||
|
{
|
||||||
|
m_status = result.status;
|
||||||
|
if (!result.error.isEmpty())
|
||||||
|
m_errors.append(result.error);
|
||||||
|
emit statusChanged();
|
||||||
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<int> indices;
|
||||||
|
indices.reserve(result.ids.size());
|
||||||
|
for (const auto &id : result.ids)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_data.size(); ++i)
|
||||||
|
{
|
||||||
|
if (m_data[i].id == id)
|
||||||
|
{
|
||||||
|
indices.append(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(indices.begin(), indices.end(), std::greater<int>());
|
||||||
|
for (const auto row : indices)
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= m_data.size())
|
||||||
|
continue;
|
||||||
|
beginRemoveRows({}, row, row);
|
||||||
|
m_data.removeAt(row);
|
||||||
|
endRemoveRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_status = Status::Ok;
|
||||||
|
emit statusChanged();
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher->setFuture(QtConcurrent::run([ids]() {
|
||||||
|
Result result;
|
||||||
|
result.ids = ids;
|
||||||
|
if (ids.isEmpty())
|
||||||
|
return result;
|
||||||
|
|
||||||
|
const QString connectionName = QString("license_delete_%1")
|
||||||
|
.arg(QUuid::createUuid().toString(QUuid::Id128));
|
||||||
|
{
|
||||||
|
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
|
||||||
|
db.setDatabaseName(DB_PATH);
|
||||||
|
if (!db.open())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbExistError;
|
||||||
|
result.error = "Database connection failed: " + db.lastError().text();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QStringList placeholders;
|
||||||
|
placeholders.reserve(ids.size());
|
||||||
|
for (int i = 0; i < ids.size(); ++i)
|
||||||
|
placeholders << "?";
|
||||||
|
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(QString("DELETE FROM clients WHERE id IN (%1);")
|
||||||
|
.arg(placeholders.join(',')));
|
||||||
|
for (const auto &id : ids)
|
||||||
|
query.addBindValue(id);
|
||||||
|
|
||||||
|
if (!query.exec())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbStructError;
|
||||||
|
result.error = query.lastError().text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QSqlDatabase::removeDatabase(connectionName);
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LicenseModel::editClient(const LicenseItem &item, int index)
|
||||||
|
{
|
||||||
|
m_status = Status::Working;
|
||||||
|
emit statusChanged();
|
||||||
|
|
||||||
|
if (index < 0 || index >= m_data.size())
|
||||||
|
{
|
||||||
|
m_status = Status::Ok;
|
||||||
|
emit statusChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LicenseItem updateItem = item;
|
||||||
|
updateItem.id = m_data[index].id;
|
||||||
|
|
||||||
|
if (updateItem.id.isEmpty())
|
||||||
|
{
|
||||||
|
m_status = Status::DbStructError;
|
||||||
|
m_errors.append("Client id is empty");
|
||||||
|
emit statusChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* watcher = new QFutureWatcher<Result>(this);
|
||||||
|
connect(watcher, &QFutureWatcher<Result>::finished, this, [this, watcher, index]() {
|
||||||
|
const Result result = watcher->result();
|
||||||
|
if (result.status != Status::Ok)
|
||||||
|
{
|
||||||
|
m_status = result.status;
|
||||||
|
if (!result.error.isEmpty())
|
||||||
|
m_errors.append(result.error);
|
||||||
|
emit statusChanged();
|
||||||
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data.isEmpty())
|
||||||
|
{
|
||||||
|
m_status = Status::DbStructError;
|
||||||
|
m_errors.append("Updated client not found");
|
||||||
|
emit statusChanged();
|
||||||
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= m_data.size())
|
||||||
|
{
|
||||||
|
m_status = Status::Ok;
|
||||||
|
emit statusChanged();
|
||||||
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_data[index] = result.data.first();
|
||||||
|
dataChanged(this->index(index, 0), this->index(index, COLUMN_COUNT - 1));
|
||||||
|
|
||||||
|
m_status = Status::Ok;
|
||||||
|
emit statusChanged();
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher->setFuture(QtConcurrent::run([updateItem]() {
|
||||||
|
Result result;
|
||||||
|
const QString connectionName = QString("license_edit_%1").arg(QUuid::createUuid().toString(QUuid::Id128));
|
||||||
|
{
|
||||||
|
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
|
||||||
|
db.setDatabaseName(DB_PATH);
|
||||||
|
if (!db.open())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbExistError;
|
||||||
|
result.error = "Database connection failed: " + db.lastError().text();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QSqlQuery queryUpdate(db);
|
||||||
|
queryUpdate.prepare(QString(
|
||||||
|
"UPDATE clients SET lastName=:lastName, firstName=:firstName, patronymic=:patronymic, "
|
||||||
|
"phone=:phone, email=:email, city=:city, yourCompany=:yourCompany, hardwareHash=:hardwareHash, "
|
||||||
|
"licenseTime=:licenseTime, comment=:comment WHERE id=:id;"
|
||||||
|
));
|
||||||
|
queryUpdate.bindValue(":lastName", updateItem.lastName);
|
||||||
|
queryUpdate.bindValue(":firstName", updateItem.firstName);
|
||||||
|
queryUpdate.bindValue(":patronymic", updateItem.patronymic);
|
||||||
|
queryUpdate.bindValue(":phone", updateItem.phone);
|
||||||
|
queryUpdate.bindValue(":email", updateItem.email);
|
||||||
|
queryUpdate.bindValue(":city", updateItem.city);
|
||||||
|
queryUpdate.bindValue(":yourCompany", updateItem.yourCompany);
|
||||||
|
queryUpdate.bindValue(":hardwareHash", updateItem.hardwareHash);
|
||||||
|
queryUpdate.bindValue(":licenseTime", updateItem.licenseTime);
|
||||||
|
queryUpdate.bindValue(":comment", updateItem.comment);
|
||||||
|
queryUpdate.bindValue(":id", updateItem.id);
|
||||||
|
|
||||||
|
if (!queryUpdate.exec())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbStructError;
|
||||||
|
result.error = queryUpdate.lastError().text();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QSqlQuery querySelect(db);
|
||||||
|
querySelect.prepare(QString("SELECT * FROM clients WHERE id=:id;"));
|
||||||
|
querySelect.bindValue(":id", updateItem.id);
|
||||||
|
|
||||||
|
if (!querySelect.exec())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbStructError;
|
||||||
|
result.error = querySelect.lastError().text();
|
||||||
|
}
|
||||||
|
else if (!querySelect.next())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbStructError;
|
||||||
|
result.error = "Updated client not found: " + updateItem.id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LicenseItem output;
|
||||||
|
output.id = updateItem.id;
|
||||||
|
output.lastName = querySelect.value("lastName").toString();
|
||||||
|
output.firstName = querySelect.value("firstName").toString();
|
||||||
|
output.patronymic = querySelect.value("patronymic").toString();
|
||||||
|
output.phone = querySelect.value("phone").toString();
|
||||||
|
output.email = querySelect.value("email").toString();
|
||||||
|
output.city = querySelect.value("city").toString();
|
||||||
|
output.yourCompany = querySelect.value("yourCompany").toString();
|
||||||
|
output.hardwareHash = querySelect.value("hardwareHash").toString();
|
||||||
|
output.licenseTime = querySelect.value("licenseTime").toString();
|
||||||
|
output.comment = querySelect.value("comment").toString();
|
||||||
|
result.data.append(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QSqlDatabase::removeDatabase(connectionName);
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LicenseModel::updateModel()
|
||||||
|
{
|
||||||
|
m_status = Status::Working;
|
||||||
|
emit statusChanged();
|
||||||
|
|
||||||
|
auto* watcher = new QFutureWatcher<Result>(this);
|
||||||
|
connect(watcher, &QFutureWatcher<Result>::finished, this, [this, watcher]() {
|
||||||
|
const Result result = watcher->result();
|
||||||
|
if (result.status != Status::Ok)
|
||||||
|
{
|
||||||
|
m_status = result.status;
|
||||||
|
if (!result.error.isEmpty())
|
||||||
|
m_errors.append(result.error);
|
||||||
|
emit statusChanged();
|
||||||
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginResetModel();
|
||||||
|
m_data = result.data;
|
||||||
|
endResetModel();
|
||||||
|
|
||||||
|
m_status = Status::Ok;
|
||||||
|
emit statusChanged();
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher->setFuture(QtConcurrent::run([]() {
|
||||||
|
Result result;
|
||||||
|
const QString connectionName = QString("license_load_%1")
|
||||||
|
.arg(QUuid::createUuid().toString(QUuid::Id128));
|
||||||
|
{
|
||||||
|
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
|
||||||
|
db.setDatabaseName(DB_PATH);
|
||||||
|
if (!db.open())
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbExistError;
|
||||||
|
result.error = "Database connection failed: " + db.lastError().text();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QSqlQuery query(db);
|
||||||
|
if (!query.exec(
|
||||||
|
"SELECT id, lastName, firstName, patronymic, phone, email, city, yourCompany, hardwareHash, licenseTime, comment "
|
||||||
|
"FROM clients ORDER BY id DESC;"
|
||||||
|
))
|
||||||
|
{
|
||||||
|
result.status = LicenseModel::Status::DbStructError;
|
||||||
|
result.error = query.lastError().text();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (query.next())
|
||||||
|
{
|
||||||
|
LicenseItem item;
|
||||||
|
item.id = query.value("id").toString();
|
||||||
|
item.lastName = query.value("lastName").toString();
|
||||||
|
item.firstName = query.value("firstName").toString();
|
||||||
|
item.patronymic = query.value("patronymic").toString();
|
||||||
|
item.phone = query.value("phone").toString();
|
||||||
|
item.email = query.value("email").toString();
|
||||||
|
item.city = query.value("city").toString();
|
||||||
|
item.yourCompany = query.value("yourCompany").toString();
|
||||||
|
item.hardwareHash = query.value("hardwareHash").toString();
|
||||||
|
item.licenseTime = query.value("licenseTime").toString();
|
||||||
|
item.comment = query.value("comment").toString();
|
||||||
|
result.data.append(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QSqlDatabase::removeDatabase(connectionName);
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
LicenseModel::LicenseItem LicenseModel::getItem(int index) const
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_data.count())
|
||||||
|
return LicenseItem();
|
||||||
|
return m_data[index];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,58 +1,149 @@
|
|||||||
#ifndef LICENSEMANAGER_LICENSEMODEL_H
|
#ifndef LICENSEMANAGER_LICENSEMODEL_H
|
||||||
#define LICENSEMANAGER_LICENSEMODEL_H
|
#define LICENSEMANAGER_LICENSEMODEL_H
|
||||||
|
|
||||||
#include <QAbstractTableModel>
|
/**
|
||||||
|
* @file LicenseModel.h
|
||||||
|
* @brief Модель данных клиентов и операции с базой SQLite.
|
||||||
|
*
|
||||||
|
* @details
|
||||||
|
* Класс инкапсулирует структуру таблицы клиентов и обеспечивает
|
||||||
|
* асинхронные операции чтения/добавления/редактирования/удаления.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Qt
|
||||||
|
#include <QAbstractItemModel>
|
||||||
|
#include <QFutureWatcher>
|
||||||
#include <QtSql/QSqlDatabase>
|
#include <QtSql/QSqlDatabase>
|
||||||
|
|
||||||
class LicenseModel : public QAbstractTableModel
|
/**
|
||||||
|
* @brief Табличная модель клиентов и лицензий.
|
||||||
|
*
|
||||||
|
* @details
|
||||||
|
* Хранит данные в памяти, синхронизируясь с SQLite. Все операции,
|
||||||
|
* требующие доступа к БД, выполняются в отдельных потоках.
|
||||||
|
*/
|
||||||
|
class LicenseModel : public QAbstractItemModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Состояние модели и базы данных.
|
||||||
|
*/
|
||||||
enum class Status
|
enum class Status
|
||||||
{
|
{
|
||||||
None = 0,
|
None = 0, //!< Состояние не определено.
|
||||||
Ok,
|
Ok, //!< База данных готова.
|
||||||
DbStructError,
|
DbStructError, //!< Ошибка структуры или запроса к БД.
|
||||||
DbExistError,
|
DbExistError, //!< Ошибка открытия базы данных.
|
||||||
|
Working, //!< Выполняется операция с БД.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Запись клиента в базе.
|
||||||
|
*/
|
||||||
struct LicenseItem
|
struct LicenseItem
|
||||||
{
|
{
|
||||||
QString id;
|
QString id; //!< Идентификатор строки в БД.
|
||||||
QString city;
|
QString city; //!< Город.
|
||||||
QString email;
|
QString email; //!< Электронная почта.
|
||||||
QString firstName;
|
QString firstName; //!< Имя.
|
||||||
QString hardwareHash;
|
QString hardwareHash; //!< Аппаратный хеш (machine id).
|
||||||
QString lastName;
|
QString lastName; //!< Фамилия.
|
||||||
QString patronymic;
|
QString patronymic; //!< Отчество.
|
||||||
QString phone;
|
QString phone; //!< Телефон.
|
||||||
QString yourCompany;
|
QString yourCompany; //!< Название компании.
|
||||||
QString comment;
|
QString licenseTime; //!< Срок лицензии (пусто = бессрочно).
|
||||||
|
QString comment; //!< Комментарий.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Результат асинхронной операции с БД.
|
||||||
|
*/
|
||||||
|
struct Result
|
||||||
|
{
|
||||||
|
QList<LicenseItem> data; //!< Данные, полученные из БД.
|
||||||
|
QStringList ids; //!< Список id, над которыми выполнялась операция.
|
||||||
|
Status status = Status::Ok; //!< Итоговый статус операции.
|
||||||
|
QString error; //!< Текст ошибки (если есть).
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Создаёт модель и выполняет первичную проверку БД.
|
||||||
|
*/
|
||||||
explicit LicenseModel(QObject* parent = nullptr);
|
explicit LicenseModel(QObject* parent = nullptr);
|
||||||
~LicenseModel();
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает количество строк модели.
|
||||||
|
*/
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
/**
|
||||||
|
* @brief Возвращает количество столбцов модели.
|
||||||
|
*/
|
||||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
/**
|
||||||
|
* @brief Возвращает отображаемые данные для ячейки.
|
||||||
|
*/
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
|
/**
|
||||||
|
* @brief Возвращает заголовки строк/столбцов.
|
||||||
|
*/
|
||||||
|
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||||
|
/**
|
||||||
|
* @brief Табличная модель не имеет иерархии.
|
||||||
|
*/
|
||||||
|
QModelIndex parent(const QModelIndex &index) const override;
|
||||||
|
/**
|
||||||
|
* @brief Возвращает индекс ячейки по координатам.
|
||||||
|
*/
|
||||||
|
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
/**
|
||||||
|
* @brief Возвращает текущий статус модели.
|
||||||
|
*/
|
||||||
Status getStatus();
|
Status getStatus();
|
||||||
|
/**
|
||||||
|
* @brief Возвращает текст ошибок, накопленных моделью.
|
||||||
|
*/
|
||||||
QString getStatusText();
|
QString getStatusText();
|
||||||
|
/**
|
||||||
|
* @brief Создаёт таблицы БД из ресурса `tables.ddl`.
|
||||||
|
*/
|
||||||
bool prepareDatabase();
|
bool prepareDatabase();
|
||||||
|
/**
|
||||||
|
* @brief Добавляет клиента в БД и обновляет модель.
|
||||||
|
*/
|
||||||
|
void addClient(const LicenseItem &item);
|
||||||
|
/**
|
||||||
|
* @brief Удаляет клиентов по индексам строк.
|
||||||
|
*/
|
||||||
|
void deleteClient(const QList<int> &rows);
|
||||||
|
/**
|
||||||
|
* @brief Обновляет клиента по индексу строки.
|
||||||
|
*/
|
||||||
|
void editClient(const LicenseItem &item, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Перезагружает модель из базы данных.
|
||||||
|
*/
|
||||||
|
void updateModel();
|
||||||
|
/**
|
||||||
|
* @brief Возвращает запись клиента по индексу строки.
|
||||||
|
*/
|
||||||
|
LicenseItem getItem(int index) const;
|
||||||
|
signals:
|
||||||
|
/**
|
||||||
|
* @brief Сигнал изменения статуса модели.
|
||||||
|
*/
|
||||||
|
void statusChanged();
|
||||||
private:
|
private:
|
||||||
|
/**
|
||||||
|
* @brief Проверяет наличие таблиц БД.
|
||||||
|
*/
|
||||||
bool checkTables();
|
bool checkTables();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QList<LicenseItem> m_data;
|
QList<LicenseItem> m_data; //!< Кэш данных для отображения.
|
||||||
Status m_status = Status::None;
|
Status m_status = Status::None; //!< Текущий статус модели.
|
||||||
QStringList m_errors;
|
QStringList m_errors; //!< Список ошибок работы с БД.
|
||||||
QSqlDatabase m_db;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
#endif // LICENSEMANAGER_LICENSEMODEL_H
|
#endif // LICENSEMANAGER_LICENSEMODEL_H
|
||||||
|
|||||||
@@ -3,19 +3,27 @@
|
|||||||
// Qt
|
// Qt
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QFileDialog>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QLabel>
|
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QStatusBar>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <QTableView>
|
#include <QTableView>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QContextMenuEvent>
|
|
||||||
|
|
||||||
// Self
|
// Self
|
||||||
|
#include "../def.h"
|
||||||
|
#include "LicenseGenerator/crypt.h"
|
||||||
|
#include "LicenseGenerator/Util.h"
|
||||||
#include "LicenseModel/LicenseModel.h"
|
#include "LicenseModel/LicenseModel.h"
|
||||||
|
#include "EditClientDialog/EditClientDialog.h"
|
||||||
|
|
||||||
|
#define WINDOW_SIZE "WindowSize"
|
||||||
|
|
||||||
MainWidget::MainWidget(QWidget *parent)
|
MainWidget::MainWidget(QWidget *parent)
|
||||||
: QMainWindow(parent)
|
: QMainWindow(parent)
|
||||||
@@ -23,24 +31,47 @@ MainWidget::MainWidget(QWidget *parent)
|
|||||||
{
|
{
|
||||||
setWindowIcon(QIcon(QStringLiteral("qrc:/deps/icon.png")));
|
setWindowIcon(QIcon(QStringLiteral("qrc:/deps/icon.png")));
|
||||||
setWindowTitle("LicenseManager");
|
setWindowTitle("LicenseManager");
|
||||||
resize({800, 600});
|
setMinimumSize({800, 600});
|
||||||
|
resize(minimumSize());
|
||||||
|
|
||||||
|
// Status bar
|
||||||
|
{
|
||||||
|
m_statusBar = new QStatusBar(this);
|
||||||
|
setStatusBar(m_statusBar);
|
||||||
|
m_statusLabel = new QLabel("No status", this);
|
||||||
|
m_statusBar->addWidget(m_statusLabel);
|
||||||
|
connect(m_licenseModel, &LicenseModel::statusChanged, this, &MainWidget::modelStatusChanged);
|
||||||
|
}
|
||||||
|
|
||||||
// Model init
|
// Model init
|
||||||
{
|
{
|
||||||
if (m_licenseModel->getStatus() == LicenseModel::Status::DbStructError)
|
if (m_licenseModel->getStatus() == LicenseModel::Status::DbStructError)
|
||||||
{
|
{
|
||||||
if (!m_licenseModel->prepareDatabase())
|
QTimer::singleShot(0, [&]() {
|
||||||
{
|
auto result = QMessageBox::question(
|
||||||
QTimer::singleShot(0, [&]() {
|
this
|
||||||
QMessageBox messageBox;
|
, tr("Database not exist")
|
||||||
messageBox.setIcon(QMessageBox::Critical);
|
, tr("Create new database?")
|
||||||
messageBox.setWindowTitle(tr("Error"));
|
);
|
||||||
messageBox.setText(tr("Error with prepare database"));
|
if (result == QMessageBox::Button::Yes)
|
||||||
messageBox.setDetailedText(m_licenseModel->getStatusText());
|
{
|
||||||
messageBox.exec();
|
if (!m_licenseModel->prepareDatabase())
|
||||||
|
{
|
||||||
|
QMessageBox messageBox;
|
||||||
|
messageBox.setIcon(QMessageBox::Critical);
|
||||||
|
messageBox.setWindowTitle(tr("Error"));
|
||||||
|
messageBox.setText(tr("Database create error"));
|
||||||
|
messageBox.setDetailedText(m_licenseModel->getStatusText());
|
||||||
|
messageBox.exec();
|
||||||
|
|
||||||
|
QApplication::quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (result == QMessageBox::Button::No)
|
||||||
QApplication::quit();
|
QApplication::quit();
|
||||||
});
|
else
|
||||||
}
|
qDebug() << "result" << result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,16 +79,22 @@ MainWidget::MainWidget(QWidget *parent)
|
|||||||
{
|
{
|
||||||
m_tableView = new QTableView(this);
|
m_tableView = new QTableView(this);
|
||||||
m_tableView->horizontalHeader()->setStretchLastSection(true);
|
m_tableView->horizontalHeader()->setStretchLastSection(true);
|
||||||
|
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||||
m_tableView->setModel(m_licenseModel);
|
m_tableView->setModel(m_licenseModel);
|
||||||
setCentralWidget(m_tableView);
|
setCentralWidget(m_tableView);
|
||||||
|
|
||||||
connect(m_tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWidget::selectionChanged);
|
connect(m_tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWidget::selectionChanged);
|
||||||
|
|
||||||
// ToolTip
|
// ToolBar
|
||||||
{
|
{
|
||||||
m_toolBar = addToolBar("ToolBar");
|
m_toolBar = addToolBar("ToolBar");
|
||||||
m_toolBar->setMovable(false);
|
m_toolBar->setMovable(false);
|
||||||
m_addClientToolBarAction = m_toolBar->addAction(QIcon(QStringLiteral(":/deps/add.png")), tr("Add client"));
|
m_addClientToolBarAction = m_toolBar->addAction(QIcon(QStringLiteral(":/deps/add.png")), tr("Add client"));
|
||||||
|
m_editClientToolBarAction = m_toolBar->addAction(QIcon(QStringLiteral(":/deps/edit.png")), tr("Edit client"));
|
||||||
|
m_deleteClientToolBarAction = m_toolBar->addAction(QIcon(QStringLiteral(":/deps/delete.png")), tr("Delete client"));
|
||||||
|
auto *spacer = new QWidget(m_toolBar);
|
||||||
|
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||||
|
m_toolBar->addWidget(spacer);
|
||||||
m_infoToolBarAction = m_toolBar->addAction(QIcon(QStringLiteral(":/deps/info.png")), tr("Get info"));
|
m_infoToolBarAction = m_toolBar->addAction(QIcon(QStringLiteral(":/deps/info.png")), tr("Get info"));
|
||||||
|
|
||||||
connect(m_addClientToolBarAction, &QAction::triggered, this, &MainWidget::onAddClientTriggered);
|
connect(m_addClientToolBarAction, &QAction::triggered, this, &MainWidget::onAddClientTriggered);
|
||||||
@@ -73,11 +110,16 @@ MainWidget::MainWidget(QWidget *parent)
|
|||||||
m_addClientsMenuAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/add.png")), tr("Add client"));
|
m_addClientsMenuAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/add.png")), tr("Add client"));
|
||||||
m_editClientsMenuAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/edit.png")), tr("Edit client"));
|
m_editClientsMenuAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/edit.png")), tr("Edit client"));
|
||||||
m_deleteClientsMenuAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/delete.png")), tr("Delete client"));
|
m_deleteClientsMenuAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/delete.png")), tr("Delete client"));
|
||||||
|
m_menu->addSeparator();
|
||||||
|
m_getLicenseFileAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/getLicenseFile.png")), tr("Get license file"));
|
||||||
|
|
||||||
connect(m_reloadTableMenuAction, &QAction::triggered, this, &MainWidget::onAddClientTriggered);
|
connect(m_deleteClientToolBarAction, &QAction::triggered, this, &MainWidget::onDeleteClientTriggered);
|
||||||
|
connect(m_editClientToolBarAction, &QAction::triggered, this, &MainWidget::onEditClientTriggered);
|
||||||
|
connect(m_reloadTableMenuAction, &QAction::triggered, this, &MainWidget::onReloadTableTriggered);
|
||||||
connect(m_addClientsMenuAction, &QAction::triggered, this, &MainWidget::onAddClientTriggered);
|
connect(m_addClientsMenuAction, &QAction::triggered, this, &MainWidget::onAddClientTriggered);
|
||||||
connect(m_editClientsMenuAction, &QAction::triggered, this, &MainWidget::onEditClientTriggered);
|
connect(m_editClientsMenuAction, &QAction::triggered, this, &MainWidget::onEditClientTriggered);
|
||||||
connect(m_deleteClientsMenuAction, &QAction::triggered, this, &MainWidget::onDeleteClientTriggered);
|
connect(m_deleteClientsMenuAction, &QAction::triggered, this, &MainWidget::onDeleteClientTriggered);
|
||||||
|
connect(m_getLicenseFileAction, &QAction::triggered, this, &MainWidget::onGetLicenseFileTriggered);
|
||||||
|
|
||||||
m_tableView->setContextMenuPolicy(Qt::CustomContextMenu);
|
m_tableView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
connect(m_tableView, &QTableView::customContextMenuRequested,
|
connect(m_tableView, &QTableView::customContextMenuRequested,
|
||||||
@@ -85,8 +127,15 @@ MainWidget::MainWidget(QWidget *parent)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit client dialog
|
||||||
|
{
|
||||||
|
m_editClientDialog = new EditClientDialog(m_licenseModel, this);
|
||||||
|
connect(m_editClientDialog, &QDialog::finished, this, &MainWidget::onEditClientDialogClosed);
|
||||||
|
}
|
||||||
|
|
||||||
m_tableView->resizeColumnsToContents();
|
m_tableView->resizeColumnsToContents();
|
||||||
selectionChanged({}, {});
|
selectionChanged();
|
||||||
|
modelStatusChanged();
|
||||||
loadSettings();
|
loadSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +156,7 @@ void MainWidget::onGetInfoTriggered()
|
|||||||
auto vLayout = new QVBoxLayout;
|
auto vLayout = new QVBoxLayout;
|
||||||
dialog.setLayout(vLayout);
|
dialog.setLayout(vLayout);
|
||||||
QIcon icon(":/deps/icon.png");
|
QIcon icon(":/deps/icon.png");
|
||||||
QLabel *iconLabel = new QLabel;
|
QLabel *iconLabel = new QLabel(&dialog);
|
||||||
iconLabel->setPixmap(icon.pixmap(300, 300));
|
iconLabel->setPixmap(icon.pixmap(300, 300));
|
||||||
iconLabel->setAlignment(Qt::AlignCenter);
|
iconLabel->setAlignment(Qt::AlignCenter);
|
||||||
vLayout->addWidget(iconLabel);
|
vLayout->addWidget(iconLabel);
|
||||||
@@ -132,31 +181,137 @@ void MainWidget::onGetInfoTriggered()
|
|||||||
|
|
||||||
void MainWidget::onReloadTableTriggered()
|
void MainWidget::onReloadTableTriggered()
|
||||||
{
|
{
|
||||||
|
m_licenseModel->updateModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWidget::onAddClientTriggered()
|
void MainWidget::onAddClientTriggered()
|
||||||
{
|
{
|
||||||
|
m_editClientDialog->setType(EditClientDialog::Type::Add);
|
||||||
|
m_editClientDialog->clear();
|
||||||
|
m_editClientDialog->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWidget::onEditClientTriggered()
|
void MainWidget::onEditClientTriggered()
|
||||||
{
|
{
|
||||||
|
m_editClientDialog->setType(EditClientDialog::Type::Edit);
|
||||||
|
if (m_tableView->selectionModel()->selectedRows().size() != 1)
|
||||||
|
return;
|
||||||
|
m_editClientDialog->setClientInfo(m_tableView->selectionModel()->selectedRows().first().row());
|
||||||
|
m_editClientDialog->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWidget::onDeleteClientTriggered()
|
void MainWidget::onDeleteClientTriggered()
|
||||||
{
|
{
|
||||||
|
auto selectedRows = m_tableView->selectionModel()->selectedRows();
|
||||||
|
if (selectedRows.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QList<int> list;
|
||||||
|
for (auto i = selectedRows.size() - 1; i >= 0; --i)
|
||||||
|
list << selectedRows.at(i).row();
|
||||||
|
|
||||||
|
auto result = QMessageBox::question(
|
||||||
|
this,
|
||||||
|
tr("Delete rows"),
|
||||||
|
tr(QString("Do you want to delete %1 rows?").arg(list.size()).toUtf8())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == QMessageBox::Yes)
|
||||||
|
m_licenseModel->deleteClient(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWidget::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
|
void MainWidget::onGetLicenseFileTriggered()
|
||||||
{
|
{
|
||||||
Q_UNUSED(deselected);
|
auto selectedRows = m_tableView->selectionModel()->selectedRows();
|
||||||
|
if (selectedRows.size() != 1)
|
||||||
|
return;
|
||||||
|
|
||||||
auto selectedCount = selected.size();
|
const int row = selectedRows.first().row();
|
||||||
m_editClientsMenuAction->setEnabled(selectedCount == 1);
|
const auto item = m_licenseModel->getItem(row);
|
||||||
m_deleteClientsMenuAction->setEnabled(selectedCount > 0);
|
|
||||||
|
QStringList nameParts;
|
||||||
|
if (!item.lastName.isEmpty())
|
||||||
|
nameParts << item.lastName;
|
||||||
|
if (!item.firstName.isEmpty())
|
||||||
|
nameParts << item.firstName;
|
||||||
|
if (!item.patronymic.isEmpty())
|
||||||
|
nameParts << item.patronymic;
|
||||||
|
if (nameParts.isEmpty() && !item.yourCompany.isEmpty())
|
||||||
|
nameParts << item.yourCompany;
|
||||||
|
|
||||||
|
QString baseName = nameParts.join('_');
|
||||||
|
if (baseName.isEmpty())
|
||||||
|
baseName = "license";
|
||||||
|
baseName.replace('/', '_');
|
||||||
|
baseName.replace('\\', '_');
|
||||||
|
|
||||||
|
QString baseDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
||||||
|
if (baseDir.isEmpty())
|
||||||
|
baseDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
|
||||||
|
const QString defaultPath = baseDir + "/" + baseName + ".dat";
|
||||||
|
QString filePath = QFileDialog::getSaveFileName(
|
||||||
|
this,
|
||||||
|
tr("Save license file"),
|
||||||
|
defaultPath,
|
||||||
|
tr("License files (*.license);;All files (*)")
|
||||||
|
);
|
||||||
|
if (filePath.isEmpty())
|
||||||
|
return;
|
||||||
|
if (!filePath.endsWith(".dat", Qt::CaseInsensitive))
|
||||||
|
filePath += ".dat";
|
||||||
|
|
||||||
|
QJsonObject licenseObject;
|
||||||
|
licenseObject.insert("machineId", item.hardwareHash);
|
||||||
|
|
||||||
|
if (!item.licenseTime.isEmpty())
|
||||||
|
licenseObject.insert("validUntil", QDate::fromString(item.licenseTime).toString(Qt::ISODate));
|
||||||
|
|
||||||
|
QJsonDocument doc(licenseObject);
|
||||||
|
const QByteArray data = doc.toJson(QJsonDocument::Indented);
|
||||||
|
const QByteArray privateKey = fileReadAll(":/deps/Key");
|
||||||
|
if (privateKey.isEmpty())
|
||||||
|
{
|
||||||
|
QMessageBox::warning(
|
||||||
|
this,
|
||||||
|
tr("Error"),
|
||||||
|
tr("Failed to load encryption key")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray encrypted = Crypt::rsaEncrypt(toCbor(doc), privateKey);
|
||||||
|
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||||
|
{
|
||||||
|
QMessageBox::warning(
|
||||||
|
this,
|
||||||
|
tr("Error"),
|
||||||
|
tr("Failed to open license file: %1").arg(file.errorString())
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.write(encrypted.toHex()) != encrypted.toHex().size())
|
||||||
|
{
|
||||||
|
QMessageBox::warning(
|
||||||
|
this,
|
||||||
|
tr("Error"),
|
||||||
|
tr("Failed to write license file: %1").arg(file.errorString())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWidget::selectionChanged()
|
||||||
|
{
|
||||||
|
auto selectedCount = m_tableView->selectionModel()->selectedRows().size();
|
||||||
|
m_addClientToolBarAction->setEnabled(m_licenseModel->getStatus() == LicenseModel::Status::Ok);
|
||||||
|
m_editClientToolBarAction->setEnabled(selectedCount == 1 && m_licenseModel->getStatus() == LicenseModel::Status::Ok);
|
||||||
|
m_deleteClientToolBarAction->setEnabled(selectedCount > 0 && m_licenseModel->getStatus() == LicenseModel::Status::Ok);
|
||||||
|
m_addClientsMenuAction->setEnabled(m_licenseModel->getStatus() == LicenseModel::Status::Ok);
|
||||||
|
m_editClientsMenuAction->setEnabled(selectedCount == 1 && m_licenseModel->getStatus() == LicenseModel::Status::Ok);
|
||||||
|
m_deleteClientsMenuAction->setEnabled(selectedCount > 0 && m_licenseModel->getStatus() == LicenseModel::Status::Ok);
|
||||||
|
m_getLicenseFileAction->setEnabled(selectedCount == 1 && m_licenseModel->getStatus() == LicenseModel::Status::Ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWidget::closeEvent(QCloseEvent *event)
|
void MainWidget::closeEvent(QCloseEvent *event)
|
||||||
@@ -177,10 +332,57 @@ void MainWidget::closeEvent(QCloseEvent *event)
|
|||||||
|
|
||||||
void MainWidget::loadSettings()
|
void MainWidget::loadSettings()
|
||||||
{
|
{
|
||||||
|
QSettings settings(ORGANIZATION_NAME, metaObject()->className());
|
||||||
|
if (settings.contains(WINDOW_SIZE))
|
||||||
|
resize(settings.value(WINDOW_SIZE).toSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWidget::saveSettings()
|
void MainWidget::saveSettings()
|
||||||
{
|
{
|
||||||
|
QSettings settings(ORGANIZATION_NAME, metaObject()->className());
|
||||||
|
settings.setValue(WINDOW_SIZE, size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWidget::onEditClientDialogClosed(int result)
|
||||||
|
{
|
||||||
|
if (result == QDialog::Rejected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (m_editClientDialog->getType())
|
||||||
|
{
|
||||||
|
case EditClientDialog::Type::Edit:
|
||||||
|
if (m_tableView->selectionModel()->selectedRows().size() != 1)
|
||||||
|
return;
|
||||||
|
m_licenseModel->editClient(m_editClientDialog->getLicenseItem(), m_tableView->selectionModel()->selectedRows().at(0).row());
|
||||||
|
break;
|
||||||
|
case EditClientDialog::Type::Add:
|
||||||
|
m_licenseModel->addClient(m_editClientDialog->getLicenseItem());
|
||||||
|
break;
|
||||||
|
case EditClientDialog::Type::None:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWidget::modelStatusChanged()
|
||||||
|
{
|
||||||
|
switch (m_licenseModel->getStatus())
|
||||||
|
{
|
||||||
|
case LicenseModel::Status::Ok:
|
||||||
|
m_statusLabel->setText(tr("Database ready"));
|
||||||
|
break;
|
||||||
|
case LicenseModel::Status::DbExistError:
|
||||||
|
m_statusLabel->setText(tr("Database exist error"));
|
||||||
|
break;
|
||||||
|
case LicenseModel::Status::DbStructError:
|
||||||
|
m_statusLabel->setText(tr("Database struct error"));
|
||||||
|
break;
|
||||||
|
case LicenseModel::Status::Working:
|
||||||
|
m_statusLabel->setText(tr("Working"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
m_statusLabel->setText(tr("Unknown status"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,115 @@
|
|||||||
#ifndef LICENSEMANAGER_MAINWIDGET_H
|
#ifndef LICENSEMANAGER_MAINWIDGET_H
|
||||||
#define LICENSEMANAGER_MAINWIDGET_H
|
#define LICENSEMANAGER_MAINWIDGET_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file MainWidget.h
|
||||||
|
* @brief Интерфейс главного окна приложения.
|
||||||
|
*
|
||||||
|
* @details
|
||||||
|
* Содержит декларацию главного окна, управляющего таблицей клиентов,
|
||||||
|
* контекстным меню, действиями панели инструментов и созданием лицензий.
|
||||||
|
*/
|
||||||
|
|
||||||
// Qt
|
// Qt
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QItemSelection>
|
#include <QItemSelection>
|
||||||
class QTableView;
|
|
||||||
class QMenu;
|
|
||||||
class QAction;
|
class QAction;
|
||||||
|
class QLabel;
|
||||||
|
class QMenu;
|
||||||
|
class QStatusBar;
|
||||||
|
class QTableView;
|
||||||
|
|
||||||
// Self
|
// Self
|
||||||
class LicenseModel;
|
#include "LicenseModel/LicenseModel.h"
|
||||||
|
class EditClientDialog;
|
||||||
|
|
||||||
class MainWidget : public QMainWindow
|
class MainWidget : public QMainWindow
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Создаёт главное окно и инициализирует интерфейс.
|
||||||
|
*/
|
||||||
explicit MainWidget(QWidget *parent = nullptr);
|
explicit MainWidget(QWidget *parent = nullptr);
|
||||||
~MainWidget();
|
/**
|
||||||
|
* @brief Завершает работу окна и сохраняет настройки.
|
||||||
|
*/
|
||||||
|
~MainWidget() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
/**
|
||||||
|
* @brief Перехватывает закрытие окна для подтверждения выхода.
|
||||||
|
*/
|
||||||
void closeEvent(QCloseEvent *event) override;
|
void closeEvent(QCloseEvent *event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
/**
|
||||||
|
* @brief Показывает контекстное меню таблицы.
|
||||||
|
*/
|
||||||
void onCustomContextMenuRequested(const QPoint &pos);
|
void onCustomContextMenuRequested(const QPoint &pos);
|
||||||
|
/**
|
||||||
|
* @brief Отображает диалог с информацией о версии/репозитории.
|
||||||
|
*/
|
||||||
void onGetInfoTriggered();
|
void onGetInfoTriggered();
|
||||||
|
/**
|
||||||
|
* @brief Перезагружает данные модели из базы.
|
||||||
|
*/
|
||||||
void onReloadTableTriggered();
|
void onReloadTableTriggered();
|
||||||
|
/**
|
||||||
|
* @brief Открывает диалог добавления клиента.
|
||||||
|
*/
|
||||||
void onAddClientTriggered();
|
void onAddClientTriggered();
|
||||||
|
/**
|
||||||
|
* @brief Открывает диалог редактирования выбранного клиента.
|
||||||
|
*/
|
||||||
void onEditClientTriggered();
|
void onEditClientTriggered();
|
||||||
|
/**
|
||||||
|
* @brief Удаляет выбранные строки клиентов.
|
||||||
|
*/
|
||||||
void onDeleteClientTriggered();
|
void onDeleteClientTriggered();
|
||||||
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
|
/**
|
||||||
|
* @brief Генерирует и сохраняет файл лицензии для выбранного клиента.
|
||||||
|
*/
|
||||||
|
void onGetLicenseFileTriggered();
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает закрытие диалога редактирования клиента.
|
||||||
|
*/
|
||||||
|
void onEditClientDialogClosed(int result);
|
||||||
|
/**
|
||||||
|
* @brief Обновляет доступность действий при смене выбора.
|
||||||
|
*/
|
||||||
|
void selectionChanged();
|
||||||
|
/**
|
||||||
|
* @brief Обновляет статусную строку при изменении состояния модели.
|
||||||
|
*/
|
||||||
|
void modelStatusChanged();
|
||||||
private:
|
private:
|
||||||
|
/**
|
||||||
|
* @brief Сохраняет параметры окна (размер, положение).
|
||||||
|
*/
|
||||||
void saveSettings();
|
void saveSettings();
|
||||||
|
/**
|
||||||
|
* @brief Загружает параметры окна.
|
||||||
|
*/
|
||||||
void loadSettings();
|
void loadSettings();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTableView* m_tableView{nullptr};
|
QTableView* m_tableView{nullptr}; //!< Таблица с клиентами.
|
||||||
LicenseModel* m_licenseModel{nullptr};
|
LicenseModel* m_licenseModel{nullptr}; //!< Модель данных лицензий.
|
||||||
QToolBar* m_toolBar{nullptr};
|
QToolBar* m_toolBar{nullptr}; //!< Панель инструментов.
|
||||||
QMenu* m_menu{nullptr};
|
QMenu* m_menu{nullptr}; //!< Контекстное меню таблицы.
|
||||||
QAction* m_addClientToolBarAction{nullptr};
|
QAction* m_addClientToolBarAction{nullptr}; //!< Действие добавления клиента.
|
||||||
QAction* m_infoToolBarAction{nullptr};
|
QAction* m_editClientToolBarAction{nullptr}; //!< Действие редактирования клиента.
|
||||||
QAction* m_reloadTableMenuAction{nullptr};
|
QAction* m_deleteClientToolBarAction{nullptr}; //!< Действие удаления клиента.
|
||||||
QAction* m_addClientsMenuAction{nullptr};
|
QAction* m_infoToolBarAction{nullptr}; //!< Действие показа информации.
|
||||||
QAction* m_deleteClientsMenuAction{nullptr};
|
QAction* m_reloadTableMenuAction{nullptr}; //!< Действие обновления таблицы.
|
||||||
QAction* m_editClientsMenuAction{nullptr};
|
QAction* m_addClientsMenuAction{nullptr}; //!< Действие добавления из меню.
|
||||||
|
QAction* m_deleteClientsMenuAction{nullptr}; //!< Действие удаления из меню.
|
||||||
|
QAction* m_editClientsMenuAction{nullptr}; //!< Действие редактирования из меню.
|
||||||
|
QAction* m_getLicenseFileAction{nullptr}; //!< Действие сохранения файла лицензии.
|
||||||
|
EditClientDialog* m_editClientDialog{nullptr}; //!< Диалог редактирования клиента.
|
||||||
|
QStatusBar* m_statusBar{nullptr}; //!< Строка состояния.
|
||||||
|
QLabel* m_statusLabel{nullptr}; //!< Текст статуса модели.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
#endif // LICENSEMANAGER_MAINWIDGET_H
|
#endif // LICENSEMANAGER_MAINWIDGET_H
|
||||||
|
|||||||
19
src/def.h
19
src/def.h
@@ -1,8 +1,25 @@
|
|||||||
#ifndef LICENSEMANAGER_DEF_H
|
#ifndef LICENSEMANAGER_DEF_H
|
||||||
#define LICENSEMANAGER_DEF_H
|
#define LICENSEMANAGER_DEF_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file def.h
|
||||||
|
* @brief Общие константы и базовые настройки приложения.
|
||||||
|
*
|
||||||
|
* @details
|
||||||
|
* Содержит глобальные значения, используемые в разных частях проекта:
|
||||||
|
* путь к базе данных и имя организации для QSettings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Путь к SQLite базе данных приложения.
|
||||||
|
*/
|
||||||
const static QString DB_PATH = "./db.sqlite";
|
const static QString DB_PATH = "./db.sqlite";
|
||||||
|
/**
|
||||||
|
* @brief Имя организации для QSettings.
|
||||||
|
*/
|
||||||
|
const static QString ORGANIZATION_NAME = "LicenseManager";
|
||||||
|
|
||||||
#endif //LICENSEMANAGER_DEF_H
|
#endif //LICENSEMANAGER_DEF_H
|
||||||
|
|||||||
Reference in New Issue
Block a user