You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1037 lines
41 KiB
1037 lines
41 KiB
#include "mainwindow.h" |
|
#include "channellist.h" |
|
#include "messagelist.h" |
|
#include "messagedelegate.h" |
|
#include "accountlist.h" |
|
#include "invitedialog.h" |
|
#include "inviteby.h" |
|
#include "joindialog.h" |
|
#include "settingsdialog.h" |
|
#include "channelsettingsdialog.h" |
|
#include "connmonitorwindow.h" |
|
#include "videoplayer_window.h" |
|
#include "imageviewer_window.h" |
|
#include "creategroupdialog.h" |
|
#include "invite_link.h" |
|
#include "sound_manager.h" |
|
#include "voiceplayback.h" |
|
#include "audiorecorder.h" |
|
#include "voicemessageencoder.h" |
|
#include "callwindow.h" |
|
#include "call_audio_engine.h" |
|
#include "ptt_hotkey_manager.h" |
|
|
|
extern "C" { |
|
#include "../../lib/audio_compressor.h" |
|
} |
|
#include "animtimer.h" |
|
#include "../db/db_manager.h" |
|
#include "../transport/utun_node.h" |
|
#include "../transport/gui_bridge.h" |
|
#include <QSplitter> |
|
#include <QCloseEvent> |
|
#include <QApplication> |
|
#include <QScreen> |
|
#include <QClipboard> |
|
#include <QStyle> |
|
#include <QMessageBox> |
|
#include <QDebug> |
|
#include <QFile> |
|
#include <QTextStream> |
|
#include <QDir> |
|
#include <cmath> |
|
#include <QJsonDocument> |
|
#include <QJsonObject> |
|
#include <QRandomGenerator> |
|
#include <csignal> |
|
|
|
extern "C" { |
|
#include "chat/chat_core.h" |
|
#include "call/call.h" |
|
#include "dm/dm_core.h" |
|
#include "../../lib/mem.h" |
|
#include "../../lib/debug_config.h" |
|
void chat_core_attachment_download_trampoline(void* arg); |
|
void chat_core_save_ui_state_trampoline(void* arg); |
|
} |
|
|
|
static MainWindow* s_mainWindow = nullptr; |
|
|
|
#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; |
|
struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg)); |
|
if (!arg) return; |
|
arg->inst = gui_bridge_get_inst(); |
|
memcpy(arg->data, key, klen + 1); |
|
memcpy(arg->data + klen + 1, val.constData(), vlen + 1); |
|
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); |
|
} |
|
|
|
static void onChannelUpdatedCallback(const char* ch_id, int ch_id_len) { |
|
Q_UNUSED(ch_id_len); |
|
Q_UNUSED(ch_id); |
|
if (s_mainWindow) |
|
s_mainWindow->reloadChannels(); |
|
} |
|
|
|
static void onMemberListCallback(const uint8_t* data, int len) { |
|
if (len < 3) return; |
|
uint8_t cl = data[0]; |
|
if (len < 1 + (int)cl + 2) return; |
|
uint16_t count; memcpy(&count, data + 1 + cl, 2); |
|
if (s_mainWindow) |
|
s_mainWindow->onMemberList(QString::fromUtf8((const char*)data + 1, cl), count, data + 1 + cl + 2, len - 1 - cl - 2); |
|
} |
|
|
|
static void onMemberUpdatedCallback(const uint8_t* data, int len) { |
|
if (len < 1) return; |
|
uint8_t cl = data[0]; |
|
if (len < 1 + (int)cl + 1 + 79) return; |
|
if (s_mainWindow) |
|
s_mainWindow->onMemberUpdated(QString::fromUtf8((const char*)data + 1, cl), data + 1 + cl + 1); |
|
} |
|
|
|
static void onMemberRemovedCallback(const uint8_t* data, int len) { |
|
if (len < 1) return; |
|
uint8_t cl = data[0]; |
|
if (len < 1 + (int)cl + 8) return; |
|
uint64_t nid; memcpy(&nid, data + 1 + cl, 8); |
|
if (s_mainWindow) |
|
s_mainWindow->onMemberRemoved(QString::fromUtf8((const char*)data + 1, cl), nid); |
|
} |
|
|
|
static void onAutoConnectStatusCallback(uint8_t status, uint16_t total_tried, |
|
uint16_t node_count, uint16_t connected) { |
|
if (s_mainWindow) |
|
s_mainWindow->onAutoConnectStatus(status, total_tried, node_count, connected); |
|
} |
|
|
|
static void onChannelPeersOnlineCallback(const char* ch_id, int ch_id_len, uint16_t online) { |
|
if (s_mainWindow) |
|
s_mainWindow->onChannelPeersOnline(ch_id, ch_id_len, online); |
|
} |
|
|
|
MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, |
|
const QString& dbPath, const QString& debugFile, |
|
const QString& debugLevel, const QString& debugCategories) |
|
: QMainWindow(parent) |
|
, m_channelList(nullptr), m_messageList(nullptr), m_accountList(nullptr) |
|
, m_trayIcon(nullptr), m_trayMenu(nullptr) |
|
, m_db(db), m_cfgPath(cfgPath), m_cfgDbPath(dbPath) |
|
, m_cfgDebugFile(debugFile), m_cfgDebugLevel(debugLevel) |
|
, m_cfgDebugCategories(debugCategories) |
|
, m_recorder(nullptr) |
|
, m_autoPlayTimer(new QTimer(this)) |
|
{ |
|
setWindowTitle("vibechat"); |
|
resize(900, 600); |
|
if (m_db) m_db->setDbPath(m_cfgDbPath); |
|
setupUi(); |
|
setupTray(); |
|
setupSoundFromConfig(); |
|
if (m_db) { |
|
setupBridgeCallbacks(); |
|
setupMessaging(); |
|
setupNode(); |
|
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() { |
|
s_mainWindow = nullptr; |
|
m_autoPlayTimer->stop(); |
|
gui_bridge_set_msg_received_cb(nullptr); |
|
gui_bridge_set_channel_updated_cb(nullptr); |
|
gui_bridge_set_member_list_cb(nullptr); |
|
gui_bridge_set_member_updated_cb(nullptr); |
|
gui_bridge_set_member_removed_cb(nullptr); |
|
gui_bridge_set_my_node_id_cb(nullptr); |
|
gui_bridge_set_auto_connect_status_cb(nullptr); |
|
gui_bridge_set_channel_peers_online_cb(nullptr); |
|
gui_bridge_set_db_ready_cb(nullptr); |
|
gui_bridge_set_attachment_downloaded_cb(nullptr); |
|
QCoreApplication::processEvents(QEventLoop::AllEvents); |
|
if (m_recorder) { m_recorder->shutdown(); m_recorder = nullptr; } |
|
SoundManager::instance()->shutdown(); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — saving channel state"); |
|
if (m_messageList) m_messageList->saveChannelState(); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — stopping utun node"); |
|
if (m_node) { m_node->stop(); delete m_node; m_node = nullptr; } |
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — node stop+delete done"); |
|
AnimTimer::shutdown(); |
|
MessageList::cleanupAnimations(); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — animations cleaned"); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow done"); |
|
} |
|
|
|
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); |
|
m_messageList = new MessageList(m_db, m_recorder, splitter); |
|
m_accountList = new AccountList(m_db, splitter); |
|
|
|
splitter->addWidget(m_channelList); |
|
splitter->addWidget(m_messageList); |
|
splitter->addWidget(m_accountList); |
|
|
|
splitter->setStretchFactor(0, 2); |
|
splitter->setStretchFactor(1, 5); |
|
splitter->setStretchFactor(2, 1); |
|
|
|
splitter->setSizes({220, 480, 180}); |
|
|
|
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) { |
|
qWarning("UtunNode error: %s", qPrintable(msg)); |
|
}); |
|
connect(m_node, &UtunNode::started, this, [this]() { |
|
qDebug("UtunNode started"); |
|
}); |
|
connect(m_node, &UtunNode::stopped, this, [this]() { |
|
qDebug("UtunNode stopped"); |
|
qApp->quit(); |
|
}); |
|
|
|
if (!m_cfgDebugFile.isEmpty()) |
|
m_node->setDebugFile(m_cfgDebugFile); |
|
if (!m_cfgDebugLevel.isEmpty()) |
|
m_node->setDebugLevel(m_cfgDebugLevel); |
|
if (!m_cfgDebugCategories.isEmpty()) |
|
m_node->setDebugCategories(m_cfgDebugCategories); |
|
|
|
if (!m_cfgDbPath.isEmpty()) |
|
m_node->setDbPath(m_cfgDbPath); |
|
|
|
if (!m_node->start(m_cfgPath)) |
|
qWarning("Failed to start UtunNode"); |
|
else |
|
AnimTimer::instance()->logRegisteredIcons(); |
|
} |
|
|
|
void MainWindow::setupMessaging() { |
|
gui_bridge_init(); |
|
} |
|
|
|
void MainWindow::setupSoundFromConfig() { |
|
SoundManager* sm = SoundManager::instance(); |
|
if (!sm->init()) return; |
|
|
|
QString soundDir, msgInFile, msgOutFile, connectFile, disconnectFile, callInFile; |
|
QFile cfgFile(m_cfgPath); |
|
if (cfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) { |
|
bool inGui = false; |
|
QTextStream in(&cfgFile); |
|
while (!in.atEnd()) { |
|
QString line = in.readLine().trimmed(); |
|
if (line == "[gui]") { inGui = true; continue; } |
|
if (line.startsWith("[") && line.endsWith("]")) inGui = false; |
|
if (!inGui) continue; |
|
if (line.startsWith("sound_dir=")) soundDir = line.mid(10).trimmed(); |
|
else if (line.startsWith("sound_msg_incoming=")) msgInFile = line.mid(19).trimmed(); |
|
else if (line.startsWith("sound_msg_outgoing=")) msgOutFile = line.mid(19).trimmed(); |
|
else if (line.startsWith("sound_connect=")) connectFile = line.mid(14).trimmed(); |
|
else if (line.startsWith("sound_disconnect=")) disconnectFile = line.mid(17).trimmed(); |
|
else if (line.startsWith("sound_call_incoming=")) callInFile = line.mid(20).trimmed(); |
|
} |
|
cfgFile.close(); |
|
} |
|
|
|
if (soundDir.isEmpty()) soundDir = "sounds"; |
|
QDir binDir(QApplication::applicationDirPath()); |
|
QString baseDir = QDir::isAbsolutePath(soundDir) ? soundDir : binDir.absoluteFilePath(soundDir); |
|
auto resolvePath = [&baseDir](const QString& f) { return f.isEmpty() ? QString() : QDir::cleanPath(baseDir + "/" + f); }; |
|
|
|
if (msgInFile.isEmpty()) |
|
sm->loadSound("msg_incoming", ":/sounds/msg_incoming.mp3"); |
|
else |
|
sm->loadSound("msg_incoming", resolvePath(msgInFile)); |
|
|
|
if (callInFile.isEmpty()) |
|
sm->loadSound("call_incoming", ":/sounds/call_incoming.mp3"); |
|
else |
|
sm->loadSound("call_incoming", resolvePath(callInFile)); |
|
|
|
sm->loadSound("call_connecting", ":/sounds/call_connecting.mp3"); |
|
sm->loadSound("call_ringing", ":/sounds/call_ringing.mp3"); |
|
|
|
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); |
|
QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect", |
|
"call_incoming", "call_connecting", "call_ringing"}; |
|
for (int i = 0; i < 7; i++) { |
|
const QString& ek = se[i]; |
|
sm->setEvent(ek, m_db->getUiStateInt(QStringLiteral("sound_%1_on").arg(ek), (i == 0 || i >= 4) ? 1 : 0), |
|
m_db->getUiStateInt(QStringLiteral("sound_%1_vol").arg(ek), 80) / 100.0f); |
|
} |
|
} |
|
|
|
if (m_recorder) { |
|
m_recorder->init(); |
|
if (m_db && m_db->isOpen()) { |
|
m_recorder->setCaptureDeviceIndex(m_db->getUiStateInt("audio_input_device", -1)); |
|
VoiceEncoder::setPreset(m_db->getUiStateInt("opus_codec_preset", 1)); |
|
|
|
int compEnabled = m_db->getUiStateInt("compressor_enabled", 0); |
|
m_recorder->setCompressorEnabled(compEnabled != 0); |
|
if (m_recorder->compressor()) { |
|
audio_compressor_config_t compCfg = {0}; |
|
compCfg.sample_rate = 48000; |
|
compCfg.channels = 1; |
|
compCfg.max_gain_db = (float)m_db->getUiStateInt("compressor_max_gain_db", 25); |
|
compCfg.lookback_ms = 200; |
|
compCfg.lookahead_ms = 100; |
|
compCfg.rise_rate_per_sec = (float)m_db->getUiStateInt("compressor_rise_rate", 10); |
|
compCfg.target_level = 1.0f; |
|
audio_compressor_configure(m_recorder->compressor(), &compCfg); |
|
} |
|
} |
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (captureDevice=%d, opusPreset=%d, compressor=%d)", |
|
m_recorder->captureDeviceIndex(), VoiceEncoder::preset(), |
|
m_recorder->isCompressorEnabled() ? 1 : 0); |
|
} |
|
} |
|
|
|
void MainWindow::setupBridgeCallbacks() { |
|
s_mainWindow = this; |
|
gui_bridge_set_msg_received_cb(onMsgReceivedCallback); |
|
gui_bridge_set_channel_updated_cb(onChannelUpdatedCallback); |
|
gui_bridge_set_member_list_cb(onMemberListCallback); |
|
gui_bridge_set_member_updated_cb(onMemberUpdatedCallback); |
|
gui_bridge_set_member_removed_cb(onMemberRemovedCallback); |
|
gui_bridge_set_my_node_id_cb([](uint64_t nid) { |
|
qDebug("MainWindow: MY_NODE_ID setMyNodeId(%016llx)", (unsigned long long)nid); |
|
if (s_mainWindow && s_mainWindow->m_db) s_mainWindow->m_db->setMyNodeId((quint64)nid); |
|
}); |
|
gui_bridge_set_auto_connect_status_cb(onAutoConnectStatusCallback); |
|
gui_bridge_set_channel_peers_online_cb(onChannelPeersOnlineCallback); |
|
gui_bridge_set_db_ready_cb([]() { |
|
if (!s_mainWindow || !s_mainWindow->m_db) return; |
|
s_mainWindow->m_db->setDb(chat_core_get_db(gui_bridge_get_inst())); |
|
s_mainWindow->m_channelList->loadChannels(); |
|
s_mainWindow->restoreWindowGeometry(); |
|
PttHotkeyManager::instance()->init(s_mainWindow->m_db); /* глобальные PTT-хоткеи */ |
|
QString lastCid = s_mainWindow->m_db->getUiState("last_channel_id"); |
|
if (!lastCid.isEmpty()) { |
|
/* восстанавливаем канал только если он реально существует в БД — |
|
иначе не дёргаем подключение к несуществующей группе */ |
|
bool exists = false; |
|
const auto chs = s_mainWindow->m_db->getChannels(); |
|
for (const auto& ch : chs) { if (ch.channelId == lastCid) { exists = true; break; } } |
|
if (exists) { |
|
s_mainWindow->m_channelList->selectChannel(lastCid); |
|
s_mainWindow->m_accountList->setChannel(lastCid); |
|
s_mainWindow->m_currentChannelId = lastCid; |
|
s_mainWindow->m_messageList->loadChannel(lastCid); |
|
} else { |
|
saveLastChannelId(QString()); |
|
} |
|
} |
|
}); |
|
gui_bridge_set_attachment_downloaded_cb([](const char* ch_id, int ch_id_len, int64_t msg_id) { |
|
if (!s_mainWindow) return; |
|
QString chId = QString::fromUtf8(ch_id, ch_id_len); |
|
s_mainWindow->m_channelList->loadChannels(); |
|
s_mainWindow->m_messageList->onAttachmentDownloaded(chId, msg_id); |
|
s_mainWindow->onAttachmentDownloadedForAutoPlay(chId, msg_id); |
|
}); |
|
gui_bridge_set_download_progress_cb([](const char* ch_id, int ch_id_len, int64_t msg_id, int blocks_done, int num_blocks) { |
|
if (!s_mainWindow) return; |
|
QString chId = QString::fromUtf8(ch_id, ch_id_len); |
|
s_mainWindow->m_messageList->updateMessageProgress(chId, msg_id, blocks_done, num_blocks); |
|
}); |
|
gui_bridge_set_admin_key_received_cb([](const uint8_t* data, int len) { |
|
if (!s_mainWindow || len < 2) return; |
|
uint8_t chLen = data[0]; |
|
if (len < 1 + chLen + 8) return; |
|
QString chId = QString::fromUtf8((const char*)data + 1, chLen); |
|
uint64_t from; memcpy(&from, data + 1 + chLen, 8); |
|
s_mainWindow->m_trayIcon->showMessage( |
|
"vibechat", |
|
QStringLiteral("You are now a channel co-owner.\nReceived admin key for channel %1 from 0x%2") |
|
.arg(chId).arg(QString::number(from, 16)), |
|
QSystemTrayIcon::Information, 5000); |
|
if (s_mainWindow->m_currentChannelId == chId) |
|
s_mainWindow->m_accountList->setChannel(chId); |
|
}); |
|
|
|
/* ── звонки ── */ |
|
gui_bridge_set_call_incoming_cb([](const char* ch_id, int ch_id_len, uint64_t caller, uint64_t call_id) { |
|
if (!s_mainWindow) return; |
|
s_mainWindow->onCallIncoming(QString::fromUtf8(ch_id, ch_id_len), caller, call_id); |
|
}); |
|
gui_bridge_set_call_ringing_cb([](uint64_t call_id) { |
|
if (s_mainWindow) s_mainWindow->onCallRinging(call_id); |
|
}); |
|
gui_bridge_set_call_accepted_cb([](uint64_t call_id) { |
|
if (s_mainWindow) s_mainWindow->onCallAccepted(call_id); |
|
}); |
|
gui_bridge_set_call_ended_cb([](uint64_t call_id, uint8_t reason) { |
|
if (s_mainWindow) s_mainWindow->onCallEnded(call_id, reason); |
|
}); |
|
gui_bridge_set_call_declined_cb([](uint64_t call_id, uint8_t reason) { |
|
if (s_mainWindow) s_mainWindow->onCallEnded(call_id, reason); |
|
}); |
|
gui_bridge_set_call_error_cb([](uint64_t call_id, uint8_t err, const char* text, int len) { |
|
Q_UNUSED(text); Q_UNUSED(len); |
|
if (s_mainWindow) s_mainWindow->onCallError(call_id, err); |
|
}); |
|
gui_bridge_set_call_stats_cb([](uint64_t id, int rtt, int buffer, int tempo, int dropped, |
|
int underruns, int minimum, int maximum, int reserve) { |
|
if (!s_mainWindow) return; |
|
auto* w = s_mainWindow->m_activeCalls.value(id, nullptr); |
|
if (w) w->setAudioStats(rtt, buffer, tempo, dropped, underruns, minimum, maximum, reserve); |
|
}); |
|
gui_bridge_set_call_path_cb([](uint64_t call_id, const char* text, int len) { |
|
if (s_mainWindow) s_mainWindow->onCallPath(call_id, QString::fromUtf8(text, len)); |
|
}); |
|
gui_bridge_set_call_connection_cb([](uint64_t id, uint8_t type, uint64_t nextHop) { |
|
if (!s_mainWindow) return; |
|
auto* w = s_mainWindow->m_activeCalls.value(id, nullptr); |
|
if (w && w->state() != CallWindow::Ended) w->setConnectionType(type, nextHop); |
|
}); |
|
|
|
/* ── рация: индикация говорящего ── */ |
|
gui_bridge_set_radio_talk_cb([](uint64_t group_id, uint64_t src_node_id, uint8_t on) { |
|
if (s_mainWindow && s_mainWindow->m_messageList) |
|
s_mainWindow->m_messageList->onRadioTalk((quint64)group_id, (quint64)src_node_id, on != 0); |
|
}); |
|
|
|
/* ── DM (p2p чат) ── */ |
|
gui_bridge_set_dm_conv_updated_cb([](const char* conv_id, int conv_id_len) { |
|
if (s_mainWindow) |
|
s_mainWindow->onDmConvUpdated(QString::fromUtf8(conv_id, conv_id_len)); |
|
}); |
|
gui_bridge_set_dm_msg_received_cb([](const char* conv_id, int conv_id_len, uint64_t author) { |
|
if (s_mainWindow) |
|
s_mainWindow->onDmMsgReceived(QString::fromUtf8(conv_id, conv_id_len), author); |
|
}); |
|
gui_bridge_set_dm_messages_cb([](const uint8_t* data, int len) { |
|
if (s_mainWindow) |
|
s_mainWindow->m_messageList->onDmMessages(data, len); |
|
}); |
|
gui_bridge_set_attachment_preparation_cb([](const uint8_t* data, int len) { |
|
if (s_mainWindow) s_mainWindow->m_messageList->onAttachmentPreparation(data, len); |
|
}); |
|
} |
|
|
|
void MainWindow::setupConnects() { |
|
connect(m_channelList, &ChannelList::channelSelected, |
|
this, [this](const QString& channelId) { |
|
saveLastChannelId(channelId); |
|
m_currentChannelId = channelId; |
|
m_messageList->loadChannel(channelId); |
|
if (channelId.startsWith(QStringLiteral("dm:"))) { |
|
/* DM: беседа уже создана/обслуживается dm_core */ |
|
m_accountList->setChannel(QString()); |
|
m_channelList->setChannelUnreadCount(channelId, 0); |
|
return; |
|
} |
|
m_accountList->setChannel(channelId); |
|
struct chat_create_auto_arg* arg = (struct chat_create_auto_arg*)u_calloc(1, sizeof(*arg)); |
|
if (arg) { |
|
arg->inst = gui_bridge_get_inst(); |
|
snprintf(arg->name, sizeof(arg->name), "%s", channelId.toUtf8().constData()); |
|
gui_bridge_post_uasync_fn(chat_core_connect_channel_trampoline, arg); |
|
} |
|
}); |
|
connect(m_channelList, &ChannelList::inviteRequested, this, &MainWindow::showShareInviteDialog); |
|
connect(m_channelList, &ChannelList::inviteByLinkRequested, this, &MainWindow::showInviteByLinkDialog); |
|
connect(m_channelList, &ChannelList::joinRequested, this, &MainWindow::showJoinDialog); |
|
connect(m_channelList, &ChannelList::createGroupRequested, this, &MainWindow::onCreateGroupRequested); |
|
connect(m_channelList, &ChannelList::settingsRequested, this, &MainWindow::showSettings); |
|
connect(m_channelList, &ChannelList::connMonitorRequested, this, &MainWindow::showConnMonitor); |
|
connect(m_channelList, &ChannelList::channelSettingsRequested, this, &MainWindow::showChannelSettings); |
|
connect(m_accountList, &AccountList::memberRenamed, |
|
m_messageList, &MessageList::reloadLocalNicks); |
|
connect(m_accountList, &AccountList::callRequested, this, &MainWindow::onCallRequested); |
|
connect(m_accountList, &AccountList::pmRequested, this, &MainWindow::onPmRequested); |
|
connect(m_messageList, &MessageList::readPositionChanged, |
|
this, [this](const QString& channelId, qint64 lastReadMsgId) { |
|
Q_UNUSED(lastReadMsgId); |
|
if (m_channelList && m_db) |
|
m_channelList->setChannelUnreadCount(channelId, |
|
m_db->getUnreadCount(channelId, lastReadMsgId)); |
|
}); |
|
connect(m_messageList, &MessageList::openVideoRequested, this, &MainWindow::openVideo); |
|
connect(m_messageList, &MessageList::openImageRequested, this, &MainWindow::openImage); |
|
} |
|
|
|
/* ── звонки ── */ |
|
|
|
void MainWindow::onCallRequested(const QString& chId, quint64 nodeId, const QString& name) { |
|
quint64 callId = QRandomGenerator::global()->generate64(); |
|
if (!callId) callId = 1; |
|
|
|
struct call_start_arg* a = (struct call_start_arg*)u_malloc(sizeof(*a)); |
|
if (!a) return; |
|
memset(a, 0, sizeof(*a)); |
|
a->inst = gui_bridge_get_inst(); |
|
strncpy(a->ch_id, chId.toUtf8().constData(), sizeof(a->ch_id) - 1); |
|
a->peer_node_id = nodeId; |
|
a->call_id = callId; |
|
gui_bridge_post_uasync_fn(call_start_trampoline, a); |
|
|
|
CallWindow* w = new CallWindow(this); |
|
w->setCallId(callId); |
|
w->setPeerName(name); |
|
w->setState(CallWindow::Outgoing); |
|
connect(w, &CallWindow::closed, this, &MainWindow::closeCallWindow); |
|
m_activeCalls.insert(callId, w); |
|
w->show(); |
|
|
|
SoundManager::instance()->playLoop("call_connecting"); |
|
} |
|
|
|
void MainWindow::onCallIncoming(const QString& chId, quint64 caller, quint64 callId) { |
|
Q_UNUSED(chId); |
|
if (m_activeCalls.contains(callId)) return; |
|
QString name = m_db->getDisplayName(caller); |
|
if (name.isEmpty()) name = QStringLiteral("0x%1").arg(caller, 16, 16, QChar('0')); |
|
|
|
SoundManager::instance()->playLoop("call_incoming"); |
|
if (!isVisible() || isMinimized()) { |
|
m_trayIcon->showMessage("vibechat", QStringLiteral("Incoming call from %1").arg(name), |
|
QSystemTrayIcon::Information, 5000); |
|
} |
|
|
|
CallWindow* w = new CallWindow(this); |
|
w->setCallId(callId); |
|
w->setPeerName(name); |
|
w->setState(CallWindow::Incoming); |
|
connect(w, &CallWindow::closed, this, &MainWindow::closeCallWindow); |
|
m_activeCalls.insert(callId, w); |
|
w->show(); |
|
w->raise(); |
|
w->activateWindow(); |
|
} |
|
|
|
void MainWindow::onCallRinging(quint64 callId) { |
|
/* caller: INVITE доставлен, у пира идёт вызов — фаза 2 (гудки) */ |
|
CallWindow* w = m_activeCalls.value(callId, nullptr); |
|
if (w && w->state() == CallWindow::Outgoing) |
|
w->setState(CallWindow::Ringing); |
|
SoundManager::instance()->playLoop("call_ringing"); |
|
} |
|
|
|
void MainWindow::onCallAccepted(quint64 callId) { |
|
SoundManager::instance()->stopLoop(); |
|
CallWindow* w = m_activeCalls.value(callId, nullptr); |
|
if (w) w->setState(CallWindow::Active); |
|
if (m_db) |
|
CallAudioEngine::instance()->setPlaybackDeviceIndex(m_db->getUiStateInt("audio_output_device", -1)); |
|
CallAudioEngine::instance()->start(callId); |
|
} |
|
|
|
void MainWindow::onCallEnded(quint64 callId, uint8_t reason) { |
|
Q_UNUSED(reason); |
|
/* тон завершения (~450мс) → авто-teardown аудио-движка */ |
|
if (CallAudioEngine::instance()->callId() == callId) |
|
CallAudioEngine::instance()->beginEnd(callId); |
|
closeCallWindow(callId); |
|
} |
|
|
|
void MainWindow::onCallError(quint64 callId, uint8_t err) { |
|
/* пир офлайн / нет маршрута — звонок не состоялся */ |
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow: call error id=%016llx err=%u", (unsigned long long)callId, (unsigned)err); |
|
closeCallWindow(callId); |
|
} |
|
|
|
void MainWindow::onCallPath(quint64 callId, const QString& path) { |
|
CallWindow* w = m_activeCalls.value(callId, nullptr); |
|
if (w) w->setBgpPath(path); |
|
} |
|
|
|
void MainWindow::closeCallWindow(quint64 callId) { |
|
SoundManager::instance()->stopLoop(); |
|
CallWindow* w = m_activeCalls.take(callId); |
|
if (!w) return; |
|
w->disconnect(this); |
|
w->deleteLater(); |
|
} |
|
|
|
/* ── DM (p2p чат) ── */ |
|
|
|
void MainWindow::onPmRequested(const QString& chId, quint64 nodeId, const QString& name) { |
|
struct dm_start_req* r = (struct dm_start_req*)u_malloc(sizeof(*r)); |
|
if (!r) return; |
|
memset(r, 0, sizeof(*r)); |
|
r->inst = gui_bridge_get_inst(); |
|
r->peer_node_id = nodeId; |
|
strncpy(r->source_ch_id, chId.toUtf8().constData(), sizeof(r->source_ch_id) - 1); |
|
strncpy(r->peer_name, name.toUtf8().constData(), sizeof(r->peer_name) - 1); |
|
gui_bridge_post_uasync_fn(dm_start_trampoline, r); |
|
/* открытие беседы произойдёт по CHAT_EVT_DM_CONV_UPDATED (onDmConvUpdated) */ |
|
} |
|
|
|
void MainWindow::onDmConvUpdated(const QString& convId) { |
|
if (!m_channelList) return; |
|
QString key = QStringLiteral("dm:") + convId; |
|
m_channelList->loadChannels(); |
|
m_channelList->selectChannel(key); |
|
} |
|
|
|
void MainWindow::onDmMsgReceived(const QString& convId, quint64 author) { |
|
QString key = QStringLiteral("dm:") + convId; |
|
if (m_currentChannelId == key) { |
|
m_messageList->refresh(); |
|
return; |
|
} |
|
if (author == m_db->myNodeId()) return; |
|
if (m_channelList) |
|
m_channelList->setChannelUnreadCount(key, m_channelList->getChannelUnreadCount(key) + 1); |
|
} |
|
|
|
void MainWindow::setupTray() { |
|
m_trayIcon = new QSystemTrayIcon(this); |
|
m_trayIcon->setIcon(QIcon(":/icons/app_icon.png")); |
|
m_trayIcon->setToolTip("vibechat"); |
|
|
|
m_trayMenu = new QMenu(this); |
|
QAction *settingsAction = m_trayMenu->addAction("Settings"); |
|
connect(settingsAction, &QAction::triggered, this, &MainWindow::showSettings); |
|
|
|
m_trayMenu->addSeparator(); |
|
|
|
QAction *showAction = m_trayMenu->addAction("Show"); |
|
connect(showAction, &QAction::triggered, this, [this]() { |
|
show(); |
|
raise(); |
|
activateWindow(); |
|
}); |
|
|
|
QAction *quitAction = m_trayMenu->addAction("Quit"); |
|
connect(quitAction, &QAction::triggered, this, &MainWindow::requestShutdown); |
|
|
|
m_trayIcon->setContextMenu(m_trayMenu); |
|
|
|
connect(m_trayIcon, &QSystemTrayIcon::activated, |
|
this, &MainWindow::onTrayActivated); |
|
|
|
m_trayIcon->show(); |
|
} |
|
|
|
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() { |
|
#ifndef _WIN32 |
|
if (g_signalFlag) |
|
requestShutdown(); |
|
#endif |
|
} |
|
|
|
void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { |
|
if (reason == QSystemTrayIcon::Trigger || |
|
reason == QSystemTrayIcon::DoubleClick) { |
|
show(); |
|
raise(); |
|
activateWindow(); |
|
} |
|
} |
|
|
|
void MainWindow::showShareInviteDialog(const QString& channelId) { |
|
if (!m_db || !m_db->isOpen()) return; |
|
|
|
ShareInviteDialog dlg(channelId, m_db, this); |
|
dlg.exec(); |
|
} |
|
|
|
void MainWindow::showInviteByLinkDialog(const QString& channelId) { |
|
if (!m_node || !m_node->isRunning()) { |
|
QMessageBox::warning(this, "Error", "Node is not running."); |
|
return; |
|
} |
|
InviteByLinkDialog dlg(channelId, m_node, this); |
|
dlg.exec(); |
|
} |
|
|
|
void MainWindow::showJoinDialog() { |
|
if (!m_node || !m_node->isRunning()) { |
|
QMessageBox::warning(this, "Error", "Node is not running."); |
|
return; |
|
} |
|
|
|
QString autoLink; |
|
QClipboard* cb = QApplication::clipboard(); |
|
QString clipText = cb->text(QClipboard::Clipboard).trimmed(); |
|
if (clipText.startsWith("utun://") && decodeInviteLink(clipText).error.isEmpty()) |
|
autoLink = clipText; |
|
else if (cb->supportsSelection()) { |
|
QString selText = cb->text(QClipboard::Selection).trimmed(); |
|
if (selText.startsWith("utun://") && decodeInviteLink(selText).error.isEmpty()) |
|
autoLink = selText; |
|
} |
|
|
|
JoinDialog dlg(m_node, m_db, this, autoLink); |
|
dlg.exec(); |
|
} |
|
|
|
void MainWindow::showSettings() { |
|
SettingsDialog dlg(m_cfgPath, m_db, m_node, m_recorder, this); |
|
dlg.exec(); |
|
reloadChannels(); |
|
} |
|
|
|
void MainWindow::showConnMonitor() { |
|
if (m_connMonitor) { |
|
m_connMonitor->raise(); |
|
m_connMonitor->activateWindow(); |
|
return; |
|
} |
|
m_connMonitor = new ConnMonitorWindow(nullptr); |
|
connect(m_connMonitor, &QDialog::destroyed, this, [this] { m_connMonitor = nullptr; }); |
|
m_connMonitor->show(); |
|
} |
|
|
|
void MainWindow::openVideo(const QString& filePath) { |
|
if (!m_videoPlayer) { |
|
m_videoPlayer = new VideoPlayerWindow(nullptr); |
|
connect(m_videoPlayer, &VideoPlayerWindow::closed, this, [this] { |
|
m_videoPlayer->deleteLater(); |
|
m_videoPlayer = nullptr; |
|
}); |
|
} else { |
|
m_videoPlayer->raise(); |
|
m_videoPlayer->activateWindow(); |
|
} |
|
m_videoPlayer->playFile(filePath); |
|
} |
|
|
|
void MainWindow::openImage(const QString& filePath) { |
|
if (!m_imageViewer) { |
|
m_imageViewer = new ImageViewerWindow(nullptr); |
|
connect(m_imageViewer, &ImageViewerWindow::closed, this, [this] { |
|
m_imageViewer->deleteLater(); |
|
m_imageViewer = nullptr; |
|
}); |
|
} else { |
|
m_imageViewer->raise(); |
|
m_imageViewer->activateWindow(); |
|
} |
|
m_imageViewer->loadFile(filePath); |
|
m_imageViewer->showFullScreen(); |
|
} |
|
|
|
void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author) { |
|
QString cid = QString::fromUtf8(ch_id, ch_id_len); |
|
if (m_db && author != m_db->myNodeId() |
|
&& m_db->getUiStateInt(QStringLiteral("ch_%1_sound_on_message").arg(cid), 1)) |
|
SoundManager::instance()->play("msg_incoming"); |
|
if (m_messageList) { |
|
if (cid == m_currentChannelId) { |
|
m_messageList->refresh(); |
|
} else if (m_channelList) { |
|
int unread = m_channelList->getChannelUnreadCount(cid) + 1; |
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MainWindow::onMessageReceived increment unread ch=%s %d->%d", |
|
qPrintable(cid), unread - 1, unread); |
|
m_channelList->setChannelUnreadCount(cid, unread); |
|
} |
|
} |
|
if (m_db && m_db->getUiStateInt("channel_sort", 0) == 1) |
|
reloadChannels(); |
|
|
|
if (m_db && m_db->isOpen() && m_db->getUiStateInt(QStringLiteral("ch_%1_auto_play_voice").arg(cid), 0)) { |
|
QTimer::singleShot(100, this, [this, cid]() { autoPlayCheckNewMessage(cid); }); |
|
} |
|
} |
|
|
|
void MainWindow::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) { |
|
m_accountList->onMemberList(chId, count, data, dataLen); |
|
} |
|
|
|
void MainWindow::onMemberUpdated(const QString& chId, const uint8_t* data) { |
|
m_accountList->onMemberUpdated(chId, data); |
|
} |
|
|
|
void MainWindow::onMemberRemoved(const QString& chId, uint64_t nodeId) { |
|
m_accountList->onMemberRemoved(chId, nodeId); |
|
} |
|
|
|
void MainWindow::onAutoConnectStatus(uint8_t status, uint16_t /*total_tried*/, |
|
uint16_t /*node_count*/, uint16_t /*connected*/) { |
|
if (m_channelList) { |
|
m_channelList->setAutoConnectActive(status == 0); |
|
} |
|
} |
|
|
|
void MainWindow::onChannelPeersOnline(const char* ch_id, int ch_id_len, uint16_t online) { |
|
if (m_channelList) { |
|
QString cid = QString::fromUtf8(ch_id, ch_id_len); |
|
m_channelList->setChannelPeersOnline(cid, (int)online); |
|
} |
|
} |
|
|
|
void MainWindow::reloadChannels() { |
|
if (m_channelList) { |
|
m_channelList->loadChannels(); |
|
if (!m_pendingChannelSelect.isEmpty()) { |
|
m_channelList->selectChannel(m_pendingChannelSelect); |
|
m_accountList->setChannel(m_pendingChannelSelect); |
|
m_messageList->loadChannel(m_pendingChannelSelect); |
|
saveLastChannelId(m_pendingChannelSelect); |
|
m_currentChannelId = m_pendingChannelSelect; |
|
m_pendingChannelSelect.clear(); |
|
} |
|
} |
|
} |
|
|
|
void MainWindow::onCreateGroupRequested() { |
|
if (!m_node || !m_node->isRunning()) { |
|
QMessageBox::warning(this, "Error", "Node is not running."); |
|
return; |
|
} |
|
if (!gui_bridge_is_uasync_ready()) { |
|
QMessageBox::warning(this, "Error", "uTun thread is not ready yet. Please wait and try again."); |
|
return; |
|
} |
|
|
|
CreateGroupDialog dlg(this); |
|
if (dlg.exec() != QDialog::Accepted) return; |
|
|
|
QString name = dlg.groupName(); |
|
if (name.isEmpty()) return; |
|
|
|
char* name_copy = u_strdup(name.toUtf8().constData()); |
|
if (name_copy) { |
|
struct chat_create_auto_arg* arg = (struct chat_create_auto_arg*)u_malloc(sizeof(*arg)); |
|
if (arg) { |
|
arg->inst = gui_bridge_get_inst(); |
|
snprintf(arg->name, sizeof(arg->name), "%s", name_copy); |
|
gui_bridge_post_uasync_fn(chat_core_create_channel_auto_trampoline, arg); |
|
} |
|
u_free(name_copy); |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow: create group request posted name=%s", qPrintable(name)); |
|
} |
|
|
|
void MainWindow::showChannelSettings(const QString& channelId) { |
|
ChannelSettingsDialog dlg(channelId, m_db, this); |
|
dlg.exec(); |
|
} |
|
|
|
QString MainWindow::autoPlayMediaDir() const { |
|
if (m_db && m_db->isOpen()) |
|
return m_db->dbPath() + QStringLiteral("/media"); |
|
return QString(); |
|
} |
|
|
|
void MainWindow::autoPlayCheckNewMessage(const QString& channelId) { |
|
if (!m_db || !m_db->isOpen()) return; |
|
auto msgs = m_db->getMessages(channelId, 1); |
|
if (msgs.isEmpty()) return; |
|
const MessageRow& msg = msgs.first(); |
|
if (msg.isOutgoing || msg.contentType != QStringLiteral("audio/opus")) |
|
return; |
|
|
|
AutoPlayItem item; |
|
item.channelId = channelId; |
|
item.msgId = msg.id; |
|
item.downloadStarted = false; |
|
item.durationSec = 0.0f; |
|
|
|
QByteArray data = msg.data; |
|
if (data.startsWith("wf=")) { |
|
QString raw = QString::fromUtf8(data); |
|
int durPos = raw.indexOf(";dur=", 3); |
|
if (durPos > 0) { |
|
int durEnd = raw.indexOf(';', durPos + 5); |
|
if (durEnd > 0) { |
|
int durMs = raw.mid(durPos + 5, durEnd - durPos - 5).toInt(); |
|
item.durationSec = durMs / 1000.0f; |
|
} |
|
} |
|
} |
|
|
|
QJsonObject la = QJsonDocument::fromJson(msg.localAttrs).object(); |
|
QString fp = la.value(QStringLiteral("fp")).toString(); |
|
if (!fp.isEmpty()) { |
|
QString fullPath = autoPlayMediaDir() + QStringLiteral("/") + channelId + QStringLiteral("/") + fp; |
|
if (QFile::exists(fullPath)) { |
|
m_autoPlayQueue.enqueue(item); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: enqueue ch=%s msg=%lld file=%s", |
|
qPrintable(channelId), (long long)msg.id, qPrintable(fullPath)); |
|
if (!m_autoPlayTimer->isActive()) |
|
m_autoPlayTimer->start(200); |
|
if (!SoundManager::instance()->isCurrentPcmPlaying()) |
|
autoPlayTimerTick(); |
|
return; |
|
} |
|
} |
|
|
|
struct attachment_dl_req* req = (struct attachment_dl_req*) |
|
u_malloc(sizeof(struct attachment_dl_req)); |
|
if (!req) return; |
|
memset(req, 0, sizeof(*req)); |
|
req->inst = gui_bridge_get_inst(); |
|
strncpy(req->channel_id, channelId.toUtf8().constData(), sizeof(req->channel_id) - 1); |
|
req->msg_id = msg.id; |
|
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req); |
|
|
|
item.downloadStarted = true; |
|
m_autoPlayQueue.enqueue(item); |
|
m_autoPlayDownloading.insert(msg.id); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: download started ch=%s msg=%lld", |
|
qPrintable(channelId), (long long)msg.id); |
|
if (!m_autoPlayTimer->isActive()) |
|
m_autoPlayTimer->start(200); |
|
} |
|
|
|
void MainWindow::autoPlayTimerTick() { |
|
if (VoicePlayer::isPreparing() || SoundManager::instance()->isCurrentPcmPlaying()) |
|
return; |
|
|
|
while (!m_autoPlayQueue.isEmpty()) { |
|
AutoPlayItem& item = m_autoPlayQueue.head(); |
|
|
|
MessageRow msg = m_db->getMessageById(item.channelId, item.msgId); |
|
if (msg.id == 0) { |
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "auto-play: msg %lld not found in DB, removing from queue", |
|
(long long)item.msgId); |
|
m_autoPlayQueue.dequeue(); |
|
continue; |
|
} |
|
|
|
QJsonObject la = QJsonDocument::fromJson(msg.localAttrs).object(); |
|
QString fp = la.value(QStringLiteral("fp")).toString(); |
|
if (!fp.isEmpty()) { |
|
QString fullPath = autoPlayMediaDir() + QStringLiteral("/") + item.channelId |
|
+ QStringLiteral("/") + fp; |
|
if (QFile::exists(fullPath)) { |
|
QString chId = item.channelId; |
|
qint64 msgId = item.msgId; |
|
float dur = item.durationSec; |
|
m_autoPlayQueue.dequeue(); |
|
m_autoPlayDownloading.remove(msgId); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: playing ch=%s msg=%lld file=%s dur=%.1fs", |
|
qPrintable(chId), (long long)msgId, qPrintable(fullPath), (double)dur); |
|
MessageDelegate::stopAllAutoPlay(); |
|
VoicePlayer::playOpusFile(fullPath, this, [fullPath](float actualDur) { |
|
MessageDelegate::startAutoPlay(fullPath, actualDur); |
|
}); |
|
return; |
|
} |
|
} |
|
|
|
if (!item.downloadStarted && !m_autoPlayDownloading.contains(item.msgId)) { |
|
struct attachment_dl_req* req = (struct attachment_dl_req*) |
|
u_malloc(sizeof(struct attachment_dl_req)); |
|
if (!req) return; |
|
memset(req, 0, sizeof(*req)); |
|
req->inst = gui_bridge_get_inst(); |
|
strncpy(req->channel_id, item.channelId.toUtf8().constData(), sizeof(req->channel_id) - 1); |
|
req->msg_id = item.msgId; |
|
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req); |
|
item.downloadStarted = true; |
|
m_autoPlayDownloading.insert(item.msgId); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: download started ch=%s msg=%lld (from queue)", |
|
qPrintable(item.channelId), (long long)item.msgId); |
|
} |
|
return; |
|
} |
|
|
|
m_autoPlayTimer->stop(); |
|
} |
|
|
|
void MainWindow::onAttachmentDownloadedForAutoPlay(const QString& chId, int64_t msgId) { |
|
(void)chId; |
|
if (m_autoPlayDownloading.remove((qint64)msgId) == 0 && m_autoPlayQueue.isEmpty()) |
|
return; |
|
if (!m_autoPlayTimer->isActive()) |
|
m_autoPlayTimer->start(200); |
|
if (!SoundManager::instance()->isCurrentPcmPlaying()) |
|
autoPlayTimerTick(); |
|
}
|
|
|