This commit is contained in:
defnax 2026-09-07 11:40:13 -07:00 committed by GitHub
commit b1ddfb6e67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 714 additions and 21 deletions

View File

@ -39,6 +39,9 @@
#endif
#include "msgs/MessageComposer.h"
#include "Posted/PostedDialog.h"
#ifdef RS_USE_WIKI
#include "WikiPoos/WikiDialog.h"
#endif
#include "util/misc.h"
#include <retroshare/rsfiles.h>
@ -79,9 +82,10 @@
#define HOST_FILE_TREE "collection"
#define HOST_CHAT_ROOM "chat_room"
#define HOST_CIRCLE "circle"
#define HOST_REGEXP "file|person|forum|channel|wire|search|message|certificate|extra|private_chat|public_msg|posted|identity|collection|chat_room|circle"
#define HOST_REGEXP "file|person|forum|channel|wire|search|message|certificate|extra|private_chat|public_msg|posted|identity|collection|chat_room|circle|wiki"
#define HOST_WIRE "wire"
#define HOST_WIKI "wiki"
#define FILE_NAME "name"
#define FILE_SIZE "size"
#define FILE_HASH "hash"
@ -102,6 +106,10 @@
#define WIRE_ID "id"
#define WIRE_MSGID "msgid"
#define WIKI_NAME "name"
#define WIKI_ID "id"
#define WIKI_MSGID "msgid"
#define SEARCH_KEYWORDS "keywords"
#define MESSAGE_ID "id"
@ -315,6 +323,19 @@ void RetroShareLink::fromUrl(const QUrl& url)
return;
}
if (url.host() == HOST_WIKI) {
_type = TYPE_WIKI;
_name = decodedQueryItemValue(urlQuery, WIKI_NAME);
_hash = urlQuery.queryItemValue(WIKI_ID);
_msgId = urlQuery.queryItemValue(WIKI_MSGID);
#ifdef DEBUG_RSLINK
std::cerr << "Got a wiki link!!" << std::endl;
#endif
check();
return;
}
if (url.host() == HOST_SEARCH) {
_type = TYPE_SEARCH;
_name = decodedQueryItemValue(urlQuery, SEARCH_KEYWORDS);
@ -857,6 +878,17 @@ void RetroShareLink::check()
_valid = false;
break;
case TYPE_WIKI:
if(_size != 0)
_valid = false;
if(_name.isEmpty())
_valid = false;
if(_hash.isEmpty())
_valid = false;
break;
case TYPE_SEARCH:
if(_size != 0)
_valid = false;
@ -963,6 +995,8 @@ QString RetroShareLink::title() const
return QString("Channel id: %1").arg(hash());
case TYPE_WIRE:
return QString("Wire id: %1").arg(hash());
case TYPE_WIKI:
return QString("Wiki id: %1").arg(hash());
case TYPE_SEARCH:
return QString("Search files");
@ -1172,6 +1206,17 @@ QString RetroShareLink::toString() const
break;
case TYPE_WIKI:
url.setScheme(RSLINK_SCHEME);
url.setHost(HOST_WIKI);
urlQuery.addQueryItem(WIKI_NAME, encodeItem(_name));
urlQuery.addQueryItem(WIKI_ID, _hash);
if (!_msgId.isEmpty()) {
urlQuery.addQueryItem(WIKI_MSGID, _msgId);
}
break;
}
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
@ -1418,6 +1463,7 @@ static void processList(const QStringList &list, const QString &textSingular, co
case TYPE_FILE_TREE:
case TYPE_CHAT_ROOM:
case TYPE_CIRCLES:
case TYPE_WIKI:
// no need to ask
break;
@ -1487,12 +1533,18 @@ static void processList(const QStringList &list, const QString &textSingular, co
QStringList chatroomFound;
QStringList chatroomUnknown;
// wiki
QStringList wikiFound;
QStringList wikiMsgFound;
QStringList wikiUnknown;
QStringList wikiMsgUnknown;
// for summary at end
QList<QStringList*> processedList;
QList<QStringList*> errorList;
processedList << &fileAdded << &personAdded << &forumFound << &channelFound << &wireFound << &searchStarted << &messageStarted << &postedFound << &chatroomFound;
errorList << &fileExist << &personExist << &personFailed << &personNotFound << &forumUnknown << &forumMsgUnknown << &channelUnknown << &channelMsgUnknown << &wireUnknown << &wireMsgUnknown << &messageReceipientNotAccepted << &messageReceipientUnknown << &postedUnknown << &postedMsgUnknown << &chatroomUnknown;
processedList << &fileAdded << &personAdded << &forumFound << &channelFound << &wireFound << &searchStarted << &messageStarted << &postedFound << &chatroomFound << &wikiFound;
errorList << &fileExist << &personExist << &personFailed << &personNotFound << &forumUnknown << &forumMsgUnknown << &channelUnknown << &channelMsgUnknown << &wireUnknown << &wireMsgUnknown << &messageReceipientNotAccepted << &messageReceipientUnknown << &postedUnknown << &postedMsgUnknown << &chatroomUnknown << &wikiUnknown << &wikiMsgUnknown;
// not needed: forumFound, channelFound, wireFound, messageStarted
// we want to merge all single file links into one collection
@ -1933,6 +1985,36 @@ static void processList(const QStringList &list, const QString &textSingular, co
}
break;
#ifdef RS_USE_WIKI
case TYPE_WIKI:
{
#ifdef DEBUG_RSLINK
std::cerr << " RetroShareLink::process WikiRequest : name : " << link.name().toStdString() << ". id : " << link.hash().toStdString() << ". msgId : " << link.msgId().toStdString() << std::endl;
#endif
MainWindow::showWindow(MainWindow::Wiki);
WikiDialog *wikiDialog = dynamic_cast<WikiDialog*>(MainWindow::getPage(MainWindow::Wiki));
if (!wikiDialog) {
return false;
}
if (wikiDialog->navigate(RsGxsGroupId(link.id().toStdString()), RsGxsMessageId(link.msgId().toStdString()))) {
if (link.msgId().isEmpty()) {
wikiFound.append(link.name());
} else {
wikiMsgFound.append(link.name());
}
} else {
if (link.msgId().isEmpty()) {
wikiUnknown.append(link.name());
} else {
wikiMsgUnknown.append(link.name());
}
}
}
break;
#endif
default:
std::cerr << " RetroShareLink::process unknown type: " << link.type() << std::endl;
++countUnknown;
@ -2049,6 +2131,16 @@ static void processList(const QStringList &list, const QString &textSingular, co
}
}
// wiki
if (flag & RSLINK_PROCESS_NOTIFY_ERROR) {
if (!wikiUnknown.isEmpty()) {
processList(wikiUnknown, QObject::tr("Wiki not found"), QObject::tr("Wikis not found"), result);
}
if (!wikiMsgUnknown.isEmpty()) {
processList(wikiMsgUnknown, QObject::tr("Wiki page not found"), QObject::tr("Wiki pages not found"), result);
}
}
// chat room
if (flag & RSLINK_PROCESS_NOTIFY_ERROR) {
if (!chatroomUnknown.isEmpty()) {

View File

@ -77,7 +77,8 @@ class RetroShareLink
TYPE_FILE_TREE = 0x0d,
TYPE_CHAT_ROOM = 0x0e,
TYPE_WIRE = 0x0f,
TYPE_CIRCLES = 0x10
TYPE_CIRCLES = 0x10,
TYPE_WIKI = 0x11
};
public:

View File

@ -31,12 +31,14 @@
#include "WikiDialog.h"
#include "WikiUserNotify.h"
#include "gui/RetroShareLink.h"
#include "gui/WikiPoos/WikiAddDialog.h"
#include "gui/WikiPoos/WikiEditDialog.h"
#include "gui/settings/rsharesettings.h"
#include "gui/gxs/WikiGroupDialog.h"
#include "gui/gxs/GxsCommentTreeWidget.h"
#include "gui/gxs/GxsIdDetails.h"
#include "gui/common/RSTreeWidget.h"
#include "util/DateTime.h"
#include "util/qtthreadsutils.h"
@ -53,6 +55,8 @@
#include <iostream>
#include <sstream>
#include <ctime>
#include <chrono>
#include <thread>
#include <QTimer>
@ -107,6 +111,9 @@ WikiDialog::WikiDialog(QWidget *parent) :
/* Invoke the Qt Designer generated object setup routine */
ui.setupUi(this);
mNavigatePendingPageId.clear();
mShouldUpdateGroupStatistics = false;
mLastGroupStatisticsUpdateTs = 0;
mAddPageDialog = NULL;
mAddGroupDialog = NULL;
mEditDialog = NULL;
@ -170,6 +177,8 @@ WikiDialog::WikiDialog(QWidget *parent) :
}, this );
},
mEventHandlerId, wikiEventType);
mFontSizeHandler.registerFontSize(ui.groupTreeWidget->treeWidget(), 1.2f);
}
WikiDialog::~WikiDialog()
@ -409,6 +418,11 @@ void WikiDialog::pagesTreeCustomPopupMenu(QPoint point)
contextMenu.addSeparator();
QAction *copyLinkAction = contextMenu.addAction(QIcon(IMAGE_COPYLINK), tr("Copy RetroShare Link"), this, SLOT(copyWikiPageLink()));
copyLinkAction->setEnabled(hasSelection);
contextMenu.addSeparator();
QAction *showPageIdAction = contextMenu.addAction(tr("Show Page Id"));
showPageIdAction->setCheckable(true);
showPageIdAction->setChecked(!ui.treeWidget_Pages->isColumnHidden(WIKI_GROUP_COL_PAGEID));
@ -631,6 +645,10 @@ void WikiDialog::loadGroupMeta()
if (groupMeta.size() > 0)
{
insertGroupsData(groupMeta);
for (const auto &meta : groupMeta)
{
updateGroupStatistics(meta.mGroupId);
}
if (!mGroupId.isNull())
{
const int subscribeFlags = ui.groupTreeWidget->subscribeFlags(
@ -709,6 +727,16 @@ void WikiDialog::loadPages(const RsGxsGroupId &groupId)
ui.treeWidget_Pages->addTopLevelItem(pageItem);
}
if (!mNavigatePendingPageId.isNull())
{
QTreeWidgetItem *item = findPageItem(mNavigatePendingPageId);
if (item)
{
ui.treeWidget_Pages->setCurrentItem(item);
mNavigatePendingPageId.clear();
}
}
}, this);
});
}
@ -881,10 +909,18 @@ void WikiDialog::groupListCustomPopupMenu(QPoint /*point*/)
//restoreKeysAct->setEnabled(!mForumId.empty() && !IS_GROUP_ADMIN(subscribeFlags));
//contextMnu.addAction( restoreKeysAct);
//action = contextMnu.addAction(QIcon(IMAGE_COPYLINK), tr("Copy RetroShare Link"), this, SLOT(copyForumLink()));
//action->setEnabled(!mForumId.empty());
contextMnu.addSeparator();
//contextMnu.addSeparator();
action = contextMnu.addAction(QIcon(IMAGE_COPYLINK), tr("Copy RetroShare Link"), this, SLOT(copyWikiGroupLink()));
action->setEnabled(!mGroupId.isNull());
contextMnu.addSeparator();
QAction *markReadAction = contextMnu.addAction(QIcon(":/images/message-mail-read.png"), tr("Mark all as read"), this, SLOT(markGroupAsRead()));
markReadAction->setEnabled(!mGroupId.isNull() && IS_GROUP_SUBSCRIBED(subscribeFlags));
QAction *markUnreadAction = contextMnu.addAction(QIcon(":/images/message-mail.png"), tr("Mark all as unread"), this, SLOT(markGroupAsUnread()));
markUnreadAction->setEnabled(!mGroupId.isNull() && IS_GROUP_SUBSCRIBED(subscribeFlags));
contextMnu.exec(QCursor::pos());
}
@ -982,6 +1018,8 @@ void WikiDialog::handleEvent_main_thread(std::shared_ptr<const RsEvent> event)
#endif
switch(e->mWikiEventCode) {
case RsWikiEventCode::UNKNOWN:
break;
case RsWikiEventCode::UPDATED_COLLECTION:
case RsWikiEventCode::NEW_COLLECTION:
updateDisplay(); // Refresh global list
@ -990,6 +1028,7 @@ void WikiDialog::handleEvent_main_thread(std::shared_ptr<const RsEvent> event)
int subscribeFlags = ui.groupTreeWidget->subscribeFlags(QString::fromStdString(mGroupId.toStdString()));
updateModerationState(mGroupId, subscribeFlags);
}
updateGroupStatistics(e->mWikiGroupId);
break;
case RsWikiEventCode::UPDATED_SNAPSHOT:
case RsWikiEventCode::NEW_SNAPSHOT:
@ -997,6 +1036,7 @@ void WikiDialog::handleEvent_main_thread(std::shared_ptr<const RsEvent> event)
if (e->mWikiGroupId == mGroupId) {
wikiGroupChanged(QString::fromStdString(mGroupId.toStdString()));
}
updateGroupStatistics(e->mWikiGroupId);
break;
case RsWikiEventCode::SUBSCRIBE_STATUS_CHANGED:
// Refresh group list to update subscription status
@ -1006,7 +1046,11 @@ void WikiDialog::handleEvent_main_thread(std::shared_ptr<const RsEvent> event)
int subscribeFlags = ui.groupTreeWidget->subscribeFlags(QString::fromStdString(mGroupId.toStdString()));
updateModerationState(mGroupId, subscribeFlags);
}
updateGroupStatistics(e->mWikiGroupId);
break;
case RsWikiEventCode::READ_STATUS_CHANGED:
updateGroupStatistics(e->mWikiGroupId);
break;
case RsWikiEventCode::NEW_COMMENT:
// Refresh comments if the event is for the currently displayed page
if (e->mWikiGroupId == mCurrentGroupId && !mCurrentPageId.isNull()) {
@ -1060,3 +1104,192 @@ void WikiDialog::loadComments(const RsGxsGroupId &groupId, const RsGxsMessageId
mCommentTreeWidget->requestComments(groupId, msgVersions, msgId);
}
}
bool WikiDialog::navigate(const RsGxsGroupId &groupId, const RsGxsMessageId &pageId)
{
if (groupId.isNull())
{
return false;
}
mGroupId = groupId;
ui.groupTreeWidget->activateId(QString::fromStdString(groupId.toStdString()), true);
if (!pageId.isNull())
{
mNavigatePendingPageId = pageId;
QTreeWidgetItem *item = findPageItem(pageId);
if (item)
{
ui.treeWidget_Pages->setCurrentItem(item);
mNavigatePendingPageId.clear();
}
}
else
{
mNavigatePendingPageId.clear();
}
return true;
}
void WikiDialog::copyWikiGroupLink()
{
if (mGroupId.isNull())
{
return;
}
QString groupName;
ui.groupTreeWidget->getGroupName(QString::fromStdString(mGroupId.toStdString()), groupName);
RetroShareLink link = RetroShareLink::createGxsGroupLink(RetroShareLink::TYPE_WIKI, mGroupId, groupName);
QList<RetroShareLink> links;
links.append(link);
RSLinkClipboard::copyLinks(links);
}
void WikiDialog::copyWikiPageLink()
{
RsGxsGroupId groupId;
RsGxsMessageId pageId;
RsGxsMessageId origPageId;
if (!getSelectedPage(groupId, pageId, origPageId))
{
return;
}
QTreeWidgetItem *item = ui.treeWidget_Pages->currentItem();
if (!item)
{
return;
}
QString pageName = item->text(WIKI_GROUP_COL_PAGENAME);
RetroShareLink link = RetroShareLink::createGxsMessageLink(RetroShareLink::TYPE_WIKI, groupId, pageId, pageName);
QList<RetroShareLink> links;
links.append(link);
RSLinkClipboard::copyLinks(links);
}
void WikiDialog::paintEvent(QPaintEvent *pe)
{
rstime_t now = time(nullptr);
if (mShouldUpdateGroupStatistics && now > 2 + mLastGroupStatisticsUpdateTs)
{
for (auto& groupId : mGroupStatisticsToUpdate)
{
updateGroupStatisticsReal(groupId);
}
mShouldUpdateGroupStatistics = false;
mLastGroupStatisticsUpdateTs = time(nullptr);
mGroupStatisticsToUpdate.clear();
}
MainPage::paintEvent(pe);
}
void WikiDialog::updateGroupStatistics(const RsGxsGroupId &groupId)
{
mGroupStatisticsToUpdate.insert(groupId);
mShouldUpdateGroupStatistics = true;
update(); // Trigger paint event
}
void WikiDialog::updateGroupStatisticsReal(const RsGxsGroupId &groupId)
{
RsThread::async([this, groupId]()
{
GxsGroupStatistic stats;
bool ok = false;
uint32_t token;
if (rsWiki->requestGroupStatistic(token, groupId))
{
auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(10);
auto st = rsWiki->requestStatus(token);
while (!(st == RsTokenService::FAILED || st >= RsTokenService::COMPLETE)
&& std::chrono::steady_clock::now() < timeout)
{
std::this_thread::sleep_for(std::chrono::milliseconds(50));
st = rsWiki->requestStatus(token);
}
if (st == RsTokenService::COMPLETE)
{
ok = rsWiki->getGroupStatistic(token, stats);
}
else
{
rsWiki->cancelRequest(token);
}
}
if (!ok)
{
std::cerr << "WikiDialog::updateGroupStatisticsReal failed to collect group statistics for group " << groupId << std::endl;
return;
}
RsQThreadUtils::postToObject([this, stats, groupId]()
{
QTreeWidgetItem *item = ui.groupTreeWidget->getItemFromId(QString::fromStdString(stats.mGrpId.toStdString()));
if (item)
{
ui.groupTreeWidget->setCounts(item, stats.mNumThreadMsgsUnread, stats.mNumMsgs);
}
mCachedGroupStats[groupId] = stats;
getUserNotify()->updateIcon();
}, this);
});
}
void WikiDialog::markGroupAsRead()
{
markAllGroupMessagesRead(true);
}
void WikiDialog::markGroupAsUnread()
{
markAllGroupMessagesRead(false);
}
void WikiDialog::markAllGroupMessagesRead(bool read)
{
if (mGroupId.isNull())
{
return;
}
RsGxsGroupId groupId = mGroupId;
QPointer<WikiDialog> self(this);
RsThread::async([self, groupId, read]()
{
if (!rsWiki)
{
return;
}
std::vector<RsWikiSnapshot> snapshots;
if (rsWiki->getSnapshots(groupId, snapshots))
{
for (const auto &page : snapshots)
{
rsWiki->setMessageReadStatus(RsGxsGrpMsgIdPair(groupId, page.mMeta.mMsgId), read);
}
}
RsQThreadUtils::postToObject([self, groupId]()
{
if (self)
{
if (self->mGroupId == groupId)
{
self->loadPages(groupId);
}
self->updateDisplay();
self->updateGroupStatistics(groupId);
}
}, self);
});
}

