mirror of
https://github.com/aljazceru/notedeck.git
synced 2025-12-17 08:44:20 +01:00
Fix all channel management issues and improve UX
This commit addresses all identified issues in channel management: 1. **Fix timeline cleanup on channel removal** (channels.rs:97-123) - remove_channel() now properly unsubscribes from timeline - Prevents memory leaks from orphaned timeline subscriptions - Ensures proper cleanup when deleting channels 2. **Add bounds checking on deserialization** (storage/channels.rs:138-158) - Validates selected index after loading channels from disk - Prevents out-of-bounds panics if saved state is corrupted - Handles edge case of empty channel lists gracefully 3. **Implement channel deletion UI** (channel_sidebar.rs:19-25, 273-287) - Added DeleteChannel action to sidebar - Right-click context menu shows "Delete Channel" option - Only allows deletion if more than one channel exists - Properly cleans up timeline and saves state after deletion 4. **Implement channel editing UI** (channel_dialog.rs:5-44, channel_sidebar.rs:19-25) - Added EditChannel action and editing_index field to dialog - open_for_edit() method pre-fills dialog with existing data - Dialog title changes to "Edit Channel" in edit mode - Button changes from "Create" to "Save" when editing - Context menu shows "Edit Channel" option 5. **Add channel editing logic** (channels.rs:99-132, app.rs:476-524) - edit_channel() method updates channel data - Unsubscribes from old timeline if hashtags changed - Re-subscribes to new timeline with updated hashtags - Properly updates router with new timeline kind 6. **Auto-select newly created channels** (channels.rs:93-97) - add_channel() now automatically selects the new channel - Improves UX by immediately showing the channel user just created - Eliminates confusion about which channel is active 7. **Optimize storage saves** (app.rs:1290-1296) - Removed automatic save on every channel selection - Channel selection state will be saved on app close instead - Prevents excessive disk I/O from rapid channel switching - Saves only on create, edit, and delete operations All changes tested and build succeeds without errors.
This commit is contained in:
@@ -473,6 +473,55 @@ fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ui::ChannelDialogAction::Edit { index, name, hashtags } => {
|
||||||
|
// Edit existing channel
|
||||||
|
let edited = damus
|
||||||
|
.channels_cache
|
||||||
|
.active_channels_mut(app_ctx.i18n, app_ctx.accounts)
|
||||||
|
.edit_channel(
|
||||||
|
index,
|
||||||
|
name,
|
||||||
|
hashtags.clone(),
|
||||||
|
&mut damus.timeline_cache,
|
||||||
|
app_ctx.ndb,
|
||||||
|
app_ctx.pool,
|
||||||
|
);
|
||||||
|
|
||||||
|
if edited {
|
||||||
|
// Save channels cache
|
||||||
|
storage::save_channels_cache(app_ctx.path, &damus.channels_cache);
|
||||||
|
|
||||||
|
// Subscribe to the new timeline if hashtags changed
|
||||||
|
let txn = match nostrdb::Transaction::new(app_ctx.ndb) {
|
||||||
|
Ok(txn) => txn,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to create transaction for channel subscription: {}", e);
|
||||||
|
return app_resp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(channel) = damus.channels_cache.active_channels_mut(app_ctx.i18n, app_ctx.accounts).get_channel_mut(index) {
|
||||||
|
if damus.timeline_cache.get(&channel.timeline_kind).is_none() {
|
||||||
|
if let Some(result) = damus.timeline_cache.open(
|
||||||
|
&mut damus.subscriptions,
|
||||||
|
app_ctx.ndb,
|
||||||
|
app_ctx.note_cache,
|
||||||
|
&txn,
|
||||||
|
app_ctx.pool,
|
||||||
|
&channel.timeline_kind,
|
||||||
|
) {
|
||||||
|
result.process(
|
||||||
|
app_ctx.ndb,
|
||||||
|
app_ctx.note_cache,
|
||||||
|
&txn,
|
||||||
|
&mut damus.timeline_cache,
|
||||||
|
app_ctx.unknown_ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ui::ChannelDialogAction::Cancel => {
|
ui::ChannelDialogAction::Cancel => {
|
||||||
// Dialog was canceled, nothing to do
|
// Dialog was canceled, nothing to do
|
||||||
}
|
}
|
||||||
@@ -1242,12 +1291,35 @@ fn timelines_view(
|
|||||||
app.channels_cache
|
app.channels_cache
|
||||||
.active_channels_mut(ctx.i18n, ctx.accounts)
|
.active_channels_mut(ctx.i18n, ctx.accounts)
|
||||||
.select_channel(idx);
|
.select_channel(idx);
|
||||||
// Save channel state after selection changes
|
// Note: Don't save on every selection to avoid excessive disk I/O
|
||||||
storage::save_channels_cache(ctx.path, &app.channels_cache);
|
// Channel selection state will be saved on app close
|
||||||
}
|
}
|
||||||
ChannelSidebarAction::AddChannel => {
|
ChannelSidebarAction::AddChannel => {
|
||||||
app.channel_dialog.open();
|
app.channel_dialog.open();
|
||||||
}
|
}
|
||||||
|
ChannelSidebarAction::DeleteChannel(idx) => {
|
||||||
|
let removed = app.channels_cache
|
||||||
|
.active_channels_mut(ctx.i18n, ctx.accounts)
|
||||||
|
.remove_channel(idx, &mut app.timeline_cache, ctx.ndb, ctx.pool);
|
||||||
|
|
||||||
|
if removed.is_some() {
|
||||||
|
// Save channels cache after deletion
|
||||||
|
storage::save_channels_cache(ctx.path, &app.channels_cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChannelSidebarAction::EditChannel(idx) => {
|
||||||
|
// Get channel data and open dialog for editing
|
||||||
|
if let Some(channel) = app.channels_cache
|
||||||
|
.active_channels(ctx.accounts)
|
||||||
|
.get_channel(idx)
|
||||||
|
{
|
||||||
|
app.channel_dialog.open_for_edit(
|
||||||
|
idx,
|
||||||
|
channel.name.clone(),
|
||||||
|
channel.hashtags.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,12 +92,62 @@ impl ChannelList {
|
|||||||
|
|
||||||
pub fn add_channel(&mut self, channel: Channel) {
|
pub fn add_channel(&mut self, channel: Channel) {
|
||||||
self.channels.push(channel);
|
self.channels.push(channel);
|
||||||
|
// Auto-select the newly added channel
|
||||||
|
self.selected = self.channels.len() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_channel(&mut self, index: usize) -> Option<Channel> {
|
pub fn edit_channel(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
name: String,
|
||||||
|
hashtags: Vec<String>,
|
||||||
|
timeline_cache: &mut TimelineCache,
|
||||||
|
ndb: &mut nostrdb::Ndb,
|
||||||
|
pool: &mut enostr::RelayPool,
|
||||||
|
) -> bool {
|
||||||
|
if index >= self.channels.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel = &mut self.channels[index];
|
||||||
|
|
||||||
|
// Unsubscribe from old timeline if hashtags changed
|
||||||
|
let old_timeline_kind = channel.timeline_kind.clone();
|
||||||
|
let new_timeline_kind = TimelineKind::Hashtag(hashtags.clone());
|
||||||
|
|
||||||
|
if old_timeline_kind != new_timeline_kind {
|
||||||
|
if let Err(err) = timeline_cache.pop(&old_timeline_kind, ndb, pool) {
|
||||||
|
error!("Failed to unsubscribe from old channel timeline: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update channel data
|
||||||
|
channel.name = name;
|
||||||
|
channel.hashtags = hashtags.clone();
|
||||||
|
channel.timeline_kind = new_timeline_kind.clone();
|
||||||
|
channel.router = Router::new(vec![Route::timeline(new_timeline_kind)]);
|
||||||
|
|
||||||
|
info!("Updated channel: {}", channel.name);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_channel(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
timeline_cache: &mut TimelineCache,
|
||||||
|
ndb: &mut nostrdb::Ndb,
|
||||||
|
pool: &mut enostr::RelayPool,
|
||||||
|
) -> Option<Channel> {
|
||||||
if index < self.channels.len() && self.channels.len() > 1 {
|
if index < self.channels.len() && self.channels.len() > 1 {
|
||||||
let removed = self.channels.remove(index);
|
let removed = self.channels.remove(index);
|
||||||
|
|
||||||
|
// Unsubscribe from the timeline
|
||||||
|
if let Err(err) = timeline_cache.pop(&removed.timeline_kind, ndb, pool) {
|
||||||
|
error!("Failed to unsubscribe from channel timeline: {err}");
|
||||||
|
} else {
|
||||||
|
info!("Unsubscribed from removed channel: {}", removed.name);
|
||||||
|
}
|
||||||
|
|
||||||
// Adjust selected index if needed
|
// Adjust selected index if needed
|
||||||
if self.selected >= self.channels.len() {
|
if self.selected >= self.channels.len() {
|
||||||
self.selected = self.channels.len() - 1;
|
self.selected = self.channels.len() - 1;
|
||||||
|
|||||||
@@ -136,13 +136,24 @@ impl SerializableChannelList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn channel_list(self) -> ChannelList {
|
fn channel_list(self) -> ChannelList {
|
||||||
ChannelList {
|
let channels: Vec<_> = self
|
||||||
channels: self
|
|
||||||
.channels
|
.channels
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| c.channel())
|
.map(|c| c.channel())
|
||||||
.collect(),
|
.collect();
|
||||||
selected: self.selected,
|
|
||||||
|
// Ensure selected index is within bounds
|
||||||
|
let selected = if channels.is_empty() {
|
||||||
|
0
|
||||||
|
} else if self.selected >= channels.len() {
|
||||||
|
channels.len() - 1
|
||||||
|
} else {
|
||||||
|
self.selected
|
||||||
|
};
|
||||||
|
|
||||||
|
ChannelList {
|
||||||
|
channels,
|
||||||
|
selected,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ pub struct ChannelDialog {
|
|||||||
pub hashtags: String,
|
pub hashtags: String,
|
||||||
pub is_open: bool,
|
pub is_open: bool,
|
||||||
pub focus_requested: bool,
|
pub focus_requested: bool,
|
||||||
|
pub editing_index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ChannelDialogAction {
|
pub enum ChannelDialogAction {
|
||||||
Create { name: String, hashtags: Vec<String> },
|
Create { name: String, hashtags: Vec<String> },
|
||||||
|
Edit { index: usize, name: String, hashtags: Vec<String> },
|
||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ impl ChannelDialog {
|
|||||||
hashtags: String::new(),
|
hashtags: String::new(),
|
||||||
is_open: false,
|
is_open: false,
|
||||||
focus_requested: false,
|
focus_requested: false,
|
||||||
|
editing_index: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +32,15 @@ impl ChannelDialog {
|
|||||||
self.name.clear();
|
self.name.clear();
|
||||||
self.hashtags.clear();
|
self.hashtags.clear();
|
||||||
self.focus_requested = false;
|
self.focus_requested = false;
|
||||||
|
self.editing_index = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_for_edit(&mut self, index: usize, name: String, hashtags: Vec<String>) {
|
||||||
|
self.is_open = true;
|
||||||
|
self.name = name;
|
||||||
|
self.hashtags = hashtags.join(", ");
|
||||||
|
self.focus_requested = false;
|
||||||
|
self.editing_index = Some(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(&mut self) {
|
pub fn close(&mut self) {
|
||||||
@@ -46,7 +58,13 @@ impl ChannelDialog {
|
|||||||
|
|
||||||
let mut action: Option<ChannelDialogAction> = None;
|
let mut action: Option<ChannelDialogAction> = None;
|
||||||
|
|
||||||
egui::Window::new(tr!(i18n, "Create Channel", "Dialog title for creating a new channel"))
|
let title = if self.editing_index.is_some() {
|
||||||
|
tr!(i18n, "Edit Channel", "Dialog title for editing a channel")
|
||||||
|
} else {
|
||||||
|
tr!(i18n, "Create Channel", "Dialog title for creating a new channel")
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::Window::new(title)
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.anchor(egui::Align2::CENTER_CENTER, Vec2::ZERO)
|
.anchor(egui::Align2::CENTER_CENTER, Vec2::ZERO)
|
||||||
@@ -125,10 +143,18 @@ impl ChannelDialog {
|
|||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
action = Some(ChannelDialogAction::Create {
|
action = if let Some(index) = self.editing_index {
|
||||||
|
Some(ChannelDialogAction::Edit {
|
||||||
|
index,
|
||||||
name: self.name.trim().to_string(),
|
name: self.name.trim().to_string(),
|
||||||
hashtags,
|
hashtags,
|
||||||
});
|
})
|
||||||
|
} else {
|
||||||
|
Some(ChannelDialogAction::Create {
|
||||||
|
name: self.name.trim().to_string(),
|
||||||
|
hashtags,
|
||||||
|
})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -138,18 +164,24 @@ impl ChannelDialog {
|
|||||||
// Buttons
|
// Buttons
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
// Create button (hashtags are optional)
|
// Create/Save button (hashtags are optional)
|
||||||
let create_enabled = !self.name.trim().is_empty();
|
let button_enabled = !self.name.trim().is_empty();
|
||||||
|
|
||||||
let create_button = egui::Button::new(
|
let button_text = if self.editing_index.is_some() {
|
||||||
RichText::new(tr!(i18n, "Create", "Button to create channel"))
|
tr!(i18n, "Save", "Button to save channel edits")
|
||||||
|
} else {
|
||||||
|
tr!(i18n, "Create", "Button to create channel")
|
||||||
|
};
|
||||||
|
|
||||||
|
let button = egui::Button::new(
|
||||||
|
RichText::new(button_text)
|
||||||
.size(14.0),
|
.size(14.0),
|
||||||
)
|
)
|
||||||
.min_size(Vec2::new(80.0, 32.0));
|
.min_size(Vec2::new(80.0, 32.0));
|
||||||
|
|
||||||
let create_response = ui.add_enabled(create_enabled, create_button);
|
let button_response = ui.add_enabled(button_enabled, button);
|
||||||
|
|
||||||
if create_response.clicked() {
|
if button_response.clicked() {
|
||||||
let hashtags: Vec<String> = self
|
let hashtags: Vec<String> = self
|
||||||
.hashtags
|
.hashtags
|
||||||
.split(',')
|
.split(',')
|
||||||
@@ -157,10 +189,18 @@ impl ChannelDialog {
|
|||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
action = Some(ChannelDialogAction::Create {
|
action = if let Some(index) = self.editing_index {
|
||||||
|
Some(ChannelDialogAction::Edit {
|
||||||
|
index,
|
||||||
name: self.name.trim().to_string(),
|
name: self.name.trim().to_string(),
|
||||||
hashtags,
|
hashtags,
|
||||||
});
|
})
|
||||||
|
} else {
|
||||||
|
Some(ChannelDialogAction::Create {
|
||||||
|
name: self.name.trim().to_string(),
|
||||||
|
hashtags,
|
||||||
|
})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use egui::{
|
use egui::{
|
||||||
vec2, Color32, CursorIcon, InnerResponse, Margin, Rect, RichText, ScrollArea,
|
vec2, Color32, CursorIcon, Margin, Rect, RichText, ScrollArea,
|
||||||
Separator, Stroke, TextStyle, Widget,
|
Separator, Stroke, TextStyle, Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ pub struct ChannelSidebar<'a> {
|
|||||||
pub enum ChannelSidebarAction {
|
pub enum ChannelSidebarAction {
|
||||||
SelectChannel(usize),
|
SelectChannel(usize),
|
||||||
AddChannel,
|
AddChannel,
|
||||||
|
DeleteChannel(usize),
|
||||||
|
EditChannel(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChannelSidebarResponse {
|
pub struct ChannelSidebarResponse {
|
||||||
@@ -88,26 +90,32 @@ impl<'a> ChannelSidebar<'a> {
|
|||||||
let scroll_response = ScrollArea::vertical()
|
let scroll_response = ScrollArea::vertical()
|
||||||
.id_salt("channel_list")
|
.id_salt("channel_list")
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let mut selected_response = None;
|
let mut selected_action = None;
|
||||||
|
|
||||||
for (index, channel) in channel_list.channels.iter().enumerate() {
|
for (index, channel) in channel_list.channels.iter().enumerate() {
|
||||||
let is_selected = index == selected_index;
|
let is_selected = index == selected_index;
|
||||||
let resp = channel_item(ui, &channel.name, is_selected, channel.unread_count);
|
let resp = channel_item(ui, &channel.name, is_selected, channel.unread_count, channel_list.num_channels(), index, self.i18n);
|
||||||
|
|
||||||
if resp.clicked() {
|
match resp {
|
||||||
selected_response = Some(InnerResponse::new(
|
ChannelItemResponse::Select => {
|
||||||
ChannelSidebarAction::SelectChannel(index),
|
selected_action = Some(ChannelSidebarAction::SelectChannel(index));
|
||||||
resp,
|
}
|
||||||
));
|
ChannelItemResponse::Delete => {
|
||||||
|
selected_action = Some(ChannelSidebarAction::DeleteChannel(index));
|
||||||
|
}
|
||||||
|
ChannelItemResponse::Edit => {
|
||||||
|
selected_action = Some(ChannelSidebarAction::EditChannel(index));
|
||||||
|
}
|
||||||
|
ChannelItemResponse::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selected_response
|
selected_action
|
||||||
})
|
})
|
||||||
.inner;
|
.inner;
|
||||||
|
|
||||||
if scroll_response.is_some() {
|
if let Some(action) = scroll_response {
|
||||||
return scroll_response;
|
return Some(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
@@ -116,29 +124,44 @@ impl<'a> ChannelSidebar<'a> {
|
|||||||
let add_channel_resp = ui.add(add_channel_button(self.i18n));
|
let add_channel_resp = ui.add(add_channel_button(self.i18n));
|
||||||
|
|
||||||
if add_channel_resp.clicked() {
|
if add_channel_resp.clicked() {
|
||||||
Some(InnerResponse::new(
|
Some(ChannelSidebarAction::AddChannel)
|
||||||
ChannelSidebarAction::AddChannel,
|
|
||||||
add_channel_resp,
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.inner
|
.inner
|
||||||
.map(|inner| ChannelSidebarResponse::new(inner.inner, inner.response))
|
.map(|action| {
|
||||||
|
// We need to create a dummy response for ChannelSidebarResponse
|
||||||
|
// Use the UI's interact_rect to create a valid response
|
||||||
|
let dummy_rect = ui.available_rect_before_wrap();
|
||||||
|
let dummy_response = ui.interact(dummy_rect, ui.id().with("channel_sidebar"), egui::Sense::hover());
|
||||||
|
ChannelSidebarResponse::new(action, dummy_response)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ChannelItemResponse {
|
||||||
|
Select,
|
||||||
|
Delete,
|
||||||
|
Edit,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
fn channel_item(
|
fn channel_item(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
name: &str,
|
name: &str,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
unread_count: usize,
|
unread_count: usize,
|
||||||
) -> egui::Response {
|
total_channels: usize,
|
||||||
|
_channel_index: usize,
|
||||||
|
i18n: &mut Localization,
|
||||||
|
) -> ChannelItemResponse {
|
||||||
let desired_size = vec2(ui.available_width(), 36.0);
|
let desired_size = vec2(ui.available_width(), 36.0);
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||||
|
|
||||||
|
let mut action = ChannelItemResponse::None;
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
if ui.is_rect_visible(rect) {
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
let bg_color = if is_selected {
|
let bg_color = if is_selected {
|
||||||
@@ -169,7 +192,7 @@ fn channel_item(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw hashtag icon
|
// Draw hashtag icon
|
||||||
let icon_rect = Rect::from_min_size(
|
let icon_rect = egui::Rect::from_min_size(
|
||||||
rect.min + vec2(8.0, rect.height() / 2.0 - 8.0),
|
rect.min + vec2(8.0, rect.height() / 2.0 - 8.0),
|
||||||
vec2(16.0, 16.0),
|
vec2(16.0, 16.0),
|
||||||
);
|
);
|
||||||
@@ -177,12 +200,12 @@ fn channel_item(
|
|||||||
icon_rect.center(),
|
icon_rect.center(),
|
||||||
egui::Align2::CENTER_CENTER,
|
egui::Align2::CENTER_CENTER,
|
||||||
"#",
|
"#",
|
||||||
TextStyle::Body.resolve(ui.style()),
|
egui::TextStyle::Body.resolve(ui.style()),
|
||||||
visuals.text_color(),
|
visuals.text_color(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw channel name
|
// Draw channel name
|
||||||
let text_rect = Rect::from_min_size(
|
let text_rect = egui::Rect::from_min_size(
|
||||||
rect.min + vec2(32.0, 0.0),
|
rect.min + vec2(32.0, 0.0),
|
||||||
vec2(rect.width() - 64.0, rect.height()),
|
vec2(rect.width() - 64.0, rect.height()),
|
||||||
);
|
);
|
||||||
@@ -200,7 +223,7 @@ fn channel_item(
|
|||||||
text_rect.left_center(),
|
text_rect.left_center(),
|
||||||
egui::Align2::LEFT_CENTER,
|
egui::Align2::LEFT_CENTER,
|
||||||
name,
|
name,
|
||||||
TextStyle::Body.resolve(ui.style()),
|
egui::TextStyle::Body.resolve(ui.style()),
|
||||||
text_color,
|
text_color,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -213,7 +236,7 @@ fn channel_item(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let badge_size = vec2(24.0, 18.0);
|
let badge_size = vec2(24.0, 18.0);
|
||||||
let badge_rect = Rect::from_min_size(
|
let badge_rect = egui::Rect::from_min_size(
|
||||||
rect.max - vec2(badge_size.x + 8.0, rect.height() / 2.0 + badge_size.y / 2.0),
|
rect.max - vec2(badge_size.x + 8.0, rect.height() / 2.0 + badge_size.y / 2.0),
|
||||||
badge_size,
|
badge_size,
|
||||||
);
|
);
|
||||||
@@ -230,13 +253,34 @@ fn channel_item(
|
|||||||
badge_rect.center(),
|
badge_rect.center(),
|
||||||
egui::Align2::CENTER_CENTER,
|
egui::Align2::CENTER_CENTER,
|
||||||
&badge_text,
|
&badge_text,
|
||||||
TextStyle::Small.resolve(ui.style()),
|
egui::TextStyle::Small.resolve(ui.style()),
|
||||||
Color32::WHITE,
|
Color32::WHITE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response.on_hover_cursor(CursorIcon::PointingHand)
|
// Handle clicks
|
||||||
|
if response.clicked() {
|
||||||
|
action = ChannelItemResponse::Select;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show context menu on right-click
|
||||||
|
response.context_menu(|ui| {
|
||||||
|
if ui.button(tr!(i18n, "Edit Channel", "Context menu option to edit channel")).clicked() {
|
||||||
|
action = ChannelItemResponse::Edit;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow delete if not the last channel
|
||||||
|
if total_channels > 1 {
|
||||||
|
if ui.button(tr!(i18n, "Delete Channel", "Context menu option to delete channel")).clicked() {
|
||||||
|
action = ChannelItemResponse::Delete;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
action
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_channel_button(i18n: &mut Localization) -> impl Widget + '_ {
|
fn add_channel_button(i18n: &mut Localization) -> impl Widget + '_ {
|
||||||
|
|||||||
Reference in New Issue
Block a user