aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorConfuSomu2024-01-20 21:57:39 -0500
committerConfuSomu2024-01-20 21:57:39 -0500
commit5aac009e969cc3bd15c484ba3437348cb7a4d186 (patch)
tree96cff55a273b9de31d639b00f7139110946ce1b6
parent328c9b166e9d623cd1b80c7ae064baf6172da58f (diff)
downloadActorViewer-5aac009e969cc3bd15c484ba3437348cb7a4d186.tar
ActorViewer-5aac009e969cc3bd15c484ba3437348cb7a4d186.tar.gz
ActorViewer-5aac009e969cc3bd15c484ba3437348cb7a4d186.zip
Implemement Actor information tab
This class still has improvements to be made, but it works and I am satisfied with it!
-rw-r--r--CMakeLists.txt3
-rw-r--r--src/activitypub/apactor.cpp25
-rw-r--r--src/activitypub/apactor.h42
-rw-r--r--src/activitypub/apattachment.cpp26
-rw-r--r--src/activitypub/apattachment.h4
-rw-r--r--src/archive/base_archive.cpp4
-rw-r--r--src/archive/base_archive.h9
-rw-r--r--src/archive/mastodon.cpp90
-rw-r--r--src/archive/mastodon.h3
-rw-r--r--src/mainwindow.cpp8
-rw-r--r--src/mainwindow.h3
-rw-r--r--src/widgets/tab_actor_info.cpp49
-rw-r--r--src/widgets/tab_actor_info.h27
-rw-r--r--src/widgets/tab_actor_info.ui79
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">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt; font-weight:600;&quot;&gt;%1&lt;/span&gt; &lt;span style=&quot; font-size:10pt;&quot;&gt;(@%2)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>