View File

@ -52,8 +52,11 @@ public:
virtual UserNotify *createUserNotify(QObject *parent) override;
bool navigate(const RsGxsGroupId &groupId, const RsGxsMessageId &pageId);
protected:
virtual void showEvent(QShowEvent *event) override;
virtual void paintEvent(QPaintEvent *pe) override;
private slots:
void updateDisplay();
@ -90,6 +93,12 @@ private slots:
// Comments (placeholder for future UI integration)
void loadComments(const RsGxsGroupId &groupId, const RsGxsMessageId &msgId);
void copyWikiGroupLink();
void copyWikiPageLink();
void markGroupAsRead();
void markGroupAsUnread();
private:
void clearWikiPage();
@ -99,6 +108,10 @@ private:
void setSelectedPageReadStatus(bool read);
QTreeWidgetItem *findPageItem(const RsGxsMessageId &pageId) const;
void markAllGroupMessagesRead(bool read);
void updateGroupStatistics(const RsGxsGroupId &groupId);
void updateGroupStatisticsReal(const RsGxsGroupId &groupId);
bool getSelectedPage(RsGxsGroupId &groupId, RsGxsMessageId &pageId, RsGxsMessageId &origPageId);
std::string getSelectedPage();
const RsGxsGroupId &getSelectedGroup();
@ -138,6 +151,14 @@ private:
RsGxsGroupId mGroupId; // From GroupTreeWidget
bool mCanModerate = false;
bool mInitialLoadDone = false;
RsGxsMessageId mNavigatePendingPageId;
std::set<RsGxsGroupId> mGroupStatisticsToUpdate;
bool mShouldUpdateGroupStatistics = false;
rstime_t mLastGroupStatisticsUpdateTs = 0;
std::map<RsGxsGroupId, GxsGroupStatistic> mCachedGroupStats;
FontSizeHandler mFontSizeHandler;
/* UI - from Designer */
Ui::WikiDialog ui;

