chatgui/chatgui-android: уведомления со звуком, флаги и прочие улучшения UI

This commit is contained in:
evgeny committed 2026-09-10 00:23:14 +03:00
1 parent 6c857ceab8
commit 0f308fdbd7
20 files changed
+292 -32

No files matched your search

+89 -14
View File
@@ -28,6 +28,7 @@ extern "C" {
#include <QSplitter>
#include <QCloseEvent>
#include <QApplication>
#include <QScreen>
#include <QClipboard>
#include <QStyle>
#include <QMessageBox>
@@ -38,6 +39,7 @@ extern "C" {
#include <cmath>
#include <QJsonDocument>
#include <QJsonObject>
#include <csignal>
extern "C" {
#include "chat/chat_core.h"
@@ -49,12 +51,16 @@ void chat_core_save_ui_state_trampoline(void* arg);
static MainWindow* s_mainWindow = nullptr;
/* Сохранение last_channel_id в БД (ui_state), как все остальные UI-настройки.
Пишется из utun-потока через trampoline, чтобы не трогать sqlite из GUI-потока. */
static void saveLastChannelId(const QString& channelId) {
const char key[] = "last_channel_id";
QByteArray val = channelId.toUtf8();
size_t klen = sizeof(key) - 1, vlen = (size_t)val.size(), total = klen + 1 + vlen + 1;
#ifndef _WIN32
static volatile std::sig_atomic_t g_signalFlag = 0;
static void unixSignalHandler(int) { g_signalFlag = 1; }
#endif
/* Сохранение key/value в БД (ui_state). Пишется из utun-потока через trampoline,
чтобы не трогать sqlite из GUI-потока. Буфер формата key\0value\0, ≤256 байт. */
static void postUiStateKv(const char* key, const QString& value) {
QByteArray val = value.toUtf8();
size_t klen = strlen(key), vlen = (size_t)val.size(), total = klen + 1 + vlen + 1;
if (total > 256) return;
void* arg = u_malloc(total);
memcpy(arg, key, klen + 1);
@@ -62,6 +68,10 @@ static void saveLastChannelId(const QString& channelId) {
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
static void saveLastChannelId(const QString& channelId) {
postUiStateKv("last_channel_id", channelId);
}
static void onMsgReceivedCallback(const char* ch_id, int ch_id_len, uint64_t author) {
if (s_mainWindow)
s_mainWindow->onMessageReceived(ch_id, ch_id_len, author);
@@ -136,6 +146,14 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
setupConnects();
}
connect(m_autoPlayTimer, &QTimer::timeout, this, &MainWindow::autoPlayTimerTick);
#ifndef _WIN32
std::signal(SIGINT, unixSignalHandler);
std::signal(SIGTERM, unixSignalHandler);
m_shutdownTimer = new QTimer(this);
connect(m_shutdownTimer, &QTimer::timeout, this, &MainWindow::onShutdownCheck);
m_shutdownTimer->start(200);
#endif
}
MainWindow::~MainWindow() {
@@ -167,6 +185,7 @@ MainWindow::~MainWindow() {
void MainWindow::setupUi() {
auto *splitter = new QSplitter(Qt::Horizontal, this);
m_splitter = splitter;
m_channelList = new ChannelList(m_db, splitter);
m_recorder = new AudioRecorder(this);
@@ -186,6 +205,49 @@ void MainWindow::setupUi() {
setCentralWidget(splitter);
}
void MainWindow::saveWindowGeometry() {
QRect g = normalGeometry();
if (g.isEmpty()) g = geometry();
postUiStateKv("window_geometry",
QStringLiteral("%1,%2,%3,%4").arg(g.width()).arg(g.height()).arg(g.x()).arg(g.y()));
postUiStateKv("window_maximized", isMaximized() ? QStringLiteral("1") : QStringLiteral("0"));
if (m_splitter) {
const QList<int> sizes = m_splitter->sizes();
if (sizes.size() == 3)
postUiStateKv("window_splitter",
QStringLiteral("%1,%2,%3").arg(sizes[0]).arg(sizes[1]).arg(sizes[2]));
}
}
void MainWindow::restoreWindowGeometry() {
if (!m_db) return;
const QStringList p = m_db->getUiState("window_geometry").split(',');
if (p.size() != 4) return;
int w = p[0].toInt(), h = p[1].toInt(), x = p[2].toInt(), y = p[3].toInt();
if (w <= 0 || h <= 0) return;
QRect target(x, y, w, h);
bool visible = false;
for (const QScreen* s : QGuiApplication::screens()) {
if (s->availableGeometry().intersects(target)) { visible = true; break; }
}
if (!visible) target.moveTo(0, 0);
setGeometry(target);
if (m_db->getUiStateInt("window_maximized", 0))
showMaximized();
if (m_splitter) {
const QStringList sp = m_db->getUiState("window_splitter").split(',');
if (sp.size() == 3) {
int s0 = sp[0].toInt(), s1 = sp[1].toInt(), s2 = sp[2].toInt();
if (s0 > 0 && s1 > 0 && s2 > 0)
m_splitter->setSizes({s0, s1, s2});
}
}
}
void MainWindow::setupNode() {
m_node = new UtunNode(this);
connect(m_node, &UtunNode::error, this, [](const QString& msg) {
@@ -247,12 +309,14 @@ void MainWindow::setupSoundFromConfig() {
QString baseDir = QDir::isAbsolutePath(soundDir) ? soundDir : binDir.absoluteFilePath(soundDir);
auto resolvePath = [&baseDir](const QString& f) { return f.isEmpty() ? QString() : QDir::cleanPath(baseDir + "/" + f); };
struct { const char* name; QString file; } events[] = {
{"msg_incoming", msgInFile}, {"msg_outgoing", msgOutFile},
{"connect", connectFile}, {"disconnect", disconnectFile},
};
for (auto& e : events) { if (!e.file.isEmpty()) sm->loadSound(e.name, resolvePath(e.file)); }
if (msgInFile.isEmpty())
sm->loadSound("msg_incoming", ":/sounds/msg_incoming.mp3");
else
sm->loadSound("msg_incoming", resolvePath(msgInFile));
if (!msgOutFile.isEmpty()) sm->loadSound("msg_outgoing", resolvePath(msgOutFile));
if (!connectFile.isEmpty()) sm->loadSound("connect", resolvePath(connectFile));
if (!disconnectFile.isEmpty()) sm->loadSound("disconnect", resolvePath(disconnectFile));
if (m_db && m_db->isOpen()) {
sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1));
sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f);
@@ -307,6 +371,7 @@ void MainWindow::setupBridgeCallbacks() {
if (!s_mainWindow || !s_mainWindow->m_db) return;
s_mainWindow->m_db->setDb(chat_core_get_db());
s_mainWindow->m_channelList->loadChannels();
s_mainWindow->restoreWindowGeometry();
QString lastCid = s_mainWindow->m_db->getUiState("last_channel_id");
if (!lastCid.isEmpty()) {
/* восстанавливаем канал только если он реально существует в БД —
@@ -402,9 +467,7 @@ void MainWindow::setupTray() {
});
QAction *quitAction = m_trayMenu->addAction("Quit");
connect(quitAction, &QAction::triggered, this, [this]() {
if (m_node) m_node->requestStop();
});
connect(quitAction, &QAction::triggered, this, &MainWindow::requestShutdown);
m_trayIcon->setContextMenu(m_trayMenu);
@@ -416,12 +479,24 @@ void MainWindow::setupTray() {
void MainWindow::closeEvent(QCloseEvent *event) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: closeEvent — hiding to tray");
saveWindowGeometry();
hide();
m_trayIcon->showMessage("vibechat", "Application minimized to tray.",
QSystemTrayIcon::Information, 2000);
event->ignore();
}
void MainWindow::requestShutdown() {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: requestShutdown — saving geometry and stopping node");
saveWindowGeometry();
if (m_node) m_node->requestStop();
}
void MainWindow::onShutdownCheck() {
if (g_signalFlag)
requestShutdown();
}
void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) {
if (reason == QSystemTrayIcon::Trigger ||
reason == QSystemTrayIcon::DoubleClick) {