diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index 845ae332..592732a3 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -97,7 +97,8 @@ QList DbManager::getChannels(ChannelSortMode sort) const { " FROM channels%s", orderBy); sqlite3_stmt* stmt = prepareOrNull(sql); if (!stmt) return list; - while (sqlite3_step(stmt) == SQLITE_ROW) { + int rc; + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { ChannelRow r; r.channelId = colText(stmt, 0); r.name = colText(stmt, 1); @@ -115,7 +116,7 @@ QList DbManager::getChannels(ChannelSortMode sort) const { r.avatarHash = colBlob(stmt, 13); list.append(r); } - sqlite3_finalize(stmt); + if (!finishRead(stmt, rc)) return {}; for (auto& ch : list) { ch.lastReadMsgId = getUiStateInt64( QStringLiteral("ch_%1_last_read").arg(ch.channelId), 0); @@ -165,11 +166,9 @@ bool DbManager::isChannelMember(const QString& chId, quint64 nodeId) const { const QByteArray sql = QStringLiteral("SELECT 1 FROM \"%1\" WHERE node_id=?").arg(peersTableName(chId)).toUtf8(); sqlite3_stmt* statement = prepareOrNull(sql.constData()); if (!statement) return false; - sqlite3_bind_int64(statement, 1, (sqlite3_int64)nodeId); - const int result = sqlite3_step(statement); - if (result != SQLITE_ROW && result != SQLITE_DONE) GUI_ERROR("channel membership read failed ch=%s rc=%d", qPrintable(chId), result); - sqlite3_finalize(statement); - return result == SQLITE_ROW; + const int bound = sqlite3_bind_int64(statement, 1, (sqlite3_int64)nodeId); + const int result = bound == SQLITE_OK ? sqlite3_step(statement) : bound; + return finishRead(statement, result) && result == SQLITE_ROW; } /* Формирует человекочитаемое тело превью последнего сообщения по content_type и "d"-полю. */ @@ -227,7 +226,8 @@ QString DbManager::getChannelLastMessage(const QString& channelId) const { sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return {}; QString result; - if (sqlite3_step(stmt) == SQLITE_ROW) { + const int rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { quint64 author = (quint64)sqlite3_column_int64(stmt, 0); QByteArray jdata = colBlob(stmt, 1); QString ct = QString::fromUtf8(parseJsonField(jdata, "ct")); @@ -235,8 +235,7 @@ QString DbManager::getChannelLastMessage(const QString& channelId) const { QString nick = getDisplayName(author); result = nick + QStringLiteral(": ") + mediaPreviewBody(ct, d); } - sqlite3_finalize(stmt); - return result; + return finishRead(stmt, rc) ? result : QString(); } /* ── messages (UI) ── */ @@ -265,7 +264,7 @@ static MessageRow readMessage(sqlite3_stmt* stmt, quint64 myId) { /* Окно вокруг оригинала выбирается одним SQL snapshot, включая одинаковые timestamp. */ QList DbManager::getMessages(const QString& chId, int limit, const QByteArray& around) const { QList list; - if (!around.isEmpty() && (around.size() != 32 || limit <= 0)) { + if (limit <= 0 || (!around.isEmpty() && around.size() != 32)) { GUI_ERROR("history: invalid anchor/window bytes=%d limit=%d", int(around.size()), limit); return list; } QString sql = QStringLiteral( @@ -277,19 +276,24 @@ QList DbManager::getMessages(const QString& chId, int limit, const Q "max(0,(SELECT count(*) FROM \"%1\")-?1))").arg(msgTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return list; - sqlite3_bind_int(stmt, 1, limit); - if (!around.isEmpty()) sqlite3_bind_blob(stmt, 2, around.constData(), around.size(), SQLITE_TRANSIENT); - uint64_t myId = 0; - { sqlite3_stmt* ms = prepareOrNull("SELECT node_id FROM local_identity WHERE id=1"); - if (ms) { if (sqlite3_step(ms) == SQLITE_ROW) myId = (uint64_t)sqlite3_column_int64(ms, 0); sqlite3_finalize(ms); } } - int rc; + int rc = sqlite3_bind_int(stmt, 1, limit); + if (rc == SQLITE_OK && !around.isEmpty()) rc = sqlite3_bind_blob(stmt, 2, around.constData(), around.size(), SQLITE_TRANSIENT); + if (rc != SQLITE_OK) { finishRead(stmt, rc); return {}; } + uint64_t myId = m_myNodeId; + if (!myId) { + sqlite3_stmt* identity = prepareOrNull("SELECT node_id FROM local_identity WHERE id=1"); + if (identity) { + const int row = sqlite3_step(identity); + if (row == SQLITE_ROW) myId = (uint64_t)sqlite3_column_int64(identity, 0); + if (!finishRead(identity, row)) { finishRead(stmt, SQLITE_ABORT); return {}; } + } + } while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { list.prepend(readMessage(stmt, myId)); } - if (rc != SQLITE_DONE) GUI_ERROR("history: read failed ch=%s rc=%d", qPrintable(chId), rc); + if (!finishRead(stmt, rc)) return {}; if (!around.isEmpty()) GUI_DEBUG("history: window loaded ch=%s anchor=%s rows=%d limit=%d", qPrintable(chId), around.toHex().constData(), int(list.size()), limit); - sqlite3_finalize(stmt); return list; } @@ -300,13 +304,13 @@ QList DbManager::getMessagesSince(const QString& chId, qint64 sinceI " FROM \"%1\" WHERE id > ? ORDER BY id ASC").arg(msgTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return list; - sqlite3_bind_int64(stmt, 1, sinceId); + int rc = sqlite3_bind_int64(stmt, 1, sinceId); + if (rc != SQLITE_OK) { finishRead(stmt, rc); return {}; } uint64_t myId = m_myNodeId; - while (sqlite3_step(stmt) == SQLITE_ROW) { + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { list.append(readMessage(stmt, myId)); } - sqlite3_finalize(stmt); - return list; + return finishRead(stmt, rc) ? list : QList(); } MessageRow DbManager::getMessageById(const QString& chId, qint64 msgId) const { @@ -316,12 +320,12 @@ MessageRow DbManager::getMessageById(const QString& chId, qint64 msgId) const { " FROM \"%1\" WHERE id = ?").arg(msgTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return m; - sqlite3_bind_int64(stmt, 1, msgId); - if (sqlite3_step(stmt) == SQLITE_ROW) { + int rc = sqlite3_bind_int64(stmt, 1, msgId); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { m = readMessage(stmt, m_myNodeId); } - sqlite3_finalize(stmt); - return m; + return finishRead(stmt, rc) ? m : MessageRow(); } /* ── DM-беседы (UI, read-only) ── */ @@ -334,12 +338,10 @@ MessageRow DbManager::getMessageByKey(const QString& chId, const QByteArray& key "FROM \"%1\" WHERE message_id=?").arg(msgTableName(chId)).toUtf8(); sqlite3_stmt* stmt = prepareOrNull(sql.constData()); if (!stmt) return message; - sqlite3_bind_blob(stmt, 1, key.constData(), key.size(), SQLITE_TRANSIENT); - const int rc = sqlite3_step(stmt); + int rc = sqlite3_bind_blob(stmt, 1, key.constData(), key.size(), SQLITE_TRANSIENT); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) message = readMessage(stmt, m_myNodeId); - else if (rc != SQLITE_DONE) GUI_ERROR("reply: lookup failed ch=%s rc=%d", qPrintable(chId), rc); - sqlite3_finalize(stmt); - return message; + return finishRead(stmt, rc) ? message : MessageRow(); } /* Короткий обычный текст без рекурсивных цитат и служебного описания медиа. */ @@ -378,12 +380,7 @@ QList DbManager::getDmConversations() const { c.generation = (quint64)sqlite3_column_int64(stmt, 6); list.append(c); } - if (rc != SQLITE_DONE) { - DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM list: read failed rc=%d sql=%s", rc, sqlite3_errmsg(m_db)); - list.clear(); - } - sqlite3_finalize(stmt); - return list; + return finishRead(stmt, rc) ? list : QList(); } /* ── peers (UI) ── */ @@ -396,31 +393,21 @@ QMap DbManager::getLocalNicknames(const QString& chId) const { ).arg(peersTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return m; - while (sqlite3_step(stmt) == SQLITE_ROW) + int rc; + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) m.insert((quint64)sqlite3_column_int64(stmt, 0), colText(stmt, 1)); - sqlite3_finalize(stmt); - return m; + return finishRead(stmt, rc) ? m : QMap(); } QString DbManager::getDisplayName(quint64 nodeId) const { - sqlite3_stmt* st = prepareOrNull( - "SELECT display_name FROM accounts WHERE node_id=?"); - if (st) { - sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); - if (sqlite3_step(st) == SQLITE_ROW) { - QString s = colText(st, 0); - sqlite3_finalize(st); - if (!s.isEmpty()) return s; - } else sqlite3_finalize(st); - } - st = prepareOrNull("SELECT name FROM nodes WHERE node_id=?"); - if (st) { - sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); - if (sqlite3_step(st) == SQLITE_ROW) { - QString s = colText(st, 0); - sqlite3_finalize(st); - if (!s.isEmpty()) return s; - } else sqlite3_finalize(st); + for (const char* sql : {"SELECT display_name FROM accounts WHERE node_id=?", "SELECT name FROM nodes WHERE node_id=?"}) { + sqlite3_stmt* st = prepareOrNull(sql); + if (!st) continue; + int rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + QString name = rc == SQLITE_ROW ? colText(st, 0) : QString(); + if (!finishRead(st, rc)) break; + if (!name.isEmpty()) return name; } return QString("node_%1").arg(nodeId); } @@ -428,10 +415,12 @@ QString DbManager::getDisplayName(quint64 nodeId) const { QByteArray DbManager::getNodeEdPub(quint64 nodeId) const { sqlite3_stmt* stmt = prepareOrNull("SELECT ed25519_pubkey FROM nodes WHERE node_id=?"); if (!stmt) return {}; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); + int rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); QByteArray r; - if (sqlite3_step(stmt) == SQLITE_ROW) r = colBlob(stmt, 0); - sqlite3_finalize(stmt); + if (rc == SQLITE_ROW) r = colBlob(stmt, 0); + if (!finishRead(stmt, rc)) return {}; + if (!r.isEmpty() && r.size() != 32) { GUI_ERROR("node signing key has invalid length=%d", int(r.size())); return {}; } return r; } @@ -446,7 +435,8 @@ QList DbManager::getAccounts(bool contactsOnly) const { " FROM accounts ORDER BY display_name"; sqlite3_stmt* stmt = prepareOrNull(sql); if (!stmt) return list; - while (sqlite3_step(stmt) == SQLITE_ROW) { + int rc; + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { AccountRow a; a.nodeId = (quint64)sqlite3_column_int64(stmt, 0); a.displayName = colText(stmt, 1); @@ -455,8 +445,7 @@ QList DbManager::getAccounts(bool contactsOnly) const { a.isContact = sqlite3_column_int(stmt, 4) != 0; list.append(a); } - sqlite3_finalize(stmt); - return list; + return finishRead(stmt, rc) ? list : QList(); } AccountRow DbManager::getAccount(quint64 nodeId) const { @@ -465,16 +454,16 @@ AccountRow DbManager::getAccount(quint64 nodeId) const { "SELECT node_id, display_name, avatar_color, avatar_letter, is_contact" " FROM accounts WHERE node_id=?"); if (!stmt) return a; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); - if (sqlite3_step(stmt) == SQLITE_ROW) { + int rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { a.nodeId = (quint64)sqlite3_column_int64(stmt, 0); a.displayName = colText(stmt, 1); a.avatarColor = colText(stmt, 2); a.avatarLetter = colText(stmt, 3); a.isContact = sqlite3_column_int(stmt, 4) != 0; } - sqlite3_finalize(stmt); - return a; + return finishRead(stmt, rc) ? a : AccountRow(); } /* ── ui_state ── */ @@ -482,11 +471,11 @@ AccountRow DbManager::getAccount(quint64 nodeId) const { QString DbManager::getUiState(const QString& key) const { sqlite3_stmt* stmt = prepareOrNull("SELECT value FROM ui_state WHERE key=?"); if (!stmt) return {}; - sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_TRANSIENT); + int rc = sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_TRANSIENT); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); QString val; - if (sqlite3_step(stmt) == SQLITE_ROW) val = colText(stmt, 0); - sqlite3_finalize(stmt); - return val; + if (rc == SQLITE_ROW) val = colText(stmt, 0); + return finishRead(stmt, rc) ? val : QString(); } int DbManager::getUiStateInt(const QString& key, int defaultVal) const { @@ -510,11 +499,11 @@ int DbManager::getUnreadCount(const QString& channelId, qint64 lastReadMsgId) co "SELECT COUNT(*) FROM \"%1\" WHERE id > ?").arg(msgTableName(channelId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return 0; - sqlite3_bind_int64(stmt, 1, lastReadMsgId); + int rc = sqlite3_bind_int64(stmt, 1, lastReadMsgId); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); int count = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0); - sqlite3_finalize(stmt); - return count; + if (rc == SQLITE_ROW) count = sqlite3_column_int(stmt, 0); + return finishRead(stmt, rc) ? count : 0; } /* ── integrity checks ── */ diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index e06617f1..97c1c313 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -26,6 +26,7 @@ #include #include #include +#include /* Один layout для размеров, отрисовки, выделения и hit-test ссылок. */ static qreal layoutMessageText(QTextLayout& layout, int width, const QString& compactLink = {}) { @@ -92,7 +93,6 @@ struct VoicePlayState { bool loading = false; bool playing = false; bool paused = false; - qint64 startMs = 0; float durationSec = 0.0f; float pausedPosSec = 0.0f; float displayFrac = 0.0f; @@ -111,19 +111,17 @@ static void ensureVoiceTimer() { bool anyActive = false; SoundManager* sm = SoundManager::instance(); for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) { - if (it->loading) { anyActive = true; continue; } - if (!it->playing && !it->paused) { - if (it->displayFrac > 0.001f || it->displayElapsed > 0.001f) anyActive = true; - continue; - } - if (it->paused) { anyActive = true; continue; } - if (!sm->isPcmPlaying(it->playbackId)) { - it->playing = false; - it->paused = false; - anyActive = true; - } else { - anyActive = true; - } + if (it->loading && !VoicePlayer::isPreparing()) it->loading = false; + if (it->playing && !sm->isPcmPlaying(it->playbackId)) it->playing = false; + float elapsed = it->paused ? it->pausedPosSec : 0; + const int rate = sm->pcmSampleRate(it->playbackId); + if (it->playing && rate > 0) elapsed = float(double(sm->pcmCursor(it->playbackId)) / rate); + elapsed = qBound(0.0f, elapsed, it->durationSec); + const float fraction = it->durationSec > 0 ? elapsed / it->durationSec : 0; + const bool moving = std::fabs(fraction - it->displayFrac) > 0.001f || std::fabs(elapsed - it->displayElapsed) > 0.001f; + it->displayFrac = moving ? it->displayFrac + (fraction - it->displayFrac) * 0.15f : fraction; + it->displayElapsed = moving ? it->displayElapsed + (elapsed - it->displayElapsed) * 0.15f : elapsed; + anyActive |= it->loading || it->playing || moving; } if (!anyActive) s_voiceTimer->stop(); if (s_voiceView) s_voiceView->viewport()->update(); @@ -288,7 +286,6 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, float clickFrac = (float)(localPos.x() - L.voiceWaveformRect.x()) / L.voiceWaveformRect.width(); if (clickFrac < 0) clickFrac = 0; if (clickFrac > 1) clickFrac = 1; float targetSec = clickFrac * dur; - qint64 now = QDateTime::currentMSecsSinceEpoch(); if (!st.playing && !st.paused) { /* Start playback then seek */ @@ -300,7 +297,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, MessageDelegate::startAutoPlay(filePath, actualDur, playbackId); float position = clickFrac * actualDur; auto& state = s_voiceStates[filePath]; - state.startMs -= (qint64)(position * 1000); state.pausedPosSec = position; + state.pausedPosSec = position; state.displayFrac = clickFrac; state.displayElapsed = position; int rate = SoundManager::instance()->pcmSampleRate(playbackId); if (rate > 0) SoundManager::instance()->seekPcm(playbackId, (unsigned long long)(position * rate)); @@ -309,7 +306,6 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, s_voiceTimer->start(); } else { /* Currently playing or paused — seek */ - st.startMs = now - (qint64)(targetSec * 1000.0); if (st.paused) st.pausedPosSec = targetSec; else if (!st.playing) @@ -332,8 +328,8 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, if (st.playing) { /* Playing → Pause */ - qint64 now = QDateTime::currentMSecsSinceEpoch(); - st.pausedPosSec = (now - st.startMs) / 1000.0f; + const int rate = SoundManager::instance()->pcmSampleRate(st.playbackId); + st.pausedPosSec = rate > 0 ? float(double(SoundManager::instance()->pcmCursor(st.playbackId)) / rate) : 0; st.playing = false; st.paused = true; SoundManager::instance()->pausePcm(st.playbackId); @@ -345,8 +341,6 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, /* Paused → Resume */ st.paused = false; st.playing = true; - qint64 now = QDateTime::currentMSecsSinceEpoch(); - st.startMs = now - (qint64)(st.pausedPosSec * 1000.0); st.displayFrac = (dur > 0) ? (st.pausedPosSec / dur) : 0; st.displayElapsed = st.pausedPosSec; SoundManager::instance()->resumePcm(st.playbackId); @@ -1108,29 +1102,11 @@ void MessageDelegate::paintBody(QPainter* painter, const QStyleOptionViewItem& o bool isPaused = st->paused; /* compute elapsed/frac for progress */ - float elapsed = 0, targetFrac = 0, durSec = st->durationSec; + float durSec = st->durationSec; if (durSec <= 0) { QString ds = index.data(MsgVoiceDurationRole).toString(); ds.remove('s'); durSec = ds.toFloat(); } - if (isPaused) { - elapsed = st->pausedPosSec; - } else if (isPlaying) { - SoundManager* sm = SoundManager::instance(); - if (sm->isPcmPlaying(st->playbackId)) { - int sr = sm->pcmSampleRate(st->playbackId); - if (sr > 0) elapsed = (float)((double)sm->pcmCursor(st->playbackId) / sr); - } - if (elapsed <= 0) { - qint64 now = QDateTime::currentMSecsSinceEpoch(); - elapsed = (now - st->startMs) / 1000.0f; - } - if (elapsed > durSec) elapsed = durSec; - } - targetFrac = (durSec > 0) ? (elapsed / durSec) : 0; - if (targetFrac < 0) targetFrac = 0; if (targetFrac > 1) targetFrac = 1; - st->displayFrac += (targetFrac - st->displayFrac) * 0.15f; - st->displayElapsed += (elapsed - st->displayElapsed) * 0.15f; float frac = st->displayFrac; QRectF playR = L.voicePlayBtnRect; @@ -1714,14 +1690,14 @@ void MessageDelegate::drawAvatar(QPainter *p, const Layout &L, } void MessageDelegate::startAutoPlay(const QString& filePath, float actualDurationSec, uint64_t playbackId) { - if (actualDurationSec <= 0) return; + if (actualDurationSec <= 0 || !playbackId) { GUI_WARN("voice delegate: invalid playback duration/id"); return; } ensureVoiceTimer(); + s_voiceStates.clear(); VoicePlayState& st = s_voiceStates[filePath]; st.playbackId = playbackId; st.playing = true; st.paused = false; st.durationSec = (actualDurationSec > 0) ? actualDurationSec : 1.0f; - st.startMs = QDateTime::currentMSecsSinceEpoch(); st.pausedPosSec = 0.0f; st.displayFrac = 0.0f; st.displayElapsed = 0.0f;