diff options
-rw-r--r-- | CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/activitypub/apactor.cpp | 25 | ||||
-rw-r--r-- | src/activitypub/apactor.h | 42 | ||||
-rw-r--r-- | src/activitypub/apattachment.cpp | 26 | ||||
-rw-r--r-- | src/activitypub/apattachment.h | 4 | ||||
-rw-r--r-- | src/archive/base_archive.cpp | 4 | ||||
-rw-r--r-- | src/archive/base_archive.h | 9 | ||||
-rw-r--r-- | src/archive/mastodon.cpp | 90 | ||||
-rw-r--r-- | src/archive/mastodon.h | 3 | ||||
-rw-r--r-- | src/mainwindow.cpp | 8 | ||||
-rw-r--r-- | src/mainwindow.h | 3 | ||||
-rw-r--r-- | src/widgets/tab_actor_info.cpp | 49 | ||||
-rw-r--r-- | src/widgets/tab_actor_info.h | 27 | ||||
-rw-r--r-- | src/widgets/tab_actor_info.ui | 79 |
14 files changed, 365 insertions, 7 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index a81928c..32b6490 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,9 @@ set(PROJECT_SOURCES src/widgets/tab_activity_list.cpp src/widgets/tab_activity_list.h src/widgets/tab_activity_list.ui + src/widgets/tab_actor_info.cpp + src/widgets/tab_actor_info.h + src/widgets/tab_actor_info.ui src/archive/base_archive.cpp src/archive/base_archive.h src/archive/mastodon.cpp diff --git a/src/activitypub/apactor.cpp b/src/activitypub/apactor.cpp index 6731dd3..78ed9a3 100644 --- a/src/activitypub/apactor.cpp +++ b/src/activitypub/apactor.cpp @@ -1,11 +1,34 @@ #include "apactor.h" +#include <QDebug> APActor::APActor() {} -APActor::APActor(const QString url) { +APActor::APActor(const QString& url) { object_url = url; } +APActor::APActor(APActorFields& fields) { + object_url = fields.url; + username = fields.username; + name = fields.display_name; + summary = fields.summary; + manuallyApprovesFollowers = fields.manuallyApprovesFollowers; + discoverable = fields.discoverable; + joined = QDateTime::fromString(fields.joined_date, Qt::ISODate); + avatar = fields.avatar; + header = fields.header; + + if (fields.keys.length() == fields.values.length()) + for (int i = 0; i < fields.keys.length(); ++i) + table.push_back({fields.keys[i], fields.values[i]}); + else qDebug() << "key and value vectors don't have the same lenght!"; +} + +APActor::~APActor() { + if (avatar) delete avatar; + if (header) delete header; +} + const QString APActor::get_url() { return object_url; } diff --git a/src/activitypub/apactor.h b/src/activitypub/apactor.h index 60ef0bf..ad8d689 100644 --- a/src/activitypub/apactor.h +++ b/src/activitypub/apactor.h @@ -1,22 +1,60 @@ #pragma once #include "apbase.h" +#include "apattachment.h" +#include <QDateTime> #include <vector> +struct APPropertyValue { + QString key; + QString value; +}; + +struct APActorFields { + QString url; + QString username; // without the leading '@' + QString display_name; + QString summary; // Profile biography/description (bio) + QStringList keys; + QStringList values; + APAttachment* avatar = nullptr; + APAttachment* header = nullptr; + bool manuallyApprovesFollowers = false; + bool discoverable = true; // is discoverable or indexable + QString joined_date; + QStringList also_known_as; // other Actors representing the same user +}; + // APActors will have an avatar, display name, username and more that will be fetched using the remote instance +// Currently missing: featured tags and featured posts class APActor : APBase { public: // Empty actor APActor(); - APActor(const QString url); - ~APActor() {}; + APActor(const QString& url); + APActor(APActorFields& fields); + ~APActor(); const QString get_url(); QString get_html_render(HtmlRenderDetails render_info); const QString get_plain_render(); private: + friend class TabActorInfo; + QString object_url; + QString username; + QString name; // Display name + QString summary; // Profile biography/description (bio) + typedef std::vector<APPropertyValue> APPropertyValueList; // Key/value table in profile (example: "web site | http://example.com/") + APPropertyValueList table; + APAttachment* avatar = nullptr; + APAttachment* header = nullptr; + bool manuallyApprovesFollowers = false; + bool discoverable = true; // is discoverable or indexable + QDateTime joined; + // TODO: parse the also_known_as field + // APActorList known_as; }; class APActorList : public std::vector<APActor>, APBase { diff --git a/src/activitypub/apattachment.cpp b/src/activitypub/apattachment.cpp index 0589291..342ea21 100644 --- a/src/activitypub/apattachment.cpp +++ b/src/activitypub/apattachment.cpp @@ -1,5 +1,6 @@ #include "apattachment.h" #include <QFileInfo> +#include <QDebug> APAttachment::APAttachment() {} @@ -28,6 +29,31 @@ QString APAttachment::get_html_render(HtmlRenderDetails info) { return html; } +const QPixmap& APAttachment::get_pixmap(int width, int height) { + if (pixmap) return *pixmap; + + if (width > 0 or height > 0) { + QPixmap image(path_url); + + /* proportionality rule: + * width image width + * ----- = ----------- + * height image height + */ + if (height == 0) + height = (width * image.height()) / image.width(); + else if (width == 0) + width = (height * image.width()) / image.height(); + + pixmap = new QPixmap(image.scaled(QSize(width, height))); + } else { + pixmap = new QPixmap; + if (not pixmap->load(path_url)) + qDebug() << "failed to load" << path_url; + } + return *pixmap; +} + QString APAttachmentList::get_html_render(HtmlRenderDetails render_info) { QString html; diff --git a/src/activitypub/apattachment.h b/src/activitypub/apattachment.h index ff40dc3..f4e620b 100644 --- a/src/activitypub/apattachment.h +++ b/src/activitypub/apattachment.h @@ -2,6 +2,7 @@ #include "apbase.h" #include "fields.h" +#include <QPixmap> #include <array> #include <vector> @@ -11,6 +12,7 @@ public: APAttachment(APAttachmentFields fields); ~APAttachment() {}; QString get_html_render(HtmlRenderDetails render_info); + const QPixmap& get_pixmap(int width = 0, int height = 0); private: QString blurhash; @@ -19,6 +21,8 @@ private: QString description; // alt text, maps to "name" QString path_url; // attachment URL, might be file on filesystem (make sure that the attachment dir has been found and that the path is correct) QString filename; // nicer descriptor of the attachment's path + + QPixmap* pixmap = nullptr; }; class APAttachmentList : public std::vector<APAttachment>, APBase { diff --git a/src/archive/base_archive.cpp b/src/archive/base_archive.cpp index 47cadf2..472abb1 100644 --- a/src/archive/base_archive.cpp +++ b/src/archive/base_archive.cpp @@ -15,3 +15,7 @@ Archive::Archive(const QString& filename) : main_filename(filename) {} const QString Archive::get_instance_address() { return ""; } + +APActor* Archive::get_main_actor() { + return nullptr; +} diff --git a/src/archive/base_archive.h b/src/archive/base_archive.h index bf43306..27259b3 100644 --- a/src/archive/base_archive.h +++ b/src/archive/base_archive.h @@ -1,10 +1,10 @@ #pragma once - +#include "src/activitypub/apactivity.h" +#include "src/activitypub/apactor.h" +#include "src/types.h" #include <QString> #include <QListWidget> #include <variant> -#include "src/activitypub/apactivity.h" -#include "src/types.h" enum ArchiveType { MASTODON @@ -35,6 +35,9 @@ public: virtual void update_status_list(ViewStatusTypes allowed_types, QListWidget *parent) = 0; virtual APActivity* get_activity(ArchiveItemRef index, Hinting_t hinting) = 0; + // Get the Actor that represents the data export, the user who's account data has been exported. + // Return nullptr if the Actor cannot be retrieved + virtual APActor* get_main_actor(); virtual const QString get_html_status_text(ArchiveItemRef status_index) = 0; virtual const QString get_instance_address(); diff --git a/src/archive/mastodon.cpp b/src/archive/mastodon.cpp index 781f928..121c366 100644 --- a/src/archive/mastodon.cpp +++ b/src/archive/mastodon.cpp @@ -355,6 +355,96 @@ const QString MastodonArchive::get_html_status_text(ArchiveItemRef index) { return text; } +APActor* MastodonArchive::get_main_actor() { + // Avoid recreating the Actor each time by caching it + if (actor) return actor; + + // First, get the file + QString actor_filepath = archive_root_dir.filePath("actor.json"); + if (not QFile::exists(actor_filepath)) return nullptr; + QFile actor_file(actor_filepath); + + if (!actor_file.open(QIODevice::ReadOnly | QIODevice::Text)) + return nullptr; + + QJsonParseError json_error; + QJsonDocument actor_json_document = QJsonDocument::fromJson(actor_file.readAll(), &json_error); + actor_file.close(); + + if (json_error.error != QJsonParseError::NoError or actor_json_document.isNull() or actor_json_document.isEmpty() or not actor_json_document.isObject()) + return nullptr; + + // Second, now check the JSON + QJsonObject actor_json(actor_json_document.object()); + + // Do some more throughful checks to make sure that the JSON is actually valid + // Please tell me if actor is not "actor.json" (also, i should instead first read actor.json to know the outbox file location, not the other way around as i've been doing since the beginning). Indeed, the checks are also quite random + if (not json_check_item(actor_json.value("@context"), "https://www.w3.org/ns/activitystreams") or actor_json.value("outbox").toString() != "outbox.json") + return nullptr; + + // Third, now parse the JSON :-D + { + APActorFields obj_fields; + // If the key doesn't exist then toString() will return a null QString + obj_fields.url = actor_json.value("id").toString(); + obj_fields.username = actor_json.value("preferredUsername").toString(); + obj_fields.display_name = actor_json.value("name").toString(); + obj_fields.summary = actor_json.value("summary").toString(); + obj_fields.manuallyApprovesFollowers = actor_json.value("manuallyApprovesFollowers").toBool(); + obj_fields.discoverable = actor_json.value("discoverable").toBool(); + obj_fields.joined_date = actor_json.value("published").toString(); + + if (actor_json.value("attachment").isArray()) { + QJsonArray table = actor_json.value("attachment").toArray(); + for (auto pair_obj : table) { + QJsonObject pair = pair_obj.toObject(); + + // Not a PropertyValue, so better not continue with this pair (just in case it represents something else) + if (pair.value("type").toString() != "PropertyValue") { + qDebug() << "not a PropertyValue!"; + continue; + } + + if (pair.contains("name") && pair.contains("value")) { + obj_fields.keys.append(pair.value("name").toString()); + obj_fields.values.append(pair.value("value").toString()); + } + } + } + + if (actor_json.value("alsoKnownAs").isArray()) { + QJsonArray list = actor_json.value("alsoKnownAs").toArray(); + for (auto id_obj : list) + obj_fields.also_known_as.append(id_obj.toString()); + } + + if (actor_json.value("icon").isObject()) { + APAttachmentFields att_fields; + QJsonObject attachment = actor_json.value("icon").toObject(); + + att_fields.media_type = attachment["mediaType"].toString(); + att_fields.path = archive_root_dir.absoluteFilePath(attachment["url"].toString()); + + obj_fields.avatar = new APAttachment(att_fields); + } + + if (actor_json.value("image").isObject()) { + APAttachmentFields att_fields; + QJsonObject attachment = actor_json.value("image").toObject(); + att_fields.media_type = attachment["mediaType"].toString(); + att_fields.path = archive_root_dir.absoluteFilePath(attachment["url"].toString()); + obj_fields.header = new APAttachment(att_fields); + } + + actor = new APActor(obj_fields); + + // TODO: query type. it seems to be always "Person", but maybe if the account is a bot, it changes? + // The code also currently ignores possible featured tags and users + } + + return actor; +} + void MastodonArchive::find_attachment_dir(QString example_attachment) { // Find the root directory name of the attachment QString root_name = example_attachment.split('/', Qt::SkipEmptyParts)[0]; diff --git a/src/archive/mastodon.h b/src/archive/mastodon.h index 75bc7ec..f148047 100644 --- a/src/archive/mastodon.h +++ b/src/archive/mastodon.h @@ -3,6 +3,7 @@ #include "src/archive/base_archive.h" #include "src/activitypub/fields.h" #include "src/activitypub/apactivity.h" +#include "src/activitypub/apactor.h" #include "src/types.h" #include <QJsonDocument> @@ -19,6 +20,7 @@ public: void update_status_list(ViewStatusTypes allowed_types, QListWidget *parent); APActivity* get_activity(ArchiveItemRef index, Hinting_t hinting = {}); + APActor* get_main_actor(); const QString get_html_status_text(ArchiveItemRef status_index); const QString get_instance_address(); private: @@ -28,6 +30,7 @@ private: QJsonObject *outbox_json = nullptr; QJsonArray *outbox_items = nullptr; + APActor* actor = nullptr; bool is_status_type_allowed(StatusType status_type, ViewStatusTypes allowed_types); StatusType get_status_type(QJsonObject obj); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c2a9820..1432116 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5,6 +5,7 @@ #include "src/settingsdialog.h" #include "src/aboutdialog.h" #include "src/widgets/tab_activity_list.h" +#include "src/widgets/tab_actor_info.h" #include <QFileDialog> #include <QInputDialog> @@ -53,6 +54,10 @@ void MainWindow::create_initial_tabs() { activity_list_tab->relist_statuses(true); }); + // TODO: maybe have one Actor info tab that is constantly updated with the new opened archive? + // actor_info_tab = new TabActorInfo(data_archive); + // ui->tabWidget->addTab(actor_info_tab, tr("Actor Info")); + // TODO: Add the "+" tab for opening new tabs } @@ -151,6 +156,9 @@ void MainWindow::finish_open_file(const Archive::InitError& parse_error) { if (parse_error == Archive::NoError) { emit new_archive_opened(); } + actor_info_tab = new TabActorInfo(data_archive); + ui->tabWidget->addTab(actor_info_tab, tr("Actor Info")); + connect(this, &MainWindow::new_archive_opened, actor_info_tab, &TabActorInfo::deleteLater); // The cursor is restored in TabActivityList::relist_statuses() } diff --git a/src/mainwindow.h b/src/mainwindow.h index e77e2a9..aadd439 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -4,12 +4,12 @@ #include <QListWidgetItem> #include <QLocale> #include <QFutureWatcher> -#include <vector> #include "archive/base_archive.h" #include "src/list_item.h" #include "src/settingsdialog.h" #include "src/widgets/tab_activity_list.h" +#include "src/widgets/tab_actor_info.h" #include "command_line.h" QT_BEGIN_NAMESPACE @@ -53,6 +53,7 @@ private: SettingsDialog* settings_dialog = nullptr; TabActivityList* activity_list_tab = nullptr; + TabActorInfo* actor_info_tab = nullptr; QString open_file_filename; diff --git a/src/widgets/tab_actor_info.cpp b/src/widgets/tab_actor_info.cpp new file mode 100644 index 0000000..79630df --- /dev/null +++ b/src/widgets/tab_actor_info.cpp @@ -0,0 +1,49 @@ +#include "tab_actor_info.h" +#include "ui_tab_actor_info.h" +#include "src/activitypub/apactor.h" +#include "src/archive/base_archive.h" +#include <QTableWidgetItem> +#include <QDebug> + +TabActorInfo::TabActorInfo(APActor* actor, QWidget* parent) + : QWidget(parent), ui(new Ui::TabActorInfo), actor(actor) +{ + ui->setupUi(this); + ui->gridLayout->setContentsMargins(0, 0, 0, 0); + + update_ui(); +} + +TabActorInfo::TabActorInfo(Archive* archive, QWidget* parent) + : QWidget(parent), ui(new Ui::TabActorInfo) +{ + ui->setupUi(this); + ui->gridLayout->setContentsMargins(0, 0, 0, 0); + + actor = archive->get_main_actor(); + qDebug() << actor; + update_ui(); +} + +TabActorInfo::~TabActorInfo() { + if (actor) delete actor; + delete ui; +} + +// TODO: do this in another thread, like for status_info +void TabActorInfo::update_ui() { + if (not actor) return; + + ui->displayNameText->setText(ui->displayNameText->text().arg(actor->name).arg(actor->username)); + ui->summaryText->setHtml(actor->summary); + ui->avatarImage->setPixmap(actor->avatar->get_pixmap(ui->avatarImage->size().width())); + // TODO: set header as top of background of widget + // Affiche le nom et la description fonctionne sans problèmes + + int row = 0; + for (APPropertyValue prop : actor->table) { + ui->attachments->insertRow(row); + ui->attachments->setItem(row, 0, new QTableWidgetItem(prop.key)); + ui->attachments->setItem(row++, 1, new QTableWidgetItem(prop.value)); + } +} diff --git a/src/widgets/tab_actor_info.h b/src/widgets/tab_actor_info.h new file mode 100644 index 0000000..a1dd84f --- /dev/null +++ b/src/widgets/tab_actor_info.h @@ -0,0 +1,27 @@ +#pragma once +#include <QWidget> +#include "src/activitypub/apactor.h" +#include "src/archive/base_archive.h" + +QT_BEGIN_NAMESPACE +namespace Ui { class TabActorInfo; } +QT_END_NAMESPACE + +class TabActorInfo : public QWidget { + Q_OBJECT + +public: + TabActorInfo(APActor* actor, QWidget *parent = nullptr); + TabActorInfo(Archive* archive, QWidget *parent = nullptr); + ~TabActorInfo(); + // TODO: when clicking/activating avatar image, open it with the user's default image viewer + +public slots: + // Update displayed information to match Actor + void update_ui(); + +private: + Ui::TabActorInfo* ui; + + APActor* actor = nullptr; +}; diff --git a/src/widgets/tab_actor_info.ui b/src/widgets/tab_actor_info.ui new file mode 100644 index 0000000..8fb7e21 --- /dev/null +++ b/src/widgets/tab_actor_info.ui @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TabActorInfo</class> + <widget class="QWidget" name="TabActorInfo"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QGridLayout" name="gridLayout" columnminimumwidth="50,0"> + <item row="0" column="1"> + <widget class="QLabel" name="displayNameText"> + <property name="text"> + <string notr="true"><html><head/><body><p><span style=" font-size:12pt; font-weight:600;">%1</span> <span style=" font-size:10pt;">(@%2)</span></p></body></html></string> + </property> + </widget> + </item> + <item row="1" column="1"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTextBrowser" name="summaryText"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QTableWidget" name="attachments"> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <property name="cornerButtonEnabled"> + <bool>false</bool> + </property> + <property name="columnCount"> + <number>2</number> + </property> + <attribute name="horizontalHeaderVisible"> + <bool>false</bool> + </attribute> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + <column/> + <column/> + </widget> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="avatarImage"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string notr="true">aaaaa</string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> |