From dc127d15fb49c108450e691c44ed930a11bb7e59 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:46:15 -0400 Subject: display user list, client side mute --- res/css/application-low-priority.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'res') diff --git a/res/css/application-low-priority.css b/res/css/application-low-priority.css index cf108f4..5078d22 100644 --- a/res/css/application-low-priority.css +++ b/res/css/application-low-priority.css @@ -44,7 +44,7 @@ has to be separate to allow main.css to override certain things background: @secondary_color; } -.app-popup list { +.app-window list, .app-popup list { background: @secondary_color; } -- cgit v1.2.3 From 92c70bda08472d880e23df30661376d526406230 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 5 Oct 2022 18:43:44 -0400 Subject: add per user volume slider --- res/css/application-low-priority.css | 8 ++++++++ src/abaddon.cpp | 6 ++++++ src/audio/manager.cpp | 14 +++++++++++++- src/audio/manager.hpp | 3 +++ src/windows/voicewindow.cpp | 33 +++++++++++++++++++++++++++++---- src/windows/voicewindow.hpp | 3 +++ 6 files changed, 62 insertions(+), 5 deletions(-) (limited to 'res') diff --git a/res/css/application-low-priority.css b/res/css/application-low-priority.css index 5078d22..130033f 100644 --- a/res/css/application-low-priority.css +++ b/res/css/application-low-priority.css @@ -87,3 +87,11 @@ has to be separate to allow main.css to override certain things .app-window colorswatch { box-shadow: 0 1px rgba(0, 0, 0, 0); } + +.app-window scale { + padding-top: 0px; + padding-bottom: 0px; + margin-top: 0px; + margin-bottom: 0px; + color: @text_color; +} diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 6dc5fd1..8a92de7 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -438,6 +438,12 @@ void Abaddon::OnVoiceConnected() { } }); + wnd->signal_user_volume_changed().connect([this](Snowflake id, double volume) { + if (const auto ssrc = m_discord.GetSSRCOfUser(id); ssrc.has_value()) { + m_audio->SetVolumeSSRC(*ssrc, volume); + } + }); + wnd->show(); wnd->signal_hide().connect([this, wnd]() { m_discord.DisconnectFromVoice(); diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 8321c3e..37f696e 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -28,13 +28,18 @@ void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uin AudioManager *mgr = reinterpret_cast(pDevice->pUserData); if (mgr == nullptr) return; std::lock_guard _(mgr->m_mutex); + std::lock_guard _2(mgr->m_volume_ssrc_mutex); auto *pOutputF32 = static_cast(pOutput); for (auto &[ssrc, pair] : mgr->m_sources) { + double volume = 1.0; + if (const auto vol_it = mgr->m_volume_ssrc.find(ssrc); vol_it != mgr->m_volume_ssrc.end()) { + volume = vol_it->second; + } auto &buf = pair.first; const size_t n = std::min(static_cast(buf.size()), static_cast(frameCount * 2ULL)); for (size_t i = 0; i < n; i++) { - pOutputF32[i] += buf[i] / 32768.F; + pOutputF32[i] += volume * buf[i] / 32768.F; } buf.erase(buf.begin(), buf.begin() + n); } @@ -180,6 +185,13 @@ void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) { } } +void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) { + std::lock_guard _(m_volume_ssrc_mutex); + volume *= 0.01; + constexpr const double E = 2.71828182845904523536; + m_volume_ssrc[ssrc] = (std::exp(volume) - 1) / (E - 1); +} + void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { if (m_opus_buffer == nullptr || !m_should_capture) return; diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 277f2f3..4d2299e 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -31,6 +31,7 @@ public: void SetPlayback(bool playback); void SetMuteSSRC(uint32_t ssrc, bool mute); + void SetVolumeSSRC(uint32_t ssrc, double volume); [[nodiscard]] bool OK() const; @@ -62,8 +63,10 @@ private: std::atomic m_should_playback = true; std::unordered_set m_muted_ssrcs; + std::unordered_map m_volume_ssrc; mutable std::mutex m_muted_ssrc_mutex; + mutable std::mutex m_volume_ssrc_mutex; public: using type_signal_opus_packet = sigc::signal; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 847ba81..ed9971e 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -10,16 +10,26 @@ class VoiceWindowUserListEntry : public Gtk::ListBoxRow { public: VoiceWindowUserListEntry(Snowflake id) - : m_main(Gtk::ORIENTATION_HORIZONTAL) + : m_main(Gtk::ORIENTATION_VERTICAL) + , m_horz(Gtk::ORIENTATION_HORIZONTAL) , m_avatar(32, 32) , m_mute("Mute") { m_name.set_halign(Gtk::ALIGN_START); m_name.set_hexpand(true); m_mute.set_halign(Gtk::ALIGN_END); - m_main.add(m_avatar); - m_main.add(m_name); - m_main.add(m_mute); + m_volume.set_range(0.0, 200.0); + m_volume.set_value_pos(Gtk::POS_LEFT); + m_volume.set_value(100.0); + m_volume.signal_value_changed().connect([this]() { + m_signal_volume.emit(m_volume.get_value()); + }); + + m_horz.add(m_avatar); + m_horz.add(m_name); + m_horz.add(m_mute); + m_main.add(m_horz); + m_main.add(m_volume); add(m_main); show_all_children(); @@ -38,18 +48,26 @@ public: private: Gtk::Box m_main; + Gtk::Box m_horz; LazyImage m_avatar; Gtk::Label m_name; Gtk::CheckButton m_mute; + Gtk::Scale m_volume; public: using type_signal_mute_cs = sigc::signal; + using type_signal_volume = sigc::signal; type_signal_mute_cs signal_mute_cs() { return m_signal_mute_cs; } + type_signal_volume signal_volume() { + return m_signal_volume; + } + private: type_signal_mute_cs m_signal_mute_cs; + type_signal_volume m_signal_volume; }; VoiceWindow::VoiceWindow(Snowflake channel_id) @@ -82,6 +100,9 @@ void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { row->signal_mute_cs().connect([this, id](bool is_muted) { m_signal_mute_user_cs.emit(id, is_muted); }); + row->signal_volume().connect([this, id](double volume) { + m_signal_user_volume_changed.emit(id, volume); + }); m_user_list.add(*row); } } @@ -105,4 +126,8 @@ VoiceWindow::type_signal_deafen VoiceWindow::signal_deafen() { VoiceWindow::type_signal_mute_user_cs VoiceWindow::signal_mute_user_cs() { return m_signal_mute_user_cs; } + +VoiceWindow::type_signal_user_volume_changed VoiceWindow::signal_user_volume_changed() { + return m_signal_user_volume_changed; +} #endif diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index 50943f4..da4987e 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -34,14 +34,17 @@ public: using type_signal_mute = sigc::signal; using type_signal_deafen = sigc::signal; using type_signal_mute_user_cs = sigc::signal; + using type_signal_user_volume_changed = sigc::signal; type_signal_mute signal_mute(); type_signal_deafen signal_deafen(); type_signal_mute_user_cs signal_mute_user_cs(); + type_signal_user_volume_changed signal_user_volume_changed(); private: type_signal_mute m_signal_mute; type_signal_deafen m_signal_deafen; type_signal_mute_user_cs m_signal_mute_user_cs; + type_signal_user_volume_changed m_signal_user_volume_changed; }; #endif -- cgit v1.2.3 From c4590f8b2382fb23a733d2cf2f6c645f02e2fc48 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 3 Jan 2023 22:52:41 -0500 Subject: start voice info box --- res/css/main.css | 18 +++++++++ src/components/voiceinfobox.cpp | 89 +++++++++++++++++++++++++++++++++++++++++ src/components/voiceinfobox.hpp | 19 +++++++++ src/discord/discord.cpp | 17 ++++++++ src/discord/discord.hpp | 9 +++++ src/discord/voiceclient.cpp | 5 +++ src/discord/voiceclient.hpp | 6 ++- src/windows/mainwindow.cpp | 13 ++++-- src/windows/mainwindow.hpp | 4 ++ 9 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/components/voiceinfobox.cpp create mode 100644 src/components/voiceinfobox.hpp (limited to 'res') diff --git a/res/css/main.css b/res/css/main.css index ace3b0b..4b76bb4 100644 --- a/res/css/main.css +++ b/res/css/main.css @@ -360,3 +360,21 @@ background-color: #dd3300; margin-left: 1px; } + +.voice-info { + background-color: #0B0B0B; + padding: 5px; + border: 1px solid #202020; +} + +.voice-info-disconnect-image { + color: #DDDDDD; +} + +.voice-info-status { + font-weight: bold; +} + +.voice-info-location { + +} diff --git a/src/components/voiceinfobox.cpp b/src/components/voiceinfobox.cpp new file mode 100644 index 0000000..f4dc6ed --- /dev/null +++ b/src/components/voiceinfobox.cpp @@ -0,0 +1,89 @@ +#include "voiceinfobox.hpp" +#include "abaddon.hpp" +#include "util.hpp" + +VoiceInfoBox::VoiceInfoBox() + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , m_left(Gtk::ORIENTATION_VERTICAL) { + m_disconnect_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { + if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) { + spdlog::get("discord")->debug("Request disconnect from info box"); + Abaddon::Get().GetDiscordClient().DisconnectFromVoice(); + return true; + } + + return false; + }); + + AddPointerCursor(m_disconnect_ev); + + get_style_context()->add_class("voice-info"); + m_status.get_style_context()->add_class("voice-info-status"); + m_location.get_style_context()->add_class("voice-info-location"); + m_disconnect_img.get_style_context()->add_class("voice-info-disconnect-image"); + + m_status.set_label("You shouldn't see me"); + m_location.set_label("You shouldn't see me"); + + Abaddon::Get().GetDiscordClient().signal_voice_requested_connect().connect([this](Snowflake channel_id) { + show(); + + if (const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(channel_id); channel.has_value() && channel->Name.has_value()) { + if (channel->GuildID.has_value()) { + if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*channel->GuildID); guild.has_value()) { + m_location.set_label(*channel->Name + " / " + guild->Name); + return; + } + } + + m_location.set_label(*channel->Name); + return; + } + + m_location.set_label("Unknown"); + }); + + Abaddon::Get().GetDiscordClient().signal_voice_requested_disconnect().connect([this]() { + hide(); + }); + + Abaddon::Get().GetDiscordClient().signal_voice_client_state_update().connect([this](DiscordVoiceClient::State state) { + Glib::ustring label; + switch (state) { + case DiscordVoiceClient::State::ConnectingToWebsocket: + label = "Connecting"; + break; + case DiscordVoiceClient::State::EstablishingConnection: + label = "Establishing connection"; + break; + case DiscordVoiceClient::State::Connected: + label = "Connected"; + break; + case DiscordVoiceClient::State::DisconnectedByServer: + case DiscordVoiceClient::State::DisconnectedByClient: + label = "Disconnected"; + break; + default: + label = "Unknown"; + break; + } + m_status.set_label(label); + }); + + m_status.set_ellipsize(Pango::ELLIPSIZE_END); + m_location.set_ellipsize(Pango::ELLIPSIZE_END); + + m_disconnect_ev.add(m_disconnect_img); + m_disconnect_img.property_icon_name() = "call-stop-symbolic"; + m_disconnect_img.property_icon_size() = 5; + m_disconnect_img.set_hexpand(true); + m_disconnect_img.set_halign(Gtk::ALIGN_END); + + m_left.add(m_status); + m_left.add(m_location); + + add(m_left); + add(m_disconnect_ev); + + show_all_children(); +} diff --git a/src/components/voiceinfobox.hpp b/src/components/voiceinfobox.hpp new file mode 100644 index 0000000..e0bc4e8 --- /dev/null +++ b/src/components/voiceinfobox.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include + +class VoiceInfoBox : public Gtk::Box { +public: + VoiceInfoBox(); + +private: + Gtk::Box m_left; + Gtk::Label m_status; + Gtk::Label m_location; + + Gtk::EventBox m_disconnect_ev; + Gtk::Image m_disconnect_img; +}; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index c068243..44f64a9 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -31,6 +31,9 @@ DiscordClient::DiscordClient(bool mem_store) m_voice.signal_speaking().connect([this](const VoiceSpeakingData &data) { m_signal_voice_speaking.emit(data); }); + m_voice.signal_state_update().connect([this](DiscordVoiceClient::State state) { + m_signal_voice_client_state_update.emit(state); + }); #endif LoadEventMap(); @@ -1186,12 +1189,14 @@ void DiscordClient::ConnectToVoice(Snowflake channel_id) { m.ChannelID = channel_id; m.PreferredRegion = "newark"; m_websocket.Send(m); + m_signal_voice_requested_connect.emit(channel_id); } void DiscordClient::DisconnectFromVoice() { m_voice.Stop(); VoiceStateUpdateMessage m; m_websocket.Send(m); + m_signal_voice_requested_disconnect.emit(); } bool DiscordClient::IsVoiceConnected() const noexcept { @@ -2995,4 +3000,16 @@ DiscordClient::type_signal_voice_user_disconnect DiscordClient::signal_voice_use DiscordClient::type_signal_voice_user_connect DiscordClient::signal_voice_user_connect() { return m_signal_voice_user_connect; } + +DiscordClient::type_signal_voice_requested_connect DiscordClient::signal_voice_requested_connect() { + return m_signal_voice_requested_connect; +} + +DiscordClient::type_signal_voice_requested_disconnect DiscordClient::signal_voice_requested_disconnect() { + return m_signal_voice_requested_disconnect; +} + +DiscordClient::type_signal_voice_client_state_update DiscordClient::signal_voice_client_state_update() { + return m_signal_voice_client_state_update; +} #endif diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 1e500e9..a7e3f79 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -440,6 +440,9 @@ public: using type_signal_voice_speaking = sigc::signal; using type_signal_voice_user_disconnect = sigc::signal; using type_signal_voice_user_connect = sigc::signal; + using type_signal_voice_requested_connect = sigc::signal; + using type_signal_voice_requested_disconnect = sigc::signal; + using type_signal_voice_client_state_update = sigc::signal; #endif type_signal_gateway_ready signal_gateway_ready(); @@ -502,6 +505,9 @@ public: type_signal_voice_speaking signal_voice_speaking(); type_signal_voice_user_disconnect signal_voice_user_disconnect(); type_signal_voice_user_connect signal_voice_user_connect(); + type_signal_voice_requested_connect signal_voice_requested_connect(); + type_signal_voice_requested_disconnect signal_voice_requested_disconnect(); + type_signal_voice_client_state_update signal_voice_client_state_update(); #endif protected: @@ -565,5 +571,8 @@ protected: type_signal_voice_speaking m_signal_voice_speaking; type_signal_voice_user_disconnect m_signal_voice_user_disconnect; type_signal_voice_user_connect m_signal_voice_user_connect; + type_signal_voice_requested_connect m_signal_voice_requested_connect; + type_signal_voice_requested_disconnect m_signal_voice_requested_disconnect; + type_signal_voice_client_state_update m_signal_voice_client_state_update; #endif }; diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 379a65c..ed83e3c 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -423,6 +423,7 @@ void DiscordVoiceClient::KeepaliveThread() { void DiscordVoiceClient::SetState(State state) { m_log->debug("Changing state to {}", GetStateName(state)); m_state = state; + m_signal_state_update.emit(state); } void DiscordVoiceClient::OnUDPData(std::vector data) { @@ -464,6 +465,10 @@ DiscordVoiceClient::type_signal_speaking DiscordVoiceClient::signal_speaking() { return m_signal_speaking; } +DiscordVoiceClient::type_signal_state_update DiscordVoiceClient::signal_state_update() { + return m_signal_state_update; +} + void from_json(const nlohmann::json &j, VoiceGatewayMessage &m) { JS_D("op", m.Opcode); m.Data = j.at("d"); diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 7e4dee3..916d070 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -201,7 +201,6 @@ public: [[nodiscard]] bool IsConnected() const noexcept; [[nodiscard]] bool IsConnecting() const noexcept; -private: enum class State { ConnectingToWebsocket, EstablishingConnection, @@ -209,6 +208,8 @@ private: DisconnectedByClient, DisconnectedByServer, }; + +private: static const char *GetStateName(State state); void OnGatewayMessage(const std::string &msg); @@ -272,13 +273,16 @@ private: using type_signal_connected = sigc::signal; using type_signal_disconnected = sigc::signal; using type_signal_speaking = sigc::signal; + using type_signal_state_update = sigc::signal; type_signal_connected m_signal_connected; type_signal_disconnected m_signal_disconnected; type_signal_speaking m_signal_speaking; + type_signal_state_update m_signal_state_update; public: type_signal_connected signal_connected(); type_signal_disconnected signal_disconnected(); type_signal_speaking signal_speaking(); + type_signal_state_update signal_state_update(); }; #endif diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp index 20da46b..c80dfda 100644 --- a/src/windows/mainwindow.cpp +++ b/src/windows/mainwindow.cpp @@ -6,6 +6,7 @@ MainWindow::MainWindow() , m_content_box(Gtk::ORIENTATION_HORIZONTAL) , m_chan_content_paned(Gtk::ORIENTATION_HORIZONTAL) , m_content_members_paned(Gtk::ORIENTATION_HORIZONTAL) + , m_left_pane(Gtk::ORIENTATION_VERTICAL) , m_accels(Gtk::AccelGroup::create()) { set_default_size(1200, 800); get_style_context()->add_class("app-window"); @@ -51,12 +52,18 @@ MainWindow::MainWindow() m_content_stack.set_visible_child("chat"); m_content_stack.show(); - m_chan_content_paned.pack1(m_channel_list); + m_voice_info.show(); + + m_left_pane.add(m_channel_list); + m_left_pane.add(m_voice_info); + m_left_pane.show(); + + m_chan_content_paned.pack1(m_left_pane); m_chan_content_paned.pack2(m_content_members_paned); m_chan_content_paned.child_property_shrink(m_content_members_paned) = true; m_chan_content_paned.child_property_resize(m_content_members_paned) = true; - m_chan_content_paned.child_property_shrink(m_channel_list) = true; - m_chan_content_paned.child_property_resize(m_channel_list) = true; + m_chan_content_paned.child_property_shrink(m_left_pane) = true; + m_chan_content_paned.child_property_resize(m_left_pane) = true; m_chan_content_paned.set_position(200); m_chan_content_paned.show(); m_content_box.add(m_chan_content_paned); diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp index 6e95b72..d4013dc 100644 --- a/src/windows/mainwindow.hpp +++ b/src/windows/mainwindow.hpp @@ -3,6 +3,7 @@ #include "components/chatwindow.hpp" #include "components/memberlist.hpp" #include "components/friendslist.hpp" +#include "components/voiceinfobox.hpp" #include class MainWindow : public Gtk::Window { @@ -53,6 +54,9 @@ private: ChatWindow m_chat; MemberList m_members; FriendsList m_friends; + VoiceInfoBox m_voice_info; + + Gtk::Box m_left_pane; Gtk::Stack m_content_stack; -- cgit v1.2.3 From 3e67e8a7aeb265914d538314b17f36a988017081 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 19 May 2023 20:20:33 -0400 Subject: add voice state icons to participant rows --- .github/workflows/ci.yml | 14 ++++----- ci/used-icons.txt | 7 +++-- res/css/main.css | 4 +++ src/components/channels.cpp | 14 +++++++++ src/components/channels.hpp | 2 ++ src/components/channelscellrenderer.cpp | 52 ++++++++++++++++++++++++++++++++- src/components/channelscellrenderer.hpp | 7 ++++- src/discord/discord.cpp | 47 ++++++++++++++++++++--------- src/discord/discord.hpp | 10 +++++-- src/discord/voicestateflags.hpp | 17 +++++++++++ 10 files changed, 146 insertions(+), 28 deletions(-) create mode 100644 src/discord/voicestateflags.hpp (limited to 'res') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63511cc..2a1bd25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,13 +93,13 @@ jobs: cd ${artifact_dir}/share/icons/Adwaita mkdir -p 16x16/actions 24x24/actions 32x32/actions 48x48/actions 64x64/actions 96x96/actions scalable/actions cd ${GITHUB_WORKSPACE} - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/16x16/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/24x24/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/32x32/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/32x32/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/48x48/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/48x48/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/64x64/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/96x96/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/actions/%.svg ${artifact_dir}/share/icons/Adwaita/scalable/actions || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/16x16/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/24x24/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/32x32/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/32x32/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/48x48/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/48x48/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/64x64/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/96x96/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/%.svg ${artifact_dir}/share/icons/Adwaita/scalable/%.svg || : - name: Upload build (1) uses: haya14busa/action-cond@v1 diff --git a/ci/used-icons.txt b/ci/used-icons.txt index 7e00c12..0ea18ae 100644 --- a/ci/used-icons.txt +++ b/ci/used-icons.txt @@ -1,2 +1,5 @@ -document-send-symbolic -call-stop-symbolic +actions/document-send-symbolic +actions/call-stop-symbolic +status/microphone-disabled-symbolic +status/audio-volume-muted-symbolic +devices/camera-web-symbolic diff --git a/res/css/main.css b/res/css/main.css index 4b76bb4..dcbdf4f 100644 --- a/res/css/main.css +++ b/res/css/main.css @@ -378,3 +378,7 @@ .voice-info-location { } + +.voice-state-server { + color: red; +} diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 5e3da86..0e97837 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -96,6 +96,7 @@ ChannelList::ChannelList() column->add_attribute(renderer->property_expanded(), m_columns.m_expanded); column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw); column->add_attribute(renderer->property_color(), m_columns.m_color); + column->add_attribute(renderer->property_voice_state(), m_columns.m_voice_flags); m_view.append_column(*column); m_menu_guild_copy_id.signal_activate().connect([this] { @@ -282,6 +283,7 @@ ChannelList::ChannelList() #if WITH_VOICE discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserConnect)); discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserDisconnect)); + discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceStateSet)); #endif } @@ -537,6 +539,12 @@ void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) m_model->erase(iter); } } + +void ChannelList::OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) { + if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { + (*iter)[m_columns.m_voice_flags] = flags; + } +} #endif void ChannelList::DeleteThreadRow(Snowflake id) { @@ -857,6 +865,11 @@ Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData & row[m_columns.m_id] = user.ID; row[m_columns.m_name] = user.GetEscapedName(); + const auto voice_state = Abaddon::Get().GetDiscordClient().GetVoiceState(user.ID); + if (voice_state.has_value()) { + row[m_columns.m_voice_flags] = voice_state->second; + } + auto &img = Abaddon::Get().GetImageManager(); row[m_columns.m_icon] = img.GetPlaceholder(VoiceParticipantIconSize); const auto cb = [this, user_id = user.ID](const Glib::RefPtr &pb) { @@ -1283,4 +1296,5 @@ ChannelList::ModelColumns::ModelColumns() { add(m_nsfw); add(m_expanded); add(m_color); + add(m_voice_flags); } diff --git a/src/components/channels.hpp b/src/components/channels.hpp index 2ad1a7c..7a23b3d 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -55,6 +55,7 @@ protected: void OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id); void OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id); + void OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags); Gtk::TreeView m_view; @@ -70,6 +71,7 @@ protected: Gtk::TreeModelColumn m_sort; Gtk::TreeModelColumn m_nsfw; Gtk::TreeModelColumn> m_color; // for folders right now + Gtk::TreeModelColumn m_voice_flags; // Gtk::CellRenderer's property_is_expanded only works how i want it to if it has children // because otherwise it doesnt count as an "expander" (property_is_expander) // so this solution will have to do which i hate but the alternative is adding invisible children diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp index ab3113c..6de7a00 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channelscellrenderer.cpp @@ -17,7 +17,8 @@ CellRendererChannels::CellRendererChannels() , m_property_pixbuf_animation(*this, "pixbuf-animation") , m_property_expanded(*this, "expanded") , m_property_nsfw(*this, "nsfw") - , m_property_color(*this, "color") { + , m_property_color(*this, "color") + , m_property_voice_state(*this, "voice-state") { property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; property_xpad() = 2; property_ypad() = 2; @@ -58,6 +59,10 @@ Glib::PropertyProxy> CellRendererChannels::property_col return m_property_color.get_proxy(); } +Glib::PropertyProxy CellRendererChannels::property_voice_state() { + return m_property_voice_state.get_proxy(); +} + void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { switch (m_property_type.get_value()) { case RenderType::Folder: @@ -733,6 +738,51 @@ void CellRendererChannels::render_vfunc_voice_participant(const Cairo::RefPtrrectangle(icon_x, icon_y, icon_w, icon_h); cr->fill(); } + + auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); + if (paned != nullptr) { + const auto edge = std::min(paned->get_position(), background_area.get_width()); + + const static std::array, 3> icon_order = { { + { VoiceStateFlags::SelfMute | VoiceStateFlags::Mute, "microphone-disabled-symbolic" }, + { VoiceStateFlags::SelfDeaf | VoiceStateFlags::Deaf, "audio-volume-muted-symbolic" }, + { VoiceStateFlags::SelfVideo, "camera-web-symbolic" }, + } }; + + constexpr static int IconSize = 18; + constexpr static int IconPad = 2; + + const VoiceStateFlags voice_flags = m_property_voice_state.get_value(); + + int offset = 0; + for (auto iter = icon_order.rbegin(); iter != icon_order.rend(); iter++) { + const auto &[flag, icon] = *iter; + if ((voice_flags & flag) == VoiceStateFlags::Clear) continue; + + const double icon_w = 18; + const double icon_h = 18; + const double icon_x = background_area.get_x() + edge - icon_w + offset; + const double icon_y = background_area.get_y() + background_area.get_height() / 2 - icon_h / 2; + Gdk::Rectangle icon_cell_area(icon_x, icon_y, icon_w, icon_h); + + offset -= (IconSize + IconPad); + + const bool is_server_mute = (voice_flags & VoiceStateFlags::Mute) == VoiceStateFlags::Mute; + const bool is_server_deaf = (voice_flags & VoiceStateFlags::Deaf) == VoiceStateFlags::Deaf; + auto context = widget.get_style_context(); + if (is_server_mute || is_server_deaf) { + context->context_save(); + context->add_class("voice-state-server"); + } + + m_renderer_pixbuf.property_icon_name() = icon; + m_renderer_pixbuf.render(cr, widget, background_area, icon_cell_area, flags); + + if (is_server_mute || is_server_deaf) { + context->context_restore(); + } + } + } } #endif diff --git a/src/components/channelscellrenderer.hpp b/src/components/channelscellrenderer.hpp index f6859dc..934ce5b 100644 --- a/src/components/channelscellrenderer.hpp +++ b/src/components/channelscellrenderer.hpp @@ -3,6 +3,8 @@ #include #include #include "discord/snowflake.hpp" +#include "discord/voicestateflags.hpp" +#include "misc/bitwise.hpp" enum class RenderType : uint8_t { Folder, @@ -34,6 +36,7 @@ public: Glib::PropertyProxy property_expanded(); Glib::PropertyProxy property_nsfw(); Glib::PropertyProxy> property_color(); + Glib::PropertyProxy property_voice_state(); protected: void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override; @@ -113,7 +116,7 @@ protected: const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags); - // voice channel + // voice participant void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; void get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; void get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; @@ -152,6 +155,7 @@ protected: private: Gtk::CellRendererText m_renderer_text; + Gtk::CellRendererPixbuf m_renderer_pixbuf; Glib::Property m_property_type; // all Glib::Property m_property_name; // all @@ -161,6 +165,7 @@ private: Glib::Property m_property_expanded; // category Glib::Property m_property_nsfw; // channel Glib::Property> m_property_color; // folder + Glib::Property m_property_voice_state; // same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39 // this will manifest though since guild icons can change diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index be939be..37d4f28 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1227,8 +1227,8 @@ std::optional DiscordClient::GetSSRCOfUser(Snowflake id) const { return m_voice.GetSSRCOfUser(id); } -std::optional DiscordClient::GetVoiceState(Snowflake user_id) const { - if (const auto it = m_voice_state_user_channel.find(user_id); it != m_voice_state_user_channel.end()) { +std::optional> DiscordClient::GetVoiceState(Snowflake user_id) const { + if (const auto it = m_voice_states.find(user_id); it != m_voice_states.end()) { return it->second; } return std::nullopt; @@ -2267,9 +2267,9 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { if (data.ChannelID.has_value()) { const auto old_state = GetVoiceState(data.UserID); - SetVoiceState(data.UserID, *data.ChannelID); - if (old_state.has_value() && *old_state != *data.ChannelID) { - m_signal_voice_user_disconnect.emit(data.UserID, *old_state); + SetVoiceState(data.UserID, data); + if (old_state.has_value() && old_state->first != *data.ChannelID) { + m_signal_voice_user_disconnect.emit(data.UserID, old_state->first); m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); } else if (!old_state.has_value()) { m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); @@ -2278,7 +2278,7 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { const auto old_state = GetVoiceState(data.UserID); ClearVoiceState(data.UserID); if (old_state.has_value()) { - m_signal_voice_user_disconnect.emit(data.UserID, *old_state); + m_signal_voice_user_disconnect.emit(data.UserID, old_state->first); } } } @@ -2333,7 +2333,7 @@ void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { for (const auto &g : data.Guilds) { for (const auto &s : g.VoiceStates) { if (s.ChannelID.has_value()) { - SetVoiceState(s.UserID, *s.ChannelID); + SetVoiceState(s.UserID, s); } } } @@ -2798,18 +2798,33 @@ void DiscordClient::SendVoiceStateUpdate() { m_websocket.Send(msg); } -void DiscordClient::SetVoiceState(Snowflake user_id, Snowflake channel_id) { - spdlog::get("discord")->debug("SetVoiceState: {} -> {}", user_id, channel_id); - m_voice_state_user_channel[user_id] = channel_id; - m_voice_state_channel_users[channel_id].insert(user_id); +void DiscordClient::SetVoiceState(Snowflake user_id, const VoiceState &state) { + if (!state.ChannelID.has_value()) { + spdlog::get("discord")->error("SetVoiceState called with missing channel ID"); + return; + } + spdlog::get("discord")->debug("SetVoiceState: {} -> {}", user_id, *state.ChannelID); + + auto flags = VoiceStateFlags::Clear; + if (state.IsSelfMuted) flags |= VoiceStateFlags::SelfMute; + if (state.IsSelfDeafened) flags |= VoiceStateFlags::SelfDeaf; + if (state.IsMuted) flags |= VoiceStateFlags::Mute; + if (state.IsDeafened) flags |= VoiceStateFlags::Deaf; + if (state.IsSelfStream) flags |= VoiceStateFlags::SelfStream; + if (state.IsSelfVideo) flags |= VoiceStateFlags::SelfVideo; + + m_voice_states[user_id] = std::make_pair(*state.ChannelID, flags); + m_voice_state_channel_users[*state.ChannelID].insert(user_id); + + m_signal_voice_state_set.emit(user_id, *state.ChannelID, flags); } void DiscordClient::ClearVoiceState(Snowflake user_id) { spdlog::get("discord")->debug("ClearVoiceState: {}", user_id); - if (const auto it = m_voice_state_user_channel.find(user_id); it != m_voice_state_user_channel.end()) { - m_voice_state_channel_users[it->second].erase(user_id); + if (const auto it = m_voice_states.find(user_id); it != m_voice_states.end()) { + m_voice_state_channel_users[it->second.first].erase(user_id); // invalidated - m_voice_state_user_channel.erase(user_id); + m_voice_states.erase(user_id); } } @@ -3119,4 +3134,8 @@ DiscordClient::type_signal_voice_client_state_update DiscordClient::signal_voice DiscordClient::type_signal_voice_channel_changed DiscordClient::signal_voice_channel_changed() { return m_signal_voice_channel_changed; } + +DiscordClient::type_signal_voice_state_set DiscordClient::signal_voice_state_set() { + return m_signal_voice_state_set; +} #endif diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index cb76ef6..7f7518c 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -5,6 +5,7 @@ #include "objects.hpp" #include "store.hpp" #include "voiceclient.hpp" +#include "voicestateflags.hpp" #include "websocket.hpp" #include #include @@ -192,7 +193,7 @@ public: [[nodiscard]] Snowflake GetVoiceChannelID() const noexcept; [[nodiscard]] std::unordered_set GetUsersInVoiceChannel(Snowflake channel_id); [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; - [[nodiscard]] std::optional GetVoiceState(Snowflake user_id) const; + [[nodiscard]] std::optional> GetVoiceState(Snowflake user_id) const; DiscordVoiceClient &GetVoiceClient(); @@ -360,12 +361,12 @@ private: Snowflake m_voice_channel_id; // todo sql i guess - std::unordered_map m_voice_state_user_channel; + std::unordered_map> m_voice_states; std::unordered_map> m_voice_state_channel_users; void SendVoiceStateUpdate(); - void SetVoiceState(Snowflake user_id, Snowflake channel_id); + void SetVoiceState(Snowflake user_id, const VoiceState &state); void ClearVoiceState(Snowflake user_id); void OnVoiceConnected(); @@ -455,6 +456,7 @@ public: using type_signal_voice_requested_disconnect = sigc::signal; using type_signal_voice_client_state_update = sigc::signal; using type_signal_voice_channel_changed = sigc::signal; + using type_signal_voice_state_set = sigc::signal; #endif type_signal_gateway_ready signal_gateway_ready(); @@ -522,6 +524,7 @@ public: type_signal_voice_requested_disconnect signal_voice_requested_disconnect(); type_signal_voice_client_state_update signal_voice_client_state_update(); type_signal_voice_channel_changed signal_voice_channel_changed(); + type_signal_voice_state_set signal_voice_state_set(); #endif protected: @@ -590,5 +593,6 @@ protected: type_signal_voice_requested_disconnect m_signal_voice_requested_disconnect; type_signal_voice_client_state_update m_signal_voice_client_state_update; type_signal_voice_channel_changed m_signal_voice_channel_changed; + type_signal_voice_state_set m_signal_voice_state_set; #endif }; diff --git a/src/discord/voicestateflags.hpp b/src/discord/voicestateflags.hpp new file mode 100644 index 0000000..e369001 --- /dev/null +++ b/src/discord/voicestateflags.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +enum class VoiceStateFlags : uint8_t { + Clear = 0, + Deaf = 1 << 0, + Mute = 1 << 1, + SelfDeaf = 1 << 2, + SelfMute = 1 << 3, + SelfStream = 1 << 4, + SelfVideo = 1 << 5, +}; + +template<> +struct Bitwise { + static const bool enable = true; +}; -- cgit v1.2.3