summaryrefslogtreecommitdiff
path: root/src/windows
diff options
context:
space:
mode:
authorouwou <26526779+ouwou@users.noreply.github.com>2024-07-05 03:51:58 -0400
committerGitHub <noreply@github.com>2024-07-05 03:51:58 -0400
commit68db143c8939e12dd569d6ac737213ad856c139b (patch)
treed5e814329fb19912b69b9804fc68b852674d8277 /src/windows
parente6191d95341d164eacaf91739c8aa7020dd5c9b6 (diff)
parentb19782c16dffd38ac5651641131a48f8ff961b32 (diff)
downloadabaddon-portaudio-68db143c8939e12dd569d6ac737213ad856c139b.tar.gz
abaddon-portaudio-68db143c8939e12dd569d6ac737213ad856c139b.zip
Merge pull request #279 from uowuo/stages
Support for stages
Diffstat (limited to 'src/windows')
-rw-r--r--src/windows/voice/voicewindow.cpp (renamed from src/windows/voicewindow.cpp)260
-rw-r--r--src/windows/voice/voicewindow.hpp (renamed from src/windows/voicewindow.hpp)36
-rw-r--r--src/windows/voice/voicewindowaudiencelistentry.cpp23
-rw-r--r--src/windows/voice/voicewindowaudiencelistentry.hpp18
-rw-r--r--src/windows/voice/voicewindowspeakerlistentry.cpp58
-rw-r--r--src/windows/voice/voicewindowspeakerlistentry.hpp38
6 files changed, 335 insertions, 98 deletions
diff --git a/src/windows/voicewindow.cpp b/src/windows/voice/voicewindow.cpp
index a005e79..e59705a 100644
--- a/src/windows/voicewindow.cpp
+++ b/src/windows/voice/voicewindow.cpp
@@ -1,89 +1,19 @@
+#include "util.hpp"
#ifdef WITH_VOICE
// clang-format off
+#include "voicewindow.hpp"
+
#include "abaddon.hpp"
#include "audio/manager.hpp"
#include "components/lazyimage.hpp"
-#include "voicesettingswindow.hpp"
-#include "voicewindow.hpp"
+#include "voicewindowaudiencelistentry.hpp"
+#include "voicewindowspeakerlistentry.hpp"
+#include "windows/voicesettingswindow.hpp"
// clang-format on
-class VoiceWindowUserListEntry : public Gtk::ListBoxRow {
-public:
- VoiceWindowUserListEntry(Snowflake id)
- : 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_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() * 0.01);
- });
-
- 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);
- m_main.add(m_meter);
- add(m_main);
- show_all_children();
-
- auto &discord = Abaddon::Get().GetDiscordClient();
- const auto user = discord.GetUser(id);
- if (user.has_value()) {
- m_name.set_text(user->GetUsername());
- m_avatar.SetURL(user->GetAvatarURL("png", "32"));
- } else {
- m_name.set_text("Unknown user");
- }
-
- m_mute.signal_toggled().connect([this]() {
- m_signal_mute_cs.emit(m_mute.get_active());
- });
- }
-
- void SetVolumeMeter(double frac) {
- m_meter.SetVolume(frac);
- }
-
- void RestoreGain(double frac) {
- m_volume.set_value(frac * 100.0);
- }
-
-private:
- Gtk::Box m_main;
- Gtk::Box m_horz;
- LazyImage m_avatar;
- Gtk::Label m_name;
- Gtk::CheckButton m_mute;
- Gtk::Scale m_volume;
- VolumeMeter m_meter;
-
-public:
- using type_signal_mute_cs = sigc::signal<void(bool)>;
- using type_signal_volume = sigc::signal<void(double)>;
- 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)
: m_main(Gtk::ORIENTATION_VERTICAL)
, m_controls(Gtk::ORIENTATION_HORIZONTAL)
@@ -91,7 +21,11 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
, m_deafen("Deafen")
, m_noise_suppression("Suppress Noise")
, m_mix_mono("Mix Mono")
+ , m_stage_command("Request to Speak")
, m_disconnect("Disconnect")
+ , m_stage_invite_lbl("You've been invited to speak")
+ , m_stage_accept("Accept")
+ , m_stage_decline("Decline")
, m_channel_id(channel_id)
, m_menu_view("View")
, m_menu_view_settings("More _Settings", true) {
@@ -102,14 +36,19 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
auto &discord = Abaddon::Get().GetDiscordClient();
auto &audio = Abaddon::Get().GetAudio();
+ const auto channel = discord.GetChannel(m_channel_id);
+ m_is_stage = channel.has_value() && channel->Type == ChannelType::GUILD_STAGE_VOICE;
+
SetUsers(discord.GetUsersInVoiceChannel(m_channel_id));
discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserDisconnect));
discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserConnect));
+ discord.signal_voice_speaker_state_changed().connect(sigc::mem_fun(*this, &VoiceWindow::OnSpeakerStateChanged));
+ discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &VoiceWindow::OnVoiceStateUpdate));
if (const auto self_state = discord.GetVoiceState(discord.GetUserData().ID); self_state.has_value()) {
- m_mute.set_active((self_state->second & VoiceStateFlags::SelfMute) == VoiceStateFlags::SelfMute);
- m_deafen.set_active((self_state->second & VoiceStateFlags::SelfDeaf) == VoiceStateFlags::SelfDeaf);
+ m_mute.set_active(util::FlagSet(self_state->second.Flags, VoiceStateFlags::SelfMute));
+ m_deafen.set_active(util::FlagSet(self_state->second.Flags, VoiceStateFlags::SelfDeaf));
}
m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged));
@@ -253,35 +192,97 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
combos_combos->pack_start(m_playback_combo);
combos_combos->pack_start(m_capture_combo);
- m_scroll.add(m_user_list);
+ if (const auto instance = discord.GetStageInstanceFromChannel(channel_id); instance.has_value()) {
+ m_stage_topic_label.show();
+ UpdateStageTopicLabel(instance->Topic);
+ } else {
+ m_stage_topic_label.hide();
+ }
+
+ discord.signal_stage_instance_create().connect(sigc::mem_fun(*this, &VoiceWindow::OnStageInstanceCreate));
+ discord.signal_stage_instance_update().connect(sigc::mem_fun(*this, &VoiceWindow::OnStageInstanceUpdate));
+ discord.signal_stage_instance_delete().connect(sigc::mem_fun(*this, &VoiceWindow::OnStageInstanceDelete));
+
+ m_stage_command.signal_clicked().connect([this]() {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto user_id = discord.GetUserData().ID;
+ const bool is_moderator = discord.IsStageModerator(user_id, m_channel_id);
+ const bool is_speaker = discord.IsUserSpeaker(user_id);
+ const bool is_invited_to_speak = discord.IsUserInvitedToSpeak(user_id);
+
+ if (is_speaker) {
+ discord.SetStageSpeaking(m_channel_id, false, NOOP_CALLBACK);
+ } else if (is_moderator) {
+ discord.SetStageSpeaking(m_channel_id, true, NOOP_CALLBACK);
+ } else if (is_invited_to_speak) {
+ discord.DeclineInviteToSpeak(m_channel_id, NOOP_CALLBACK);
+ } else {
+ const bool requested = discord.HasUserRequestedToSpeak(user_id);
+ discord.RequestToSpeak(m_channel_id, !requested, NOOP_CALLBACK);
+ }
+ });
+
+ m_stage_accept.signal_clicked().connect([this]() {
+ Abaddon::Get().GetDiscordClient().SetStageSpeaking(m_channel_id, true, NOOP_CALLBACK);
+ });
+
+ m_stage_decline.signal_clicked().connect([this]() {
+ Abaddon::Get().GetDiscordClient().DeclineInviteToSpeak(m_channel_id, NOOP_CALLBACK);
+ });
+
+ m_speakers_label.set_markup("<b>Speakers</b>");
+ if (m_is_stage) m_listing.pack_start(m_speakers_label, false, true);
+ m_listing.pack_start(m_speakers_list, false, true);
+ m_audience_label.set_markup("<b>Audience</b>");
+ if (m_is_stage) m_listing.pack_start(m_audience_label, false, true);
+ if (m_is_stage) m_listing.pack_start(m_audience_list, false, true);
+ m_scroll.add(m_listing);
m_controls.add(m_mute);
m_controls.add(m_deafen);
m_controls.add(m_noise_suppression);
m_controls.add(m_mix_mono);
- m_controls.pack_end(m_disconnect, false, true);
+ m_buttons.set_halign(Gtk::ALIGN_CENTER);
+ if (m_is_stage) m_buttons.pack_start(m_stage_command, false, true);
+ m_buttons.pack_start(m_disconnect, false, true);
+ m_stage_invite_box.pack_start(m_stage_invite_lbl, false, true);
+ m_stage_invite_box.pack_start(m_stage_invite_btns);
+ m_stage_invite_btns.set_halign(Gtk::ALIGN_CENTER);
+ m_stage_invite_btns.pack_start(m_stage_accept, false, true);
+ m_stage_invite_btns.pack_start(m_stage_decline, false, true);
m_main.pack_start(m_menu_bar, false, true);
m_main.pack_start(m_controls, false, true);
+ m_main.pack_start(m_buttons, false, true);
+ m_main.pack_start(m_stage_invite_box, false, true);
m_main.pack_start(m_vad_value, false, true);
m_main.pack_start(*Gtk::make_managed<Gtk::Label>("Input Settings"), false, true);
m_main.pack_start(*sliders_container, false, true);
m_main.pack_start(m_scroll);
+ m_stage_topic_label.set_ellipsize(Pango::ELLIPSIZE_END);
+ m_stage_topic_label.set_halign(Gtk::ALIGN_CENTER);
+ m_main.pack_start(m_stage_topic_label, false, true);
m_main.pack_start(*combos_container, false, true, 2);
add(m_main);
show_all_children();
Glib::signal_timeout().connect(sigc::mem_fun(*this, &VoiceWindow::UpdateVoiceMeters), 40);
+
+ UpdateStageCommand();
}
void VoiceWindow::SetUsers(const std::unordered_set<Snowflake> &user_ids) {
- const auto me = Abaddon::Get().GetDiscordClient().GetUserData().ID;
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto me = discord.GetUserData().ID;
for (auto id : user_ids) {
- if (id == me) continue;
- m_user_list.add(*CreateRow(id));
+ if (!m_is_stage || discord.IsUserSpeaker(id)) {
+ if (id != me) m_speakers_list.add(*CreateSpeakerRow(id));
+ } else {
+ m_audience_list.add(*CreateAudienceRow(id));
+ }
}
}
-Gtk::ListBoxRow *VoiceWindow::CreateRow(Snowflake id) {
- auto *row = Gtk::make_managed<VoiceWindowUserListEntry>(id);
+Gtk::ListBoxRow *VoiceWindow::CreateSpeakerRow(Snowflake id) {
+ auto *row = Gtk::make_managed<VoiceWindowSpeakerListEntry>(id);
m_rows[id] = row;
auto &vc = Abaddon::Get().GetDiscordClient().GetVoiceClient();
row->RestoreGain(vc.GetUserVolume(id));
@@ -291,7 +292,14 @@ Gtk::ListBoxRow *VoiceWindow::CreateRow(Snowflake id) {
row->signal_volume().connect([this, id](double volume) {
m_signal_user_volume_changed.emit(id, volume);
});
- row->show_all();
+ row->show();
+ return row;
+}
+
+Gtk::ListBoxRow *VoiceWindow::CreateAudienceRow(Snowflake id) {
+ auto *row = Gtk::make_managed<VoiceWindowAudienceListEntry>(id);
+ m_rows[id] = row;
+ row->show();
return row;
}
@@ -303,6 +311,13 @@ void VoiceWindow::OnDeafenChanged() {
m_signal_deafen.emit(m_deafen.get_active());
}
+void VoiceWindow::TryDeleteRow(Snowflake id) {
+ if (auto it = m_rows.find(id); it != m_rows.end()) {
+ delete it->second;
+ m_rows.erase(it);
+ }
+}
+
bool VoiceWindow::UpdateVoiceMeters() {
auto &audio = Abaddon::Get().GetAudio();
switch (audio.GetVADMethod()) {
@@ -319,7 +334,9 @@ bool VoiceWindow::UpdateVoiceMeters() {
for (auto [id, row] : m_rows) {
const auto ssrc = Abaddon::Get().GetDiscordClient().GetSSRCOfUser(id);
if (ssrc.has_value()) {
- row->SetVolumeMeter(audio.GetSSRCVolumeLevel(*ssrc));
+ if (auto *speaker_row = dynamic_cast<VoiceWindowSpeakerListEntry *>(row)) {
+ speaker_row->SetVolumeMeter(audio.GetSSRCVolumeLevel(*ssrc));
+ }
}
}
return true;
@@ -339,23 +356,80 @@ void VoiceWindow::UpdateVADParamValue() {
}
}
+void VoiceWindow::UpdateStageCommand() {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto user_id = discord.GetUserData().ID;
+
+ m_has_requested_to_speak = discord.HasUserRequestedToSpeak(user_id);
+ const bool is_moderator = discord.IsStageModerator(user_id, m_channel_id);
+ const bool is_speaker = discord.IsUserSpeaker(user_id);
+ const bool is_invited_to_speak = discord.IsUserInvitedToSpeak(user_id);
+
+ m_stage_invite_box.set_visible(is_invited_to_speak);
+
+ if (is_speaker) {
+ m_stage_command.set_label("Leave the Stage");
+ } else if (is_moderator) {
+ m_stage_command.set_label("Speak on Stage");
+ } else if (m_has_requested_to_speak) {
+ m_stage_command.set_label("Cancel Request");
+ } else if (is_invited_to_speak) {
+ m_stage_command.set_label("Decline Invite");
+ } else {
+ m_stage_command.set_label("Request to Speak");
+ }
+}
+
+void VoiceWindow::UpdateStageTopicLabel(const std::string &topic) {
+ m_stage_topic_label.set_markup("Topic: " + topic);
+}
+
void VoiceWindow::OnUserConnect(Snowflake user_id, Snowflake to_channel_id) {
if (m_channel_id == to_channel_id) {
if (auto it = m_rows.find(user_id); it == m_rows.end()) {
- m_user_list.add(*CreateRow(user_id));
+ if (Abaddon::Get().GetDiscordClient().IsUserSpeaker(user_id)) {
+ m_speakers_list.add(*CreateSpeakerRow(user_id));
+ } else {
+ m_audience_list.add(*CreateAudienceRow(user_id));
+ }
}
}
}
void VoiceWindow::OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id) {
- if (m_channel_id == from_channel_id) {
- if (auto it = m_rows.find(user_id); it != m_rows.end()) {
- delete it->second;
- m_rows.erase(it);
- }
+ if (m_channel_id == from_channel_id) TryDeleteRow(user_id);
+}
+
+void VoiceWindow::OnSpeakerStateChanged(Snowflake channel_id, Snowflake user_id, bool is_speaker) {
+ if (m_channel_id != channel_id) return;
+ TryDeleteRow(user_id);
+ if (is_speaker) {
+ m_speakers_list.add(*CreateSpeakerRow(user_id));
+ } else {
+ m_audience_list.add(*CreateAudienceRow(user_id));
}
}
+void VoiceWindow::OnVoiceStateUpdate(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (user_id != discord.GetUserData().ID) return;
+
+ UpdateStageCommand();
+}
+
+void VoiceWindow::OnStageInstanceCreate(const StageInstance &instance) {
+ m_stage_topic_label.show();
+ UpdateStageTopicLabel(instance.Topic);
+}
+
+void VoiceWindow::OnStageInstanceUpdate(const StageInstance &instance) {
+ UpdateStageTopicLabel(instance.Topic);
+}
+
+void VoiceWindow::OnStageInstanceDelete(const StageInstance &instance) {
+ m_stage_topic_label.hide();
+}
+
VoiceWindow::type_signal_mute VoiceWindow::signal_mute() {
return m_signal_mute;
}
diff --git a/src/windows/voicewindow.hpp b/src/windows/voice/voicewindow.hpp
index fb64010..05033d9 100644
--- a/src/windows/voicewindow.hpp
+++ b/src/windows/voice/voicewindow.hpp
@@ -1,4 +1,6 @@
#pragma once
+#include "discord/stage.hpp"
+#include "discord/voicestate.hpp"
#ifdef WITH_VOICE
// clang-format off
@@ -16,7 +18,6 @@
#include <unordered_set>
// clang-format on
-class VoiceWindowUserListEntry;
class VoiceWindow : public Gtk::Window {
public:
VoiceWindow(Snowflake channel_id);
@@ -24,17 +25,25 @@ public:
private:
void SetUsers(const std::unordered_set<Snowflake> &user_ids);
- Gtk::ListBoxRow *CreateRow(Snowflake id);
+ Gtk::ListBoxRow *CreateSpeakerRow(Snowflake id);
+ Gtk::ListBoxRow *CreateAudienceRow(Snowflake id);
void OnUserConnect(Snowflake user_id, Snowflake to_channel_id);
void OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id);
+ void OnSpeakerStateChanged(Snowflake channel_id, Snowflake user_id, bool is_speaker);
+ void OnVoiceStateUpdate(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags);
+ void OnStageInstanceCreate(const StageInstance &instance);
+ void OnStageInstanceUpdate(const StageInstance &instance);
+ void OnStageInstanceDelete(const StageInstance &instance);
void OnMuteChanged();
void OnDeafenChanged();
+ void TryDeleteRow(Snowflake id);
bool UpdateVoiceMeters();
-
void UpdateVADParamValue();
+ void UpdateStageCommand();
+ void UpdateStageTopicLabel(const std::string &topic);
Gtk::Box m_main;
Gtk::Box m_controls;
@@ -43,7 +52,9 @@ private:
Gtk::CheckButton m_deafen;
Gtk::ScrolledWindow m_scroll;
- Gtk::ListBox m_user_list;
+ Gtk::VBox m_listing;
+ Gtk::ListBox m_speakers_list;
+ Gtk::ListBox m_audience_list;
// Shows volume for gate VAD method
// Shows probability for RNNoise VAD method
@@ -56,21 +67,36 @@ private:
Gtk::CheckButton m_noise_suppression;
Gtk::CheckButton m_mix_mono;
+ Gtk::HBox m_buttons;
Gtk::Button m_disconnect;
+ Gtk::Button m_stage_command;
+
+ Gtk::VBox m_stage_invite_box;
+ Gtk::Label m_stage_invite_lbl;
+ Gtk::HBox m_stage_invite_btns;
+ Gtk::Button m_stage_accept;
+ Gtk::Button m_stage_decline;
+
+ bool m_has_requested_to_speak = false;
Gtk::ComboBoxText m_vad_combo;
Gtk::ComboBox m_playback_combo;
Gtk::ComboBox m_capture_combo;
Snowflake m_channel_id;
+ bool m_is_stage;
- std::unordered_map<Snowflake, VoiceWindowUserListEntry *> m_rows;
+ std::unordered_map<Snowflake, Gtk::ListBoxRow *> m_rows;
Gtk::MenuBar m_menu_bar;
Gtk::MenuItem m_menu_view;
Gtk::Menu m_menu_view_sub;
Gtk::MenuItem m_menu_view_settings;
+ Gtk::Label m_stage_topic_label;
+ Gtk::Label m_speakers_label;
+ Gtk::Label m_audience_label;
+
public:
using type_signal_mute = sigc::signal<void(bool)>;
using type_signal_deafen = sigc::signal<void(bool)>;
diff --git a/src/windows/voice/voicewindowaudiencelistentry.cpp b/src/windows/voice/voicewindowaudiencelistentry.cpp
new file mode 100644
index 0000000..cf93343
--- /dev/null
+++ b/src/windows/voice/voicewindowaudiencelistentry.cpp
@@ -0,0 +1,23 @@
+#include "voicewindowaudiencelistentry.hpp"
+#include "abaddon.hpp"
+
+VoiceWindowAudienceListEntry::VoiceWindowAudienceListEntry(Snowflake id)
+ : m_main(Gtk::ORIENTATION_HORIZONTAL)
+ , m_avatar(32, 32) {
+ m_name.set_halign(Gtk::ALIGN_START);
+ m_name.set_hexpand(true);
+
+ m_main.add(m_avatar);
+ m_main.add(m_name);
+ add(m_main);
+ show_all_children();
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto user = discord.GetUser(id);
+ if (user.has_value()) {
+ m_name.set_text(user->GetUsername());
+ m_avatar.SetURL(user->GetAvatarURL("png", "32"));
+ } else {
+ m_name.set_text("Unknown user");
+ }
+}
diff --git a/src/windows/voice/voicewindowaudiencelistentry.hpp b/src/windows/voice/voicewindowaudiencelistentry.hpp
new file mode 100644
index 0000000..e7bdbb1
--- /dev/null
+++ b/src/windows/voice/voicewindowaudiencelistentry.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "components/lazyimage.hpp"
+#include "discord/snowflake.hpp"
+
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/listboxrow.h>
+
+class VoiceWindowAudienceListEntry : public Gtk::ListBoxRow {
+public:
+ VoiceWindowAudienceListEntry(Snowflake id);
+
+private:
+ Gtk::Box m_main;
+ LazyImage m_avatar;
+ Gtk::Label m_name;
+};
diff --git a/src/windows/voice/voicewindowspeakerlistentry.cpp b/src/windows/voice/voicewindowspeakerlistentry.cpp
new file mode 100644
index 0000000..a7bf2b8
--- /dev/null
+++ b/src/windows/voice/voicewindowspeakerlistentry.cpp
@@ -0,0 +1,58 @@
+#include "voicewindowspeakerlistentry.hpp"
+
+#include "abaddon.hpp"
+
+VoiceWindowSpeakerListEntry::VoiceWindowSpeakerListEntry(Snowflake id)
+ : 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_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() * 0.01);
+ });
+
+ 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);
+ m_main.add(m_meter);
+ add(m_main);
+ show_all_children();
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto user = discord.GetUser(id);
+ if (user.has_value()) {
+ m_name.set_text(user->GetUsername());
+ m_avatar.SetURL(user->GetAvatarURL("png", "32"));
+ } else {
+ m_name.set_text("Unknown user");
+ }
+
+ m_mute.signal_toggled().connect([this]() {
+ m_signal_mute_cs.emit(m_mute.get_active());
+ });
+}
+
+void VoiceWindowSpeakerListEntry::SetVolumeMeter(double frac) {
+ m_meter.SetVolume(frac);
+}
+
+void VoiceWindowSpeakerListEntry::RestoreGain(double frac) {
+ m_volume.set_value(frac * 100.0);
+}
+
+VoiceWindowSpeakerListEntry::type_signal_mute_cs VoiceWindowSpeakerListEntry::signal_mute_cs() {
+ return m_signal_mute_cs;
+}
+
+VoiceWindowSpeakerListEntry::type_signal_volume VoiceWindowSpeakerListEntry::signal_volume() {
+ return m_signal_volume;
+}
diff --git a/src/windows/voice/voicewindowspeakerlistentry.hpp b/src/windows/voice/voicewindowspeakerlistentry.hpp
new file mode 100644
index 0000000..a3b6429
--- /dev/null
+++ b/src/windows/voice/voicewindowspeakerlistentry.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "components/lazyimage.hpp"
+#include "components/volumemeter.hpp"
+#include "discord/snowflake.hpp"
+
+#include <gtkmm/box.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/label.h>
+#include <gtkmm/listboxrow.h>
+#include <gtkmm/scale.h>
+
+class VoiceWindowSpeakerListEntry : public Gtk::ListBoxRow {
+public:
+ VoiceWindowSpeakerListEntry(Snowflake id);
+
+ void SetVolumeMeter(double frac);
+ void RestoreGain(double frac);
+
+private:
+ Gtk::Box m_main;
+ Gtk::Box m_horz;
+ LazyImage m_avatar;
+ Gtk::Label m_name;
+ Gtk::CheckButton m_mute;
+ Gtk::Scale m_volume;
+ VolumeMeter m_meter;
+
+public:
+ using type_signal_mute_cs = sigc::signal<void(bool)>;
+ using type_signal_volume = sigc::signal<void(double)>;
+ type_signal_mute_cs signal_mute_cs();
+ type_signal_volume signal_volume();
+
+private:
+ type_signal_mute_cs m_signal_mute_cs;
+ type_signal_volume m_signal_volume;
+};