View File

@ -46,7 +46,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>240</width>
<width>302</width>
<height>510</height>
</rect>
</property>
@ -315,7 +315,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>511</width>
<width>449</width>
<height>480</height>
</rect>
</property>
@ -366,7 +366,11 @@
<number>0</number>
</property>
<item>
<widget class="QTextBrowser" name="textBrowser"/>
<widget class="QTextBrowser" name="textBrowser">
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>

View File

@ -21,11 +21,19 @@
#include <QDateTime>
#include <QMessageBox>
#include <QTreeWidgetItemIterator>
#include <QToolButton>
#include <QHBoxLayout>
#include <QTextBlock>
#include <QTextCursor>
#include <QFileDialog>
#include <QFile>
#include <QFileInfo>
#include "gui/gxs/GxsIdTreeWidgetItem.h"
#include "gui/WikiPoos/WikiEditDialog.h"
#include "util/DateTime.h"
#include "util/qtthreadsutils.h"
#include "util/HandleRichText.h"
#include <retroshare/rsidentity.h>
#include "util/rstime.h"
@ -96,6 +104,16 @@ WikiEditDialog::WikiEditDialog(QWidget *parent)
ui.pushButton_Preview->setIcon(FilesDefs::getIconFromQtResourcePath(QString(":/icons/png/search.png")));
ui.pushButton_History->setIcon(FilesDefs::getIconFromQtResourcePath(QString(":/icons/png/history-clock-white.png")));
connect(ui.btnHeading, SIGNAL(clicked()), this, SLOT(onMarkdownHeading()));
connect(ui.btnBold, SIGNAL(clicked()), this, SLOT(onMarkdownBold()));
connect(ui.btnItalic, SIGNAL(clicked()), this, SLOT(onMarkdownItalic()));
connect(ui.btnQuote, SIGNAL(clicked()), this, SLOT(onMarkdownQuote()));
connect(ui.btnCode, SIGNAL(clicked()), this, SLOT(onMarkdownCode()));
connect(ui.btnLink, SIGNAL(clicked()), this, SLOT(onMarkdownLink()));
connect(ui.btnImage, SIGNAL(clicked()), this, SLOT(onMarkdownImage()));
connect(ui.btnUnorderedList, SIGNAL(clicked()), this, SLOT(onMarkdownUnorderedList()));
connect(ui.btnOrderedList, SIGNAL(clicked()), this, SLOT(onMarkdownOrderedList()));
ui.checkBox_OldHistory->setChecked(false);
mOldHistoryEnabled = false;
ui.groupBox_History->hide();
@ -1184,3 +1202,169 @@ QTreeWidgetItem *WikiEditDialog::findHistoryItem(const RsGxsMessageId &msgId) co
return nullptr;
}
void WikiEditDialog::insertMarkdownWrap(const QString &prefix, const QString &suffix)
{
QTextCursor cursor = ui.textEdit->textCursor();
if (cursor.hasSelection()) {
QString selectedText = cursor.selectedText();
cursor.insertText(prefix + selectedText + suffix);
} else {
cursor.insertText(prefix + suffix);
// Move cursor inside the wrapper
cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, suffix.length());
ui.textEdit->setTextCursor(cursor);
}
ui.textEdit->setFocus();
}
void WikiEditDialog::insertMarkdownBlockPrefix(const QString &prefix)
{
QTextCursor cursor = ui.textEdit->textCursor();
cursor.beginEditBlock();
int startPos = cursor.selectionStart();
int endPos = cursor.selectionEnd();
QTextBlock startBlock = ui.textEdit->document()->findBlock(startPos);
QTextBlock endBlock = ui.textEdit->document()->findBlock(endPos);
QTextBlock block = startBlock;
while (block.isValid()) {
QTextCursor blockCursor(block);
blockCursor.movePosition(QTextCursor::StartOfBlock);
blockCursor.insertText(prefix);
if (block == endBlock) {
break;
}
block = block.next();
}
cursor.endEditBlock();
ui.textEdit->setFocus();
}
void WikiEditDialog::onMarkdownHeading()
{
insertMarkdownBlockPrefix("### ");
}
void WikiEditDialog::onMarkdownBold()
{
insertMarkdownWrap("**", "**");
}
void WikiEditDialog::onMarkdownItalic()
{
insertMarkdownWrap("*", "*");
}
void WikiEditDialog::onMarkdownQuote()
{
insertMarkdownBlockPrefix("> ");
}
void WikiEditDialog::onMarkdownCode()
{
insertMarkdownWrap("`", "`");
}
void WikiEditDialog::onMarkdownLink()
{
QTextCursor cursor = ui.textEdit->textCursor();
if (cursor.hasSelection()) {
QString selectedText = cursor.selectedText();
cursor.insertText("[" + selectedText + "](http://)");
// Position cursor inside parentheses (right after http://)
cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1);
ui.textEdit->setTextCursor(cursor);
} else {
cursor.insertText("[text](http://)");
// Position cursor inside parentheses (right after http://)
cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 8);
ui.textEdit->setTextCursor(cursor);
}
ui.textEdit->setFocus();
}
void WikiEditDialog::onMarkdownImage()
{
QString fileName = QFileDialog::getOpenFileName(this,
tr("Select Image to Embed"), "",
tr("Images (*.png *.jpg *.jpeg *.gif *.bmp);;All Files (*)"));
if (fileName.isEmpty()) {
return;
}
// Try to optimize, scale, and compress the image using RsHtml::makeEmbeddedImage
QString encodedImage;
static const uint32_t MAX_ALLOWED_GXS_MESSAGE_SIZE = 199000;
if (RsHtml::makeEmbeddedImage(fileName, encodedImage, 640 * 480, MAX_ALLOWED_GXS_MESSAGE_SIZE - 200)) {
int srcStart = encodedImage.indexOf("src=\"");
if (srcStart != -1) {
srcStart += 5;
int srcEnd = encodedImage.indexOf("\"", srcStart);
if (srcEnd != -1) {
QString src = encodedImage.mid(srcStart, srcEnd - srcStart);
QTextCursor cursor = ui.textEdit->textCursor();
QString altText = cursor.selectedText();
if (altText.isEmpty()) {
altText = "image";
}
cursor.insertText(QString("![%1](%2)").arg(altText, src));
ui.textEdit->setFocus();
return;
}
}
}
// Fallback to raw unoptimized embedding if makeEmbeddedImage fails
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, tr("Error"), tr("Could not open image file."));
return;
}
QByteArray data = file.readAll();
if (data.size() > 150 * 1024) {
QMessageBox::StandardButton reply = QMessageBox::warning(this,
tr("Image Too Large"),
tr("The selected image is %1 KB. Wiki pages have a total message limit of 200 KB, "
"and base64 encoding will increase this size further.\n\n"
"Are you sure you want to embed this image anyway?").arg(data.size() / 1024),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::No) {
return;
}
}
QFileInfo fileInfo(fileName);
QString ext = fileInfo.suffix().toLower();
if (ext == "jpg") ext = "jpeg";
QString mimeType = "image/" + ext;
QString base64Data = QString::fromLatin1(data.toBase64());
QTextCursor cursor = ui.textEdit->textCursor();
QString altText = cursor.selectedText();
if (altText.isEmpty()) {
altText = "image";
}
cursor.insertText(QString("![%1](data:%2;base64,%3)")
.arg(altText)
.arg(mimeType)
.arg(base64Data));
ui.textEdit->setFocus();
}
void WikiEditDialog::onMarkdownUnorderedList()
{
insertMarkdownBlockPrefix("- ");
}
void WikiEditDialog::onMarkdownOrderedList()
{
insertMarkdownBlockPrefix("1. ");
}

