diff options
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | abaddon.cpp | 6 | ||||
-rw-r--r-- | abaddon.hpp | 2 | ||||
-rw-r--r-- | components/cellrendererpixbufanimation.cpp | 95 | ||||
-rw-r--r-- | components/cellrendererpixbufanimation.hpp | 41 | ||||
-rw-r--r-- | discord/discord.cpp | 49 | ||||
-rw-r--r-- | discord/discord.hpp | 10 | ||||
-rw-r--r-- | discord/emoji.cpp | 22 | ||||
-rw-r--r-- | discord/emoji.hpp | 6 | ||||
-rw-r--r-- | discord/objects.cpp | 8 | ||||
-rw-r--r-- | discord/objects.hpp | 16 | ||||
-rw-r--r-- | windows/guildsettings/emojispane.cpp | 255 | ||||
-rw-r--r-- | windows/guildsettings/emojispane.hpp | 55 | ||||
-rw-r--r-- | windows/guildsettingswindow.cpp | 5 | ||||
-rw-r--r-- | windows/guildsettingswindow.hpp | 2 |
15 files changed, 565 insertions, 8 deletions
@@ -15,6 +15,7 @@ Current features: * Kick, ban, and unban members * Modify roles and modify members' roles * Manage invites +* Manage emojis * View audit log * Emojis<sup>2</sup> * Animated avatars, server icons, emojis (can be turned off) diff --git a/abaddon.cpp b/abaddon.cpp index 9700546..4a32b5d 100644 --- a/abaddon.cpp +++ b/abaddon.cpp @@ -578,6 +578,12 @@ void Abaddon::ActionAddRecipient(Snowflake channel_id) { } } +bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) { + ConfirmDialog dlg(window != nullptr ? *window : *m_main_window); + dlg.SetConfirmText(prompt); + return dlg.run() == Gtk::RESPONSE_OK; +} + void Abaddon::ActionReloadSettings() { m_settings.Reload(); } diff --git a/abaddon.hpp b/abaddon.hpp index 802f2b8..0799ed3 100644 --- a/abaddon.hpp +++ b/abaddon.hpp @@ -48,6 +48,8 @@ public: void ActionGuildSettings(Snowflake id); void ActionAddRecipient(Snowflake channel_id); + bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr); + void ActionReloadSettings(); void ActionReloadCSS(); diff --git a/components/cellrendererpixbufanimation.cpp b/components/cellrendererpixbufanimation.cpp new file mode 100644 index 0000000..1f49402 --- /dev/null +++ b/components/cellrendererpixbufanimation.cpp @@ -0,0 +1,95 @@ +#include "cellrendererpixbufanimation.hpp" + +CellRendererPixbufAnimation::CellRendererPixbufAnimation() + : Glib::ObjectBase(typeid(CellRendererPixbufAnimation)) + , Gtk::CellRenderer() + , m_property_pixbuf(*this, "pixbuf") + , m_property_pixbuf_animation(*this, "pixbuf-animation") { + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + property_xpad() = 2; + property_ypad() = 2; +} + +CellRendererPixbufAnimation::~CellRendererPixbufAnimation() {} + +Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererPixbufAnimation::property_pixbuf() { + return m_property_pixbuf.get_proxy(); +} + +Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> CellRendererPixbufAnimation::property_pixbuf_animation() { + return m_property_pixbuf_animation.get_proxy(); +} + +void CellRendererPixbufAnimation::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { + int width = 0; + + if (auto pixbuf = m_property_pixbuf_animation.get_value()) + width = pixbuf->get_width(); + else if (auto pixbuf = m_property_pixbuf.get_value()) + width = pixbuf->get_width(); + + int xpad, ypad; + get_padding(xpad, ypad); + minimum_width = natural_width = xpad * 2 + width; +} + +void CellRendererPixbufAnimation::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { + get_preferred_width_vfunc(widget, minimum_width, natural_width); +} + +void CellRendererPixbufAnimation::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { + int height = 0; + + if (auto pixbuf = m_property_pixbuf_animation.get_value()) + height = pixbuf->get_height(); + else if (auto pixbuf = m_property_pixbuf.get_value()) + height = pixbuf->get_height(); + + int xpad, ypad; + get_padding(xpad, ypad); + minimum_height = natural_height = ypad * 2 + height; +} + +void CellRendererPixbufAnimation::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { + get_preferred_height_vfunc(widget, minimum_height, natural_height); +} + +void CellRendererPixbufAnimation::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) { + Gtk::Requisition minimum, natural; + get_preferred_size(widget, minimum, natural); + auto alloc = widget.get_allocation(); + int xpad, ypad; + get_padding(xpad, ypad); + int pix_x = cell_area.get_x() + xpad; + int pix_y = cell_area.get_y() + ypad; + natural.width -= xpad * 2; + natural.height -= ypad * 2; + + Gdk::Rectangle pix_rect(pix_x, pix_y, natural.width, natural.height); + if (!cell_area.intersects(pix_rect)) + return; + + if (auto anim = m_property_pixbuf_animation.get_value()) { + auto map_iter = m_pixbuf_animation_iters.find(anim); + if (map_iter == m_pixbuf_animation_iters.end()) + m_pixbuf_animation_iters[anim] = anim->get_iter(nullptr); + auto pb_iter = m_pixbuf_animation_iters.at(anim); + + const auto cb = [this, &widget, anim] { + if (m_pixbuf_animation_iters.at(anim)->advance()) + widget.queue_draw(); + }; + Glib::signal_timeout().connect_once(sigc::track_obj(cb, widget), pb_iter->get_delay_time()); + Gdk::Cairo::set_source_pixbuf(cr, pb_iter->get_pixbuf(), pix_x, pix_y); + cr->rectangle(pix_x, pix_y, natural.width, natural.height); + cr->fill(); + } else if (auto pixbuf = m_property_pixbuf.get_value()) { + Gdk::Cairo::set_source_pixbuf(cr, pixbuf, pix_x, pix_y); + cr->rectangle(pix_x, pix_y, natural.width, natural.height); + cr->fill(); + } +} diff --git a/components/cellrendererpixbufanimation.hpp b/components/cellrendererpixbufanimation.hpp new file mode 100644 index 0000000..f47e928 --- /dev/null +++ b/components/cellrendererpixbufanimation.hpp @@ -0,0 +1,41 @@ +#pragma once +#include <gtkmm.h> +#include <unordered_map> + +// handles both static and animated +class CellRendererPixbufAnimation : public Gtk::CellRenderer { +public: + CellRendererPixbufAnimation(); + virtual ~CellRendererPixbufAnimation(); + + Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_pixbuf(); + Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_pixbuf_animation(); + +protected: + void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override; + + void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override; + + void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override; + + void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override; + + void render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flag) override; + +private: + Glib::Property<Glib::RefPtr<Gdk::Pixbuf>> m_property_pixbuf; + Glib::Property<Glib::RefPtr<Gdk::PixbufAnimation>> m_property_pixbuf_animation; + /* one cellrenderer is used for every animation and i dont know how to + store data per-pixbuf (in this case the iter) so this little map thing will have to do + i would try set_data on the pixbuf but i dont know if that will cause memory leaks + this would mean if a row's pixbuf animation is changed more than once then it wont be released immediately + but thats not a problem for me in this case + + unordered_map doesnt compile cuz theres no hash overload, i guess + */ + std::map<Glib::RefPtr<Gdk::PixbufAnimation>, Glib::RefPtr<Gdk::PixbufAnimationIter>> m_pixbuf_animation_iters; +}; diff --git a/discord/discord.cpp b/discord/discord.cpp index 8f5a1ea..a16a119 100644 --- a/discord/discord.cpp +++ b/discord/discord.cpp @@ -616,6 +616,21 @@ void DiscordClient::ModifyRolePosition(Snowflake guild_id, Snowflake role_id, in }); } +void DiscordClient::ModifyEmojiName(Snowflake guild_id, Snowflake emoji_id, const Glib::ustring &name, sigc::slot<void(bool success)> callback) { + ModifyGuildEmojiObject obj; + obj.Name = name; + + m_http.MakePATCH("/guilds/" + std::to_string(guild_id) + "/emojis/" + std::to_string(emoji_id), nlohmann::json(obj).dump(), [this, callback](const http::response_type &response) { + callback(CheckCode(response)); + }); +} + +void DiscordClient::DeleteEmoji(Snowflake guild_id, Snowflake emoji_id, sigc::slot<void(bool success)> callback) { + m_http.MakeDELETE("/guilds/" + std::to_string(guild_id) + "/emojis/" + std::to_string(emoji_id), [this, callback](const http::response_type &response) { + callback(CheckCode(response, 204)); + }); +} + bool DiscordClient::CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const { const auto guild = *GetGuild(guild_id); if (guild.OwnerID == user_id) return true; @@ -687,6 +702,18 @@ void DiscordClient::FetchAuditLog(Snowflake guild_id, sigc::slot<void(AuditLogDa }); } +void DiscordClient::FetchGuildEmojis(Snowflake guild_id, sigc::slot<void(std::vector<EmojiData>)> callback) { + m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/emojis", [this, callback](const http::response_type &response) { + if (!CheckCode(response)) return; + auto emojis = nlohmann::json::parse(response.text).get<std::vector<EmojiData>>(); + m_store.BeginTransaction(); + for (const auto &emoji : emojis) + m_store.SetEmoji(emoji.ID, emoji); + m_store.EndTransaction(); + callback(std::move(emojis)); + }); +} + void DiscordClient::FetchUserProfile(Snowflake user_id, sigc::slot<void(UserProfileData)> callback) { m_http.MakeGET("/users/" + std::to_string(user_id) + "/profile", [this, callback](const http::response_type &response) { if (!CheckCode(response)) return; @@ -919,6 +946,9 @@ void DiscordClient::HandleGatewayMessage(std::string str) { case GatewayEvent::READY_SUPPLEMENTAL: { HandleGatewayReadySupplemental(m); } break; + case GatewayEvent::GUILD_EMOJIS_UPDATE: { + HandleGatewayGuildEmojisUpdate(m); + } break; } } break; default: @@ -1317,6 +1347,20 @@ void DiscordClient::HandleGatewayUserNoteUpdate(const GatewayMessage &msg) { m_signal_note_update.emit(data.ID, data.Note); } +void DiscordClient::HandleGatewayGuildEmojisUpdate(const GatewayMessage &msg) { + // like the real client, the emoji data sent in this message is ignored + // we just use it as a signal to re-request all emojis + GuildEmojisUpdateObject data = msg.Data; + const auto cb = [this, id = data.GuildID](const std::vector<EmojiData> &emojis) { + m_store.BeginTransaction(); + for (const auto &emoji : emojis) + m_store.SetEmoji(emoji.ID, emoji); + m_store.EndTransaction(); + m_signal_guild_emojis_update.emit(id, emojis); + }; + FetchGuildEmojis(data.GuildID, cb); +} + void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { ReadySupplementalData data = msg.Data; for (const auto &p : data.MergedPresences.Friends) { @@ -1637,6 +1681,7 @@ void DiscordClient::LoadEventMap() { m_event_map["INVITE_DELETE"] = GatewayEvent::INVITE_DELETE; m_event_map["USER_NOTE_UPDATE"] = GatewayEvent::USER_NOTE_UPDATE; m_event_map["READY_SUPPLEMENTAL"] = GatewayEvent::READY_SUPPLEMENTAL; + m_event_map["GUILD_EMOJIS_UPDATE"] = GatewayEvent::GUILD_EMOJIS_UPDATE; } DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { @@ -1742,3 +1787,7 @@ DiscordClient::type_signal_presence_update DiscordClient::signal_presence_update DiscordClient::type_signal_note_update DiscordClient::signal_note_update() { return m_signal_note_update; } + +DiscordClient::type_signal_guild_emojis_update DiscordClient::signal_guild_emojis_update() { + return m_signal_guild_emojis_update; +} diff --git a/discord/discord.hpp b/discord/discord.hpp index d8b2363..476adc8 100644 --- a/discord/discord.hpp +++ b/discord/discord.hpp @@ -129,6 +129,8 @@ public: void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, uint32_t color, sigc::slot<void(bool success)> callback); void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, Gdk::RGBA color, sigc::slot<void(bool success)> callback); void ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, sigc::slot<void(bool success)> callback); + void ModifyEmojiName(Snowflake guild_id, Snowflake emoji_id, const Glib::ustring &name, sigc::slot<void(bool success)> callback); + void DeleteEmoji(Snowflake guild_id, Snowflake emoji_id, sigc::slot<void(bool success)> callback); bool CanModifyRole(Snowflake guild_id, Snowflake role_id) const; bool CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const; @@ -153,6 +155,8 @@ public: void FetchAuditLog(Snowflake guild_id, sigc::slot<void(AuditLogData)> callback); + void FetchGuildEmojis(Snowflake guild_id, sigc::slot<void(std::vector<EmojiData>)> callback); + void FetchUserProfile(Snowflake user_id, sigc::slot<void(UserProfileData)> callback); void FetchUserNote(Snowflake user_id, sigc::slot<void(std::string note)> callback); void SetUserNote(Snowflake user_id, std::string note); @@ -204,6 +208,7 @@ private: void HandleGatewayInviteCreate(const GatewayMessage &msg); void HandleGatewayInviteDelete(const GatewayMessage &msg); void HandleGatewayUserNoteUpdate(const GatewayMessage &msg); + void HandleGatewayGuildEmojisUpdate(const GatewayMessage &msg); void HandleGatewayReadySupplemental(const GatewayMessage &msg); void HandleGatewayReconnect(const GatewayMessage &msg); void HandleGatewayInvalidSession(const GatewayMessage &msg); @@ -289,7 +294,8 @@ public: typedef sigc::signal<void, InviteDeleteObject> type_signal_invite_delete; typedef sigc::signal<void, Snowflake, PresenceStatus> type_signal_presence_update; typedef sigc::signal<void, Snowflake, std::string> type_signal_note_update; - typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting + typedef sigc::signal<void, Snowflake, std::vector<EmojiData>> type_signal_guild_emojis_update; // guild id + typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting typedef sigc::signal<void> type_signal_connected; type_signal_gateway_ready signal_gateway_ready(); @@ -316,6 +322,7 @@ public: type_signal_invite_delete signal_invite_delete(); // safe to assume guild id is set type_signal_presence_update signal_presence_update(); type_signal_note_update signal_note_update(); + type_signal_guild_emojis_update signal_guild_emojis_update(); type_signal_disconnected signal_disconnected(); type_signal_connected signal_connected(); @@ -344,6 +351,7 @@ protected: type_signal_invite_delete m_signal_invite_delete; type_signal_presence_update m_signal_presence_update; type_signal_note_update m_signal_note_update; + type_signal_guild_emojis_update m_signal_guild_emojis_update; type_signal_disconnected m_signal_disconnected; type_signal_connected m_signal_connected; }; diff --git a/discord/emoji.cpp b/discord/emoji.cpp index f219f0e..1a97eb8 100644 --- a/discord/emoji.cpp +++ b/discord/emoji.cpp @@ -28,10 +28,24 @@ void to_json(nlohmann::json &j, const EmojiData &m) { JS_IF("available", m.IsAvailable); } -std::string EmojiData::GetURL() const { - return "https://cdn.discordapp.com/emojis/" + std::to_string(ID) + ".png"; +std::string EmojiData::GetURL(const char *ext, const char *size) const { + if (size != nullptr) + return "https://cdn.discordapp.com/emojis/" + std::to_string(ID) + "." + ext + "?size=" + size; + else + return "https://cdn.discordapp.com/emojis/" + std::to_string(ID) + "." + ext; +} + +std::string EmojiData::URLFromID(const std::string &emoji_id, const char *ext, const char *size) { + if (size != nullptr) + return "https://cdn.discordapp.com/emojis/" + emoji_id + "." + ext + "?size=" + size; + else + return "https://cdn.discordapp.com/emojis/" + emoji_id + "." + ext; +} + +std::string EmojiData::URLFromID(Snowflake emoji_id, const char *ext, const char *size) { + return URLFromID(std::to_string(emoji_id), ext, size); } -std::string EmojiData::URLFromID(std::string emoji_id, std::string ext) { - return "https://cdn.discordapp.com/emojis/" + emoji_id + "." + ext; +std::string EmojiData::URLFromID(const Glib::ustring &emoji_id, const char *ext, const char *size) { + return URLFromID(emoji_id.raw(), ext, size); } diff --git a/discord/emoji.hpp b/discord/emoji.hpp index 1cbc108..156e127 100644 --- a/discord/emoji.hpp +++ b/discord/emoji.hpp @@ -18,6 +18,8 @@ struct EmojiData { friend void from_json(const nlohmann::json &j, EmojiData &m); friend void to_json(nlohmann::json &j, const EmojiData &m); - std::string GetURL() const; - static std::string URLFromID(std::string emoji_id, std::string ext = "png"); + std::string GetURL(const char *ext = "png", const char *size = nullptr) const; + static std::string URLFromID(const std::string &emoji_id, const char *ext = "png", const char *size = nullptr); + static std::string URLFromID(Snowflake emoji_id, const char *ext = "png", const char *size = nullptr); + static std::string URLFromID(const Glib::ustring &emoji_id, const char *ext = "png", const char *size = nullptr); }; diff --git a/discord/objects.cpp b/discord/objects.cpp index 5d451f6..3aef9ee 100644 --- a/discord/objects.cpp +++ b/discord/objects.cpp @@ -386,3 +386,11 @@ void to_json(nlohmann::json &j, const ModifyGuildRolePositionsObject::PositionPa void to_json(nlohmann::json &j, const ModifyGuildRolePositionsObject &m) { j = m.Positions; } + +void from_json(const nlohmann::json &j, GuildEmojisUpdateObject &m) { + JS_D("guild_id", m.GuildID); +} + +void to_json(nlohmann::json &j, const ModifyGuildEmojiObject &m) { + JS_IF("name", m.Name); +} diff --git a/discord/objects.hpp b/discord/objects.hpp index 5ada2ee..88cac21 100644 --- a/discord/objects.hpp +++ b/discord/objects.hpp @@ -64,6 +64,7 @@ enum class GatewayEvent : int { INVITE_DELETE, USER_NOTE_UPDATE, READY_SUPPLEMENTAL, + GUILD_EMOJIS_UPDATE, }; enum class GatewayCloseCode : uint16_t { @@ -539,3 +540,18 @@ struct ModifyGuildRolePositionsObject { friend void to_json(nlohmann::json &j, const ModifyGuildRolePositionsObject &m); }; + +struct GuildEmojisUpdateObject { + Snowflake GuildID; + // std::vector<EmojiData> Emojis; + // GuildHashes, undocumented + + friend void from_json(const nlohmann::json &j, GuildEmojisUpdateObject &m); +}; + +struct ModifyGuildEmojiObject { + std::optional<std::string> Name; + // std::optional<std::vector<Snowflake>> Roles; + + friend void to_json(nlohmann::json &j, const ModifyGuildEmojiObject &m); +}; diff --git a/windows/guildsettings/emojispane.cpp b/windows/guildsettings/emojispane.cpp new file mode 100644 index 0000000..178b533 --- /dev/null +++ b/windows/guildsettings/emojispane.cpp @@ -0,0 +1,255 @@ +#include "emojispane.hpp" +#include "../../abaddon.hpp" +#include "../../components/cellrendererpixbufanimation.hpp" + +GuildSettingsEmojisPane::GuildSettingsEmojisPane(Snowflake guild_id) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , GuildID(guild_id) + , m_model(Gtk::ListStore::create(m_columns)) + , m_filter(Gtk::TreeModelFilter::create(m_model)) + , m_menu_copy_id("Copy ID") + , m_menu_delete("Delete") + , m_menu_copy_emoji_url("Copy Emoji URL") + , m_menu_show_emoji("Open in Browser") { + set_name("guild-emojis-pane"); + + m_view_scroll.set_hexpand(true); + m_view_scroll.set_vexpand(true); + + m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnTreeButtonPress), false); + + m_menu_copy_id.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnMenuCopyID)); + m_menu_delete.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnMenuDelete)); + m_menu_copy_emoji_url.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnMenuCopyEmojiURL)); + m_menu_show_emoji.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnMenuShowEmoji)); + + m_menu.append(m_menu_delete); + m_menu.append(m_menu_copy_id); + m_menu.append(m_menu_copy_emoji_url); + m_menu.append(m_menu_show_emoji); + m_menu.show_all(); + + auto &discord = Abaddon::Get().GetDiscordClient(); + auto &img = Abaddon::Get().GetImageManager(); + + discord.signal_guild_emojis_update().connect(sigc::hide<0>(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnFetchEmojis))); + + const auto self_id = discord.GetUserData().ID; + const bool can_manage = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_EMOJIS); + m_menu_delete.set_sensitive(can_manage); + + m_search.set_placeholder_text("Filter"); + m_search.signal_changed().connect([this]() { + m_filter->refilter(); + }); + + m_view_scroll.add(m_view); + add(m_search); + add(m_view_scroll); + m_search.show(); + m_view.show(); + m_view_scroll.show(); + + m_filter->set_visible_func([this](const Gtk::TreeModel::const_iterator &iter) -> bool { + const auto text = m_search.get_text(); + if (text == "") return true; + return StringContainsCaseless((*iter)[m_columns.m_col_name], text); + }); + m_view.set_enable_search(false); + m_view.set_model(m_filter); + + auto *column = Gtk::manage(new Gtk::TreeView::Column("Emoji")); + auto *renderer = Gtk::manage(new CellRendererPixbufAnimation); + column->pack_start(*renderer); + column->add_attribute(renderer->property_pixbuf(), m_columns.m_col_pixbuf); + column->add_attribute(renderer->property_pixbuf_animation(), m_columns.m_col_pixbuf_animation); + m_view.append_column(*column); + + if (can_manage) { + auto *column = Gtk::manage(new Gtk::TreeView::Column("Name")); + auto *renderer = Gtk::manage(new Gtk::CellRendererText); + column->pack_start(*renderer); + column->add_attribute(renderer->property_text(), m_columns.m_col_name); + renderer->property_editable() = true; + renderer->signal_edited().connect([this, renderer, column](const Glib::ustring &path, const Glib::ustring &text) { + std::string new_str; + int size = 0; + for (const auto ch : text) { + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') + new_str += ch; + else if (ch == ' ') + new_str += '_'; + if (++size == 32) break; + } + if (auto row = *m_model->get_iter(path)) { + row[m_columns.m_col_name] = new_str; + OnEditName(row[m_columns.m_col_id], new_str); + } + }); + m_view.append_column(*column); + } else + m_view.append_column("Name", m_columns.m_col_name); + if (can_manage) + m_view.append_column("Creator", m_columns.m_col_creator); + m_view.append_column("Is Animated?", m_columns.m_col_animated); + + for (const auto column : m_view.get_columns()) + column->set_resizable(true); +} + +void GuildSettingsEmojisPane::on_switched_to() { + m_view.grab_focus(); + + if (m_requested) return; + m_requested = true; + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + const bool can_manage = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_EMOJIS); + m_menu_delete.set_sensitive(can_manage); + + discord.FetchGuildEmojis(GuildID, sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnFetchEmojis)); +} + +void GuildSettingsEmojisPane::AddEmojiRow(const EmojiData &emoji) { + auto &img = Abaddon::Get().GetImageManager(); + + auto &row = *m_model->append(); + + row[m_columns.m_col_id] = emoji.ID; + row[m_columns.m_col_pixbuf] = img.GetPlaceholder(32); + row[m_columns.m_col_name] = emoji.Name; + if (emoji.Creator.has_value()) + row[m_columns.m_col_creator] = emoji.Creator->Username + "#" + emoji.Creator->Discriminator; + if (emoji.IsAnimated.has_value()) + row[m_columns.m_col_animated] = *emoji.IsAnimated ? "Yes" : "No"; + else + row[m_columns.m_col_animated] = "No"; + if (emoji.IsAvailable.has_value()) + row[m_columns.m_col_available] = *emoji.IsAvailable ? "Yes" : "No"; + else + row[m_columns.m_col_available] = "Yes"; + + static bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations(); + if (show_animations && emoji.IsAnimated.has_value() && *emoji.IsAnimated) { + const auto cb = [this, id = emoji.ID](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) { + for (auto &row : m_model->children()) { + if (static_cast<Snowflake>(row[m_columns.m_col_id]) == id) { + row[m_columns.m_col_pixbuf_animation] = pb; + return; + } + } + }; + img.LoadAnimationFromURL(emoji.GetURL("gif"), 32, 32, sigc::track_obj(cb, *this)); + } else { + const auto cb = [this, id = emoji.ID](const Glib::RefPtr<Gdk::Pixbuf> &pb) { + for (auto &row : m_model->children()) { + if (static_cast<Snowflake>(row[m_columns.m_col_id]) == id) { + row[m_columns.m_col_pixbuf] = pb->scale_simple(32, 32, Gdk::INTERP_BILINEAR); + return; + } + } + }; + img.LoadFromURL(emoji.GetURL(), sigc::track_obj(cb, *this)); + } +} + +void GuildSettingsEmojisPane::OnFetchEmojis(std::vector<EmojiData> emojis) { + m_model->clear(); + + // put animated emojis at the end then sort alphabetically + std::sort(emojis.begin(), emojis.end(), [&](const EmojiData &a, const EmojiData &b) { + const bool a_is_animated = a.IsAnimated.has_value() && *a.IsAnimated; + const bool b_is_animated = b.IsAnimated.has_value() && *b.IsAnimated; + if (a_is_animated == b_is_animated) + return a.Name < b.Name; + else if (a_is_animated && !b_is_animated) + return false; + else if (!a_is_animated && b_is_animated) + return true; + return false; // this wont happen please be quiet compiler + }); + + for (const auto &emoji : emojis) + AddEmojiRow(emoji); +} + +void GuildSettingsEmojisPane::OnEditName(Snowflake id, const std::string &name) { + const auto cb = [this](bool success) { + if (!success) { + Gtk::MessageDialog dlg("Failed to set emoji name", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK); + dlg.run(); + } + }; + Abaddon::Get().GetDiscordClient().ModifyEmojiName(GuildID, id, name, cb); +} + +void GuildSettingsEmojisPane::OnMenuCopyID() { + if (auto selected_row = *m_view.get_selection()->get_selected()) { + const auto id = static_cast<Snowflake>(selected_row[m_columns.m_col_id]); + Gtk::Clipboard::get()->set_text(std::to_string(id)); + } +} + +void GuildSettingsEmojisPane::OnMenuDelete() { + if (auto selected_row = *m_view.get_selection()->get_selected()) { + const auto name = static_cast<Glib::ustring>(selected_row[m_columns.m_col_name]); + const auto id = static_cast<Snowflake>(selected_row[m_columns.m_col_id]); + if (auto *window = dynamic_cast<Gtk::Window *>(get_toplevel())) + if (Abaddon::Get().ShowConfirm("Are you sure you want to delete " + name + "?", window)) { + const auto cb = [this](bool success) { + if (!success) { + Gtk::MessageDialog dlg("Failed to delete emoji", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK); + dlg.run(); + } + }; + Abaddon::Get().GetDiscordClient().DeleteEmoji(GuildID, id, cb); + } + } +} + +void GuildSettingsEmojisPane::OnMenuCopyEmojiURL() { + if (auto selected_row = *m_view.get_selection()->get_selected()) { + const auto id = static_cast<Snowflake>(selected_row[m_columns.m_col_id]); + const bool is_animated = static_cast<Glib::ustring>(selected_row[m_columns.m_col_animated]) == "Yes"; + Gtk::Clipboard::get()->set_text(EmojiData::URLFromID(id, is_animated ? "gif" : "png", "256")); + } +} + +void GuildSettingsEmojisPane::OnMenuShowEmoji() { + if (auto selected_row = *m_view.get_selection()->get_selected()) { + const auto id = static_cast<Snowflake>(selected_row[m_columns.m_col_id]); + const bool is_animated = static_cast<Glib::ustring>(selected_row[m_columns.m_col_animated]) == "Yes"; + LaunchBrowser(EmojiData::URLFromID(id, is_animated ? "gif" : "png", "256")); + } +} + +bool GuildSettingsEmojisPane::OnTreeButtonPress(GdkEventButton *event) { + if (event->button == GDK_BUTTON_SECONDARY) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + const bool can_manage = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_EMOJIS); + m_menu_delete.set_sensitive(can_manage); + + auto selection = m_view.get_selection(); + Gtk::TreeModel::Path path; + if (m_view.get_path_at_pos(event->x, event->y, path)) { + m_view.get_selection()->select(path); + m_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } + + return true; + } + + return false; +} + +GuildSettingsEmojisPane::ModelColumns::ModelColumns() { + add(m_col_id); + add(m_col_pixbuf); + add(m_col_pixbuf_animation); + add(m_col_name); + add(m_col_creator); + add(m_col_animated); + add(m_col_available); +} diff --git a/windows/guildsettings/emojispane.hpp b/windows/guildsettings/emojispane.hpp new file mode 100644 index 0000000..806babb --- /dev/null +++ b/windows/guildsettings/emojispane.hpp @@ -0,0 +1,55 @@ +#pragma once +#include <gtkmm.h> +#include "../../components/inotifyswitched.hpp" +#include "../../discord/emoji.hpp" + +class GuildSettingsEmojisPane : public Gtk::Box + , public INotifySwitched { +public: + GuildSettingsEmojisPane(Snowflake guild_id); + +private: + void on_switched_to() override; + + bool m_requested = false; + + void AddEmojiRow(const EmojiData &emoji); + + void OnFetchEmojis(std::vector<EmojiData> emojis); + + void OnEditName(Snowflake id, const std::string &name); + void OnMenuCopyID(); + void OnMenuDelete(); + void OnMenuCopyEmojiURL(); + void OnMenuShowEmoji(); + bool OnTreeButtonPress(GdkEventButton *event); + + Snowflake GuildID; + + Gtk::Entry m_search; + Gtk::ScrolledWindow m_view_scroll; + Gtk::TreeView m_view; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns(); + + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_col_pixbuf; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::PixbufAnimation>> m_col_pixbuf_animation; + Gtk::TreeModelColumn<Glib::ustring> m_col_name; + Gtk::TreeModelColumn<Glib::ustring> m_col_creator; + Gtk::TreeModelColumn<Glib::ustring> m_col_animated; + Gtk::TreeModelColumn<Glib::ustring> m_col_available; + Gtk::TreeModelColumn<Snowflake> m_col_id; + }; + + ModelColumns m_columns; + Glib::RefPtr<Gtk::ListStore> m_model; + Glib::RefPtr<Gtk::TreeModelFilter> m_filter; + + Gtk::Menu m_menu; + Gtk::MenuItem m_menu_delete; + Gtk::MenuItem m_menu_copy_id; + Gtk::MenuItem m_menu_copy_emoji_url; + Gtk::MenuItem m_menu_show_emoji; +}; diff --git a/windows/guildsettingswindow.cpp b/windows/guildsettingswindow.cpp index bcbc393..e3b66f7 100644 --- a/windows/guildsettingswindow.cpp +++ b/windows/guildsettingswindow.cpp @@ -10,7 +10,8 @@ GuildSettingsWindow::GuildSettingsWindow(Snowflake id) , m_pane_invites(id) , m_pane_audit_log(id) , m_pane_members(id) - , m_pane_roles(id) { + , m_pane_roles(id) + , m_pane_emojis(id) { auto &discord = Abaddon::Get().GetDiscordClient(); const auto guild = *discord.GetGuild(id); @@ -51,6 +52,7 @@ GuildSettingsWindow::GuildSettingsWindow(Snowflake id) m_pane_roles.show(); m_pane_bans.show(); m_pane_invites.show(); + m_pane_emojis.show(); m_pane_audit_log.show(); m_stack.set_transition_duration(100); @@ -68,6 +70,7 @@ GuildSettingsWindow::GuildSettingsWindow(Snowflake id) m_stack.add(m_pane_bans, "bans", "Bans"); if (discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_GUILD)) m_stack.add(m_pane_invites, "invites", "Invites"); + m_stack.add(m_pane_emojis, "emojis", "Emojis"); if (discord.HasGuildPermission(self_id, GuildID, Permission::VIEW_AUDIT_LOG)) m_stack.add(m_pane_audit_log, "audit-log", "Audit Log"); m_stack.show(); diff --git a/windows/guildsettingswindow.hpp b/windows/guildsettingswindow.hpp index 3d9d857..7447840 100644 --- a/windows/guildsettingswindow.hpp +++ b/windows/guildsettingswindow.hpp @@ -7,6 +7,7 @@ #include "guildsettings/auditlogpane.hpp" #include "guildsettings/memberspane.hpp" #include "guildsettings/rolespane.hpp" +#include "guildsettings/emojispane.hpp" class GuildSettingsWindow : public Gtk::Window { public: @@ -22,6 +23,7 @@ private: GuildSettingsRolesPane m_pane_roles; GuildSettingsBansPane m_pane_bans; GuildSettingsInvitesPane m_pane_invites; + GuildSettingsEmojisPane m_pane_emojis; GuildSettingsAuditLogPane m_pane_audit_log; Snowflake GuildID; |