diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84d93e0..0d4870c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,8 @@ jobs: del /f /s /q "${{ runner.workspace }}\build\CMakeCache.txt" xcopy /E /I "${{ github.workspace }}\css" "${{ runner.workspace }}\build\css" xcopy /E /I "${{ github.workspace }}\res" "${{ runner.workspace }}\build\res" + mkdir "${{ runner.workspace }}\build\share" + xcopy /E /I "${{ github.workspace }}\ci\gtk-for-windows\gtk-nsis-pack\share\glib-2.0" "${{ runner.workspace }}\build\share\glib-2.0" copy "${{ github.workspace }}\ci\vcpkg\installed\x64-windows\tools\glib\gspawn-win64-helper.exe" "${{ runner.workspace }}\build\gspawn-win64-helper.exe" copy "${{ github.workspace }}\ci\vcpkg\installed\x64-windows\tools\glib\gspawn-win64-helper-console.exe" "${{ runner.workspace }}\build\gspawn-win64-helper-console.exe" diff --git a/.gitmodules b/.gitmodules index 58279b0..bcbf397 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "ci/vcpkg"] path = ci/vcpkg url = https://github.com/microsoft/vcpkg +[submodule "ci/gtk-for-windows"] + path = ci/gtk-for-windows + url = https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a348ad..80e617e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,8 @@ file(GLOB ABADDON_SOURCES "components/*.cpp" "windows/*.hpp" "windows/*.cpp" + "windows/guildsettings/*.hpp" + "windows/guildsettings/*.cpp" "dialogs/*.hpp" "dialogs/*.cpp" ) diff --git a/abaddon.cpp b/abaddon.cpp index 06a9912..2578e8e 100644 --- a/abaddon.cpp +++ b/abaddon.cpp @@ -9,6 +9,7 @@ #include "dialogs/confirm.hpp" #include "dialogs/setstatus.hpp" #include "abaddon.hpp" +#include "windows/guildsettingswindow.hpp" #ifdef _WIN32 #pragma comment(lib, "crypt32.lib") @@ -105,6 +106,7 @@ int Abaddon::StartGTK() { m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened)); m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild)); + m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings)); m_main_window->GetChatWindow()->signal_action_message_delete().connect(sigc::mem_fun(*this, &Abaddon::ActionChatDeleteMessage)); m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage)); @@ -231,6 +233,10 @@ const SettingsManager &Abaddon::GetSettings() const { return m_settings; } +Glib::RefPtr Abaddon::GetStyleProvider() { + return m_css_provider; +} + void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_id) { m_shown_user_menu_id = id; m_shown_user_menu_guild_id = guild_id; @@ -455,6 +461,11 @@ void Abaddon::ActionReactionRemove(Snowflake id, const Glib::ustring ¶m) { m_discord.RemoveReaction(id, param); } +void Abaddon::ActionGuildSettings(Snowflake id) { + auto *window = new GuildSettingsWindow(id); + window->show(); +} + void Abaddon::ActionReloadSettings() { m_settings.Reload(); } diff --git a/abaddon.hpp b/abaddon.hpp index 649578e..4b409a3 100644 --- a/abaddon.hpp +++ b/abaddon.hpp @@ -45,6 +45,7 @@ public: void ActionSetStatus(); void ActionReactionAdd(Snowflake id, const Glib::ustring ¶m); void ActionReactionRemove(Snowflake id, const Glib::ustring ¶m); + void ActionGuildSettings(Snowflake id); void ActionReloadSettings(); void ActionReloadCSS(); @@ -73,6 +74,8 @@ public: const SettingsManager &GetSettings() const; + Glib::RefPtr GetStyleProvider(); + protected: Snowflake m_shown_user_menu_id; Snowflake m_shown_user_menu_guild_id; diff --git a/ci/gtk-for-windows b/ci/gtk-for-windows new file mode 160000 index 0000000..27bc126 --- /dev/null +++ b/ci/gtk-for-windows @@ -0,0 +1 @@ +Subproject commit 27bc126a0d9f1a3ac46c6cd7ce2e6b9eea813127 diff --git a/components/channels.cpp b/components/channels.cpp index 64613a4..0819f8c 100644 --- a/components/channels.cpp +++ b/components/channels.cpp @@ -117,6 +117,12 @@ ChannelListRowGuild::ChannelListRowGuild(const GuildData *data) { }); m_menu.append(*m_menu_leave); + m_menu_settings = Gtk::manage(new Gtk::MenuItem("Guild _Settings", true)); + m_menu_settings->signal_activate().connect([this]() { + m_signal_settings.emit(); + }); + m_menu.append(*m_menu_settings); + m_menu.show_all(); const auto show_animations = Abaddon::Get().GetSettings().GetShowAnimations(); @@ -176,6 +182,10 @@ ChannelListRowGuild::type_signal_leave ChannelListRowGuild::signal_leave() { return m_signal_leave; } +ChannelListRowGuild::type_signal_settings ChannelListRowGuild::signal_settings() { + return m_signal_settings; +} + ChannelListRowCategory::ChannelListRowCategory(const ChannelData *data) { ID = data->ID; m_ev = Gtk::manage(new Gtk::EventBox); @@ -519,6 +529,7 @@ void ChannelList::UpdateGuild(Snowflake id) { m_guild_id_to_row[new_row->ID] = new_row; new_row->signal_leave().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnGuildMenuLeave), new_row->ID)); new_row->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), new_row->ID)); + new_row->signal_settings().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnGuildMenuSettings), new_row->ID)); new_row->Children = children; for (auto child : children) child->Parent = new_row; @@ -615,6 +626,7 @@ void ChannelList::InsertGuildAt(Snowflake id, int pos) { m_guild_id_to_row[guild_row->ID] = guild_row; guild_row->signal_leave().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnGuildMenuLeave), guild_row->ID)); guild_row->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), guild_row->ID)); + guild_row->signal_settings().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnGuildMenuSettings), guild_row->ID)); // add channels with no parent category for (const auto &[pos, channel] : orphan_channels) { @@ -726,6 +738,10 @@ void ChannelList::OnGuildMenuLeave(Snowflake id) { m_signal_action_guild_leave.emit(id); } +void ChannelList::OnGuildMenuSettings(Snowflake id) { + m_signal_action_guild_settings.emit(id); +} + void ChannelList::CheckBumpDM(Snowflake channel_id) { auto it = m_dm_id_to_row.find(channel_id); if (it == m_dm_id_to_row.end()) return; @@ -756,3 +772,7 @@ ChannelList::type_signal_action_channel_item_select ChannelList::signal_action_c ChannelList::type_signal_action_guild_leave ChannelList::signal_action_guild_leave() { return m_signal_action_guild_leave; } + +ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_settings() { + return m_signal_action_guild_settings; +} diff --git a/components/channels.hpp b/components/channels.hpp index 15632ab..8f73a50 100644 --- a/components/channels.hpp +++ b/components/channels.hpp @@ -64,17 +64,21 @@ protected: Gtk::Menu m_menu; Gtk::MenuItem *m_menu_copyid; Gtk::MenuItem *m_menu_leave; + Gtk::MenuItem *m_menu_settings; private: typedef sigc::signal type_signal_copy_id; typedef sigc::signal type_signal_leave; + typedef sigc::signal type_signal_settings; type_signal_copy_id m_signal_copy_id; type_signal_leave m_signal_leave; + type_signal_settings m_signal_settings; public: type_signal_copy_id signal_copy_id(); type_signal_leave signal_leave(); + type_signal_settings signal_settings(); }; class ChannelListRowCategory : public ChannelListRow { @@ -156,6 +160,7 @@ protected: int m_guild_count; void OnMenuCopyID(Snowflake id); void OnGuildMenuLeave(Snowflake id); + void OnGuildMenuSettings(Snowflake id); Gtk::Menu m_channel_menu; Gtk::MenuItem *m_channel_menu_copyid; @@ -179,11 +184,14 @@ protected: public: typedef sigc::signal type_signal_action_channel_item_select; typedef sigc::signal type_signal_action_guild_leave; + typedef sigc::signal type_signal_action_guild_settings; type_signal_action_channel_item_select signal_action_channel_item_select(); type_signal_action_guild_leave signal_action_guild_leave(); + type_signal_action_guild_settings signal_action_guild_settings(); protected: type_signal_action_channel_item_select m_signal_action_channel_item_select; type_signal_action_guild_leave m_signal_action_guild_leave; + type_signal_action_guild_settings m_signal_action_guild_settings; }; diff --git a/css/main.css b/css/main.css index d7d4108..b1f8705 100644 --- a/css/main.css +++ b/css/main.css @@ -162,16 +162,40 @@ color: @text_color; } -paned separator { +.app-window label:not(:disabled) { + color: @text_color; +} + +.app-window entry { + background: @secondary_color; + color: @text_color; + border: 1px solid #1c2e40; +} + +.app-window button { + background: @secondary_color; + color: @text_color; + border: 1px solid #1c2e40; +} + +.app-window.background { + background: @background_color; +} + +.app-window listbox { + background: @background_color; +} + +.app-window paned separator { background: #37474f; } -scrollbar { +.app-window scrollbar { background: @background_color; border-left: 1px solid transparent; } -menubar, menu { +.app-window menubar, menu { background: @background_color; color: #cccccc; } diff --git a/dialogs/confirm.cpp b/dialogs/confirm.cpp index 0881dd9..68902a5 100644 --- a/dialogs/confirm.cpp +++ b/dialogs/confirm.cpp @@ -7,6 +7,7 @@ ConfirmDialog::ConfirmDialog(Gtk::Window &parent) , m_ok("OK") , m_cancel("Cancel") { set_default_size(300, 50); + get_style_context()->add_class("app-window"); m_label.set_text("Are you sure?"); diff --git a/dialogs/editmessage.cpp b/dialogs/editmessage.cpp index a5f583d..23f0fe1 100644 --- a/dialogs/editmessage.cpp +++ b/dialogs/editmessage.cpp @@ -7,6 +7,7 @@ EditMessageDialog::EditMessageDialog(Gtk::Window &parent) , m_ok("OK") , m_cancel("Cancel") { set_default_size(300, 50); + get_style_context()->add_class("app-window"); m_ok.signal_clicked().connect([&]() { m_content = m_text.get_buffer()->get_text(); diff --git a/dialogs/joinguild.cpp b/dialogs/joinguild.cpp index 5fbc6c0..6fd21a8 100644 --- a/dialogs/joinguild.cpp +++ b/dialogs/joinguild.cpp @@ -10,6 +10,7 @@ JoinGuildDialog::JoinGuildDialog(Gtk::Window &parent) , m_cancel("Cancel") , m_info("Enter code") { set_default_size(300, 50); + get_style_context()->add_class("app-window"); Glib::signal_idle().connect(sigc::mem_fun(*this, &JoinGuildDialog::on_idle_slot)); diff --git a/dialogs/setstatus.cpp b/dialogs/setstatus.cpp index cbd42ff..4c36325 100644 --- a/dialogs/setstatus.cpp +++ b/dialogs/setstatus.cpp @@ -8,6 +8,7 @@ SetStatusDialog::SetStatusDialog(Gtk::Window &parent) , m_ok("OK") , m_cancel("Cancel") { set_default_size(300, 50); + get_style_context()->add_class("app-window"); m_text.set_placeholder_text("Status text"); diff --git a/dialogs/token.cpp b/dialogs/token.cpp index ca016a0..b36bb84 100644 --- a/dialogs/token.cpp +++ b/dialogs/token.cpp @@ -7,6 +7,7 @@ TokenDialog::TokenDialog(Gtk::Window &parent) , m_ok("OK") , m_cancel("Cancel") { set_default_size(300, 50); + get_style_context()->add_class("app-window"); m_ok.signal_clicked().connect([&]() { m_token = m_entry.get_text(); diff --git a/discord/discord.cpp b/discord/discord.cpp index 4e1a35e..a3acd2a 100644 --- a/discord/discord.cpp +++ b/discord/discord.cpp @@ -437,6 +437,36 @@ void DiscordClient::RemoveReaction(Snowflake id, Glib::ustring param) { m_http.MakeDELETE("/channels/" + std::to_string(channel_id) + "/messages/" + std::to_string(id) + "/reactions/" + param + "/@me", [](auto) {}); } +void DiscordClient::SetGuildName(Snowflake id, const Glib::ustring &name) { + SetGuildName(id, name, [](auto) {}); +} + +void DiscordClient::SetGuildName(Snowflake id, const Glib::ustring &name, sigc::slot callback) { + ModifyGuildObject obj; + obj.Name = name; + sigc::signal signal; + signal.connect(callback); + m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, signal](const cpr::Response &r) { + const auto success = r.status_code == 200; + signal.emit(success); + }); +} + +void DiscordClient::SetGuildIcon(Snowflake id, const std::string &data) { + SetGuildIcon(id, data, [](auto) {}); +} + +void DiscordClient::SetGuildIcon(Snowflake id, const std::string &data, sigc::slot callback) { + ModifyGuildObject obj; + obj.IconData = data; + sigc::signal signal; + signal.connect(callback); + m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, signal](const cpr::Response &r) { + const auto success = r.status_code == 200; + signal.emit(success); + }); +} + void DiscordClient::UpdateToken(std::string token) { if (!IsStarted()) { m_token = token; diff --git a/discord/discord.hpp b/discord/discord.hpp index c2b2bef..5b08bef 100644 --- a/discord/discord.hpp +++ b/discord/discord.hpp @@ -110,6 +110,10 @@ public: std::optional FindDM(Snowflake user_id); // wont find group dms void AddReaction(Snowflake id, Glib::ustring param); void RemoveReaction(Snowflake id, Glib::ustring param); + void SetGuildName(Snowflake id, const Glib::ustring &name); + void SetGuildName(Snowflake id, const Glib::ustring &name, sigc::slot callback); + void SetGuildIcon(Snowflake id, const std::string &data); + void SetGuildIcon(Snowflake id, const std::string &data, sigc::slot callback); void UpdateToken(std::string token); void SetUserAgent(std::string agent); diff --git a/discord/guild.cpp b/discord/guild.cpp index 40762ad..8b8a5c2 100644 --- a/discord/guild.cpp +++ b/discord/guild.cpp @@ -118,6 +118,13 @@ void GuildData::update_from_json(const nlohmann::json &j) { JS_RD("approximate_presence_count", ApproximatePresenceCount); } +bool GuildData::HasFeature(const std::string &search_feature) { + for (const auto &feature : Features) + if (search_feature == feature) + return true; + return false; +} + bool GuildData::HasIcon() const { return Icon != ""; } diff --git a/discord/guild.hpp b/discord/guild.hpp index c8676b3..eeb62cf 100644 --- a/discord/guild.hpp +++ b/discord/guild.hpp @@ -67,6 +67,7 @@ struct GuildData { friend void from_json(const nlohmann::json &j, GuildData &m); void update_from_json(const nlohmann::json &j); + bool HasFeature(const std::string &feature); bool HasIcon() const; bool HasAnimatedIcon() const; std::string GetIconURL(std::string ext = "png", std::string size = "32") const; diff --git a/discord/objects.cpp b/discord/objects.cpp index cf0f504..07e616b 100644 --- a/discord/objects.cpp +++ b/discord/objects.cpp @@ -247,3 +247,8 @@ void from_json(const nlohmann::json &j, TypingStartObject &m) { JS_D("timestamp", m.Timestamp); JS_O("member", m.Member); } + +void to_json(nlohmann::json &j, const ModifyGuildObject &m) { + JS_IF("name", m.Name); + JS_IF("icon", m.IconData); +} diff --git a/discord/objects.hpp b/discord/objects.hpp index 9c5e648..89770a5 100644 --- a/discord/objects.hpp +++ b/discord/objects.hpp @@ -345,3 +345,11 @@ struct TypingStartObject { friend void from_json(const nlohmann::json &j, TypingStartObject &m); }; + +// implement rest as needed +struct ModifyGuildObject { + std::optional Name; + std::optional IconData; + + friend void to_json(nlohmann::json &j, const ModifyGuildObject &m); +}; diff --git a/util.cpp b/util.cpp index bd70a21..38e65c5 100644 --- a/util.cpp +++ b/util.cpp @@ -100,8 +100,17 @@ std::string IntToCSSColor(int color) { } void AddWidgetMenuHandler(Gtk::Widget *widget, Gtk::Menu &menu) { - widget->signal_button_press_event().connect([&menu](GdkEventButton *ev) -> bool { + AddWidgetMenuHandler(widget, menu, []() {}); +} + +// so widgets can modify the menu before it is displayed +// maybe theres a better way to do this idk +void AddWidgetMenuHandler(Gtk::Widget *widget, Gtk::Menu &menu, sigc::slot pre_callback) { + sigc::signal signal; + signal.connect(pre_callback); + widget->signal_button_press_event().connect([&menu, signal](GdkEventButton *ev) -> bool { if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_SECONDARY) { + signal.emit(); menu.popup_at_pointer(reinterpret_cast(ev)); return true; } diff --git a/util.hpp b/util.hpp index f5f9edf..cab3461 100644 --- a/util.hpp +++ b/util.hpp @@ -39,6 +39,7 @@ void LaunchBrowser(Glib::ustring url); void GetImageDimensions(int inw, int inh, int &outw, int &outh, int clampw = 400, int clamph = 300); std::string IntToCSSColor(int color); void AddWidgetMenuHandler(Gtk::Widget *widget, Gtk::Menu &menu); +void AddWidgetMenuHandler(Gtk::Widget *widget, Gtk::Menu &menu, sigc::slot pre_callback); std::vector StringSplit(const std::string &str, const char *delim); std::string GetExtension(std::string url); bool IsURLViewableImage(const std::string &url); diff --git a/windows/guildsettings/infopane.cpp b/windows/guildsettings/infopane.cpp new file mode 100644 index 0000000..862c082 --- /dev/null +++ b/windows/guildsettings/infopane.cpp @@ -0,0 +1,206 @@ +#include "infopane.hpp" +#include "../../abaddon.hpp" +#include + +GuildSettingsInfoPane::GuildSettingsInfoPane(Snowflake id) + : GuildID(id) + , m_guild_name_label("Guild name") + , m_guild_icon_label("Guild icon") { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto guild = *discord.GetGuild(id); + const auto self_id = discord.GetUserData().ID; + const auto can_modify = discord.HasGuildPermission(self_id, id, Permission::MANAGE_GUILD); + + set_name("guild-info-pane"); + set_margin_top(10); + set_margin_bottom(10); + set_margin_left(10); + set_margin_top(10); + + m_guild_name.set_sensitive(can_modify); + m_guild_name.set_text(guild.Name); + m_guild_name.signal_focus_out_event().connect([this](GdkEventFocus *e) -> bool { + UpdateGuildName(); + return false; + }); + m_guild_name.signal_key_press_event().connect([this](GdkEventKey *e) -> bool { + if (e->keyval == GDK_KEY_Return) + UpdateGuildName(); + return false; + // clang-format off + }, false); + // clang-format on + m_guild_name.set_tooltip_text("Press enter or lose focus to submit"); + m_guild_name.show(); + m_guild_name_label.show(); + + auto load_icon_cb = [this](const Glib::RefPtr &pixbuf) { + m_guild_icon.property_pixbuf() = pixbuf->scale_simple(64, 64, Gdk::INTERP_BILINEAR); + }; + + auto guild_update_cb = [this, load_icon_cb](Snowflake id) { + if (id != GuildID) return; + const auto guild = *Abaddon::Get().GetDiscordClient().GetGuild(id); + if (guild.HasIcon()) + Abaddon::Get().GetImageManager().LoadFromURL(guild.GetIconURL("png", "64"), sigc::track_obj(load_icon_cb, *this)); + }; + discord.signal_guild_update().connect(sigc::track_obj(guild_update_cb, *this)); + + m_guild_icon.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(32); + if (guild.HasIcon()) { + Abaddon::Get().GetImageManager().LoadFromURL(guild.GetIconURL("png", "64"), sigc::track_obj(load_icon_cb, *this)); + } + m_guild_icon.set_margin_bottom(10); + if (can_modify) { + m_guild_icon_ev.signal_realize().connect([this]() { + auto window = m_guild_icon_ev.get_window(); + auto display = window->get_display(); + auto cursor = Gdk::Cursor::create(display, "pointer"); + window->set_cursor(cursor); + }); + + m_guild_icon_ev.signal_button_press_event().connect([this](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS) + if (event->button == GDK_BUTTON_PRIMARY) + UpdateGuildIconPicker(); + else if (event->button == GDK_BUTTON_SECONDARY) + UpdateGuildIconClipboard(); + + return false; + }); + } + + m_guild_icon_ev.set_tooltip_text("Click to choose a file, right click to paste"); + m_guild_icon.show(); + m_guild_icon_ev.show(); + //m_guild_icon_label.show(); + + m_guild_icon_ev.add(m_guild_icon); + attach(m_guild_icon_ev, 0, 0, 1, 1); + attach(m_guild_name_label, 0, 1, 1, 1); + attach_next_to(m_guild_name, m_guild_name_label, Gtk::POS_RIGHT, 1, 1); + //attach(m_guild_icon_label, 0, 1, 1, 1); + //attach_next_to(m_guild_icon, m_guild_icon_label, Gtk::POS_RIGHT, 1, 1); +} + +void GuildSettingsInfoPane::UpdateGuildName() { + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.GetGuild(GuildID)->Name == m_guild_name.get_text()) return; + + auto cb = [this](bool success) { + if (!success) { + m_guild_name.set_text(Abaddon::Get().GetDiscordClient().GetGuild(GuildID)->Name); + Gtk::MessageDialog dlg("Failed to set guild name", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.run(); + } + }; + discord.SetGuildName(GuildID, m_guild_name.get_text(), sigc::track_obj(cb, *this)); +} + +void GuildSettingsInfoPane::UpdateGuildIconFromData(const std::vector &data, const std::string &mime) { + auto encoded = "data:" + mime + ";base64," + Glib::Base64::encode(std::string(data.begin(), data.end())); + auto &discord = Abaddon::Get().GetDiscordClient(); + + auto cb = [this](bool success) { + if (!success) { + Gtk::MessageDialog dlg("Failed to set guild icon", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.run(); + } + }; + discord.SetGuildIcon(GuildID, encoded, sigc::track_obj(cb, *this)); +} + +void GuildSettingsInfoPane::UpdateGuildIconFromPixbuf(Glib::RefPtr pixbuf) { + int w = pixbuf->get_width(); + int h = pixbuf->get_height(); + if (w > 1024 || h > 1024) { + GetImageDimensions(w, h, w, h, 1024, 1024); + pixbuf = pixbuf->scale_simple(w, h, Gdk::INTERP_BILINEAR); + } + gchar *buffer; + gsize buffer_size; + pixbuf->save_to_buffer(buffer, buffer_size, "png"); + std::vector data(buffer_size); + std::memcpy(data.data(), buffer, buffer_size); + UpdateGuildIconFromData(data, "image/png"); +} + +void GuildSettingsInfoPane::UpdateGuildIconPicker() { + // this picker fucking sucks + Gtk::FileChooserDialog dlg("Choose new guild icon", Gtk::FILE_CHOOSER_ACTION_OPEN); + dlg.get_style_context()->remove_provider(Abaddon::Get().GetStyleProvider()); + dlg.set_modal(true); + dlg.signal_response().connect([this, &dlg](int response) { + if (response == Gtk::RESPONSE_OK) { + auto data = ReadWholeFile(dlg.get_filename()); + if (GetExtension(dlg.get_filename()) == ".gif") + UpdateGuildIconFromData(data, "image/gif"); + else + try { + auto loader = Gdk::PixbufLoader::create(); + loader->signal_size_prepared().connect([&loader](int inw, int inh) { + int w, h; + GetImageDimensions(inw, inh, w, h, 1024, 1024); + loader->set_size(w, h); + }); + loader->write(data.data(), data.size()); + loader->close(); + UpdateGuildIconFromPixbuf(loader->get_pixbuf()); + } catch (const std::exception &) {}; + } + }); + + dlg.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + dlg.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + + auto filter_images = Gtk::FileFilter::create(); + if (Abaddon::Get().GetDiscordClient().GetGuild(GuildID)->HasFeature("ANIMATED_ICON")) { + filter_images->set_name("Supported images (*.jpg, *.jpeg, *.png, *.gif)"); + filter_images->add_pattern("*.gif"); + } else { + filter_images->set_name("Supported images (*.jpg, *.jpeg, *.png)"); + } + filter_images->add_pattern("*.jpg"); + filter_images->add_pattern("*.jpeg"); + filter_images->add_pattern("*.png"); + dlg.add_filter(filter_images); + + auto filter_all = Gtk::FileFilter::create(); + filter_all->set_name("All files (*.*)"); + filter_all->add_pattern("*.*"); + dlg.add_filter(filter_all); + + dlg.run(); +} + +void GuildSettingsInfoPane::UpdateGuildIconClipboard() { + std::vector icon_data; + + auto cb = Gtk::Clipboard::get(); + // query for file path then for actual image + if (cb->wait_is_text_available()) { + auto path = cb->wait_for_text(); + if (!std::filesystem::exists(path.c_str())) return; + auto data = ReadWholeFile(path); + try { + auto loader = Gdk::PixbufLoader::create(); + loader->signal_size_prepared().connect([&loader](int inw, int inh) { + int w, h; + GetImageDimensions(inw, inh, w, h, 1024, 1024); + loader->set_size(w, h); + }); + loader->write(data.data(), data.size()); + loader->close(); + auto pb = loader->get_pixbuf(); + UpdateGuildIconFromPixbuf(pb); + + return; + } catch (const std::exception &) {}; + } + + if (cb->wait_is_image_available()) { + auto pb = cb->wait_for_image(); + UpdateGuildIconFromPixbuf(pb); + return; + } +} diff --git a/windows/guildsettings/infopane.hpp b/windows/guildsettings/infopane.hpp new file mode 100644 index 0000000..8e7be82 --- /dev/null +++ b/windows/guildsettings/infopane.hpp @@ -0,0 +1,24 @@ +#pragma once +#include +#include "../../discord/snowflake.hpp" + +class GuildSettingsInfoPane : public Gtk::Grid { +public: + GuildSettingsInfoPane(Snowflake id); + +private: + void UpdateGuildName(); + void UpdateGuildIconFromData(const std::vector &data, const std::string &mime); + void UpdateGuildIconFromPixbuf(Glib::RefPtr pixbuf); + void UpdateGuildIconPicker(); + void UpdateGuildIconClipboard(); + + Gtk::Label m_guild_icon_label; + Gtk::EventBox m_guild_icon_ev; // necessary to make custom cursor behave properly + Gtk::Image m_guild_icon; + + Gtk::Label m_guild_name_label; + Gtk::Entry m_guild_name; + + Snowflake GuildID; +}; diff --git a/windows/guildsettingswindow.cpp b/windows/guildsettingswindow.cpp new file mode 100644 index 0000000..618bcf5 --- /dev/null +++ b/windows/guildsettingswindow.cpp @@ -0,0 +1,45 @@ +#include "guildsettingswindow.hpp" +#include "../abaddon.hpp" + +GuildSettingsWindow::GuildSettingsWindow(Snowflake id) + : m_main(Gtk::ORIENTATION_VERTICAL) + , GuildID(id) + , m_pane_info(id) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto guild = *discord.GetGuild(id); + + auto guild_update_cb = [this](Snowflake id) { + if (id != GuildID) return; + const auto guild = *Abaddon::Get().GetDiscordClient().GetGuild(id); + set_title(guild.Name); + if (guild.HasIcon()) + Abaddon::Get().GetImageManager().LoadFromURL(guild.GetIconURL(), sigc::mem_fun(*this, &GuildSettingsWindow::set_icon)); + }; + discord.signal_guild_update().connect(sigc::track_obj(guild_update_cb, *this)); + + set_name("guild-settings"); + set_default_size(800, 600); + set_title(guild.Name); + set_position(Gtk::WIN_POS_CENTER); + get_style_context()->add_class("app-window"); + + if (guild.HasIcon()) { + Abaddon::Get().GetImageManager().LoadFromURL(guild.GetIconURL(), sigc::mem_fun(*this, &GuildSettingsWindow::set_icon)); + } + + m_switcher.set_stack(m_stack); + m_switcher.set_halign(Gtk::ALIGN_CENTER); + m_switcher.set_hexpand(true); + m_switcher.set_margin_top(10); + m_switcher.show(); + + m_pane_info.show(); + + m_stack.add(m_pane_info, "info", "Info"); + m_stack.show(); + + m_main.add(m_switcher); + m_main.add(m_stack); + m_main.show(); + add(m_main); +} diff --git a/windows/guildsettingswindow.hpp b/windows/guildsettingswindow.hpp new file mode 100644 index 0000000..1904216 --- /dev/null +++ b/windows/guildsettingswindow.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include "../discord/snowflake.hpp" +#include "guildsettings/infopane.hpp" + +class GuildSettingsWindow : public Gtk::Window { +public: + GuildSettingsWindow(Snowflake id); + +private: + Gtk::Box m_main; + Gtk::Stack m_stack; + Gtk::StackSwitcher m_switcher; + + GuildSettingsInfoPane m_pane_info; + + Snowflake GuildID; +}; diff --git a/windows/mainwindow.cpp b/windows/mainwindow.cpp index 883801f..32625a0 100644 --- a/windows/mainwindow.cpp +++ b/windows/mainwindow.cpp @@ -7,6 +7,7 @@ MainWindow::MainWindow() , m_chan_chat_paned(Gtk::ORIENTATION_HORIZONTAL) , m_chat_members_paned(Gtk::ORIENTATION_HORIZONTAL) { set_default_size(1200, 800); + get_style_context()->add_class("app-window"); m_menu_discord.set_label("Discord"); m_menu_discord.set_submenu(m_menu_discord_sub);