View File

@ -63,6 +63,16 @@ void generateMerge();
void historyItemChanged(QTreeWidgetItem *item, int column);
void updateMergeButtonState();
void onMarkdownHeading();
void onMarkdownBold();
void onMarkdownItalic();
void onMarkdownQuote();
void onMarkdownCode();
void onMarkdownLink();
void onMarkdownImage();
void onMarkdownUnorderedList();
void onMarkdownOrderedList();
private:
void updateHistoryStatus();
@ -94,6 +104,9 @@ QString getAuthorName(const RsGxsMessageId &msgId) const;
QTreeWidgetItem *findHistoryItem(const RsGxsMessageId &msgId) const;
void resetEditorState();
void insertMarkdownWrap(const QString &prefix, const QString &suffix);
void insertMarkdownBlockPrefix(const QString &prefix);
bool mNewPage;

View File

@ -78,16 +78,16 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_Merge">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Merge for Republish</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_Merge">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Merge for Republish</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="treeWidget_History">
<column>
@ -282,6 +282,146 @@
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="btnHeading">
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Heading (###)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>H</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/textedit/heading.png</normaloff>:/icons/textedit/heading.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnBold">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Bold (**bold**)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/textedit/bold.png</normaloff>:/icons/textedit/bold.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnItalic">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Italic (*italic*)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/textedit/italic.png</normaloff>:/icons/textedit/italic.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnCode">
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Code (`code`)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>C</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/textedit/code.png</normaloff>:/icons/textedit/code.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnQuote">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Blockquote (&amp;gt; quote)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/textedit/blockquote.png</normaloff>:/icons/textedit/blockquote.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnLink">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Link ([text](url))&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/textedit/link.png</normaloff>:/icons/textedit/link.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnImage">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Image (![alt](url))&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/textedit/photo-of-a-landscape.png</normaloff>:/icons/textedit/photo-of-a-landscape.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnUnorderedList">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Unordered List (- item)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/textedit/bullet-list.png</normaloff>:/icons/textedit/bullet-list.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnOrderedList">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Ordered List (1. item)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/textedit/numberd-list.png</normaloff>:/icons/textedit/numberd-list.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="GxsIdChooser" name="comboBox_IdChooser"/>
</item>
@ -371,6 +511,9 @@
<header>gui/gxs/GxsIdChooser.h</header>
</customwidget>
</customwidgets>
<resources/>
<resources>
<include location="Wiki_images.qrc"/>
<include location="../icons.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -353,6 +353,8 @@
<file>icons/textedit/smile.png</file>
<file>icons/textedit/font-increase.png</file>
<file>icons/textedit/code.png</file>
<file>icons/textedit/blockquote.png</file>
<file>icons/textedit/heading.png</file>
<file>icons/fullscreen.png</file>
<file>icons/fullscreen-exit.png</file>
<file>icons/notification.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB