refactor: migrate to DDD layered architecture
Split the monolithic app.rs (762 lines) into four DDD layers: - domain/: models, navigation enums (Tab, ModalKind), conversions, aggregates - application/: App state, LibraryState, NotificationManager, event handlers - infrastructure/: config, gRPC client, system utilities - presentation/: all render functions, widgets, views, modals Original modules (app, data, ui, config, grpc) preserved as thin re-export facades for backward compatibility. All 13 insta snapshot tests pass without modification.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Clear, Paragraph},
|
||||
};
|
||||
|
||||
use crate::application::notification_state::{format_elapsed, Notification, NotificationManager, MAX_VISIBLE};
|
||||
use crate::theme;
|
||||
|
||||
impl NotificationManager {
|
||||
pub fn render(&self, frame: &mut Frame, area: Rect) {
|
||||
let visible: Vec<&Notification> = self
|
||||
.active()
|
||||
.iter()
|
||||
.rev()
|
||||
.take(MAX_VISIBLE)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect();
|
||||
|
||||
if visible.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let notif_width = 50u16.min(area.width.saturating_sub(4));
|
||||
let notif_height = 3u16;
|
||||
let spacing = 1u16;
|
||||
let total_height = visible.len() as u16 * (notif_height + spacing);
|
||||
|
||||
let start_y = area.y + area.height.saturating_sub(total_height + 1);
|
||||
let start_x = area.x + area.width.saturating_sub(notif_width + 2);
|
||||
|
||||
for (i, notif) in visible.iter().enumerate() {
|
||||
let y = start_y + (i as u16) * (notif_height + spacing);
|
||||
let notif_area = Rect::new(start_x, y, notif_width, notif_height);
|
||||
|
||||
frame.render_widget(Clear, notif_area);
|
||||
|
||||
let border_color = notif.kind.color();
|
||||
let block = Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.border_style(Style::default().fg(border_color))
|
||||
.style(Style::default().bg(theme::BG1));
|
||||
|
||||
let inner = block.inner(notif_area);
|
||||
frame.render_widget(block, notif_area);
|
||||
|
||||
let elapsed = Instant::now().duration_since(notif.created_at).as_secs();
|
||||
let timestamp = if elapsed == 0 {
|
||||
"now".to_string()
|
||||
} else {
|
||||
format!("{}s", elapsed)
|
||||
};
|
||||
|
||||
let mut lines = vec![Line::from(vec![
|
||||
Span::styled(¬if.icon, Style::default().fg(border_color)),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
¬if.title,
|
||||
Style::default()
|
||||
.fg(theme::FG1)
|
||||
.add_modifier(ratatui::style::Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled(timestamp, Style::default().fg(theme::GRAY)),
|
||||
])];
|
||||
|
||||
if let Some(detail) = ¬if.detail {
|
||||
let max_len = inner.width.saturating_sub(2) as usize;
|
||||
let d = if detail.len() > max_len {
|
||||
format!("{}…", &detail[..max_len.saturating_sub(1)])
|
||||
} else {
|
||||
detail.clone()
|
||||
};
|
||||
lines.push(Line::from(Span::styled(
|
||||
d,
|
||||
Style::default().fg(theme::GRAY),
|
||||
)));
|
||||
}
|
||||
|
||||
let para = Paragraph::new(lines);
|
||||
frame.render_widget(para, inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_notification_item(frame: &mut Frame, area: Rect, notif: &Notification) {
|
||||
let border_color = notif.kind.color();
|
||||
let block = Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.border_style(Style::default().fg(border_color))
|
||||
.style(Style::default().bg(theme::BG1));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let elapsed = Instant::now().duration_since(notif.created_at).as_secs();
|
||||
let timestamp = format_elapsed(elapsed);
|
||||
|
||||
let mut lines = vec![Line::from(vec![
|
||||
Span::styled(¬if.icon, Style::default().fg(border_color)),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
notif.kind.label(),
|
||||
Style::default()
|
||||
.fg(theme::FG1)
|
||||
.add_modifier(ratatui::style::Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled(timestamp, Style::default().fg(theme::GRAY)),
|
||||
])];
|
||||
|
||||
if let Some(detail) = ¬if.detail {
|
||||
let max_len = inner.width.saturating_sub(2) as usize;
|
||||
let d = if detail.len() > max_len {
|
||||
format!("{}…", &detail[..max_len.saturating_sub(1)])
|
||||
} else {
|
||||
detail.clone()
|
||||
};
|
||||
lines.push(Line::from(Span::styled(
|
||||
d,
|
||||
Style::default().fg(theme::GRAY),
|
||||
)));
|
||||
} else {
|
||||
let max_len = inner.width.saturating_sub(2) as usize;
|
||||
let title = if notif.title.len() > max_len {
|
||||
format!("{}…", ¬if.title[..max_len.saturating_sub(1)])
|
||||
} else {
|
||||
notif.title.clone()
|
||||
};
|
||||
lines.push(Line::from(Span::styled(
|
||||
title,
|
||||
Style::default().fg(theme::GRAY),
|
||||
)));
|
||||
}
|
||||
|
||||
let para = Paragraph::new(lines);
|
||||
frame.render_widget(para, inner);
|
||||
}
|
||||
Reference in New Issue
Block a user