Initial commit — ferrosonic terminal Subsonic client
Terminal-based Subsonic music client in Rust featuring bit-perfect audio playback via PipeWire sample rate switching, gapless playback, MPRIS2 desktop integration, cava audio visualizer with theme-matched gradients, 13 built-in color themes with custom TOML theme support, mouse controls, artist/album browser, playlist support, and play queue management. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
112
src/ui/layout.rs
Normal file
112
src/ui/layout.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! Main layout and rendering
|
||||
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::state::{AppState, LayoutAreas, Page};
|
||||
|
||||
use super::footer::Footer;
|
||||
use super::header::Header;
|
||||
use super::pages;
|
||||
use super::widgets::{CavaWidget, NowPlayingWidget};
|
||||
|
||||
/// Draw the entire UI
|
||||
pub fn draw(frame: &mut Frame, state: &mut AppState) {
|
||||
let area = frame.area();
|
||||
|
||||
let cava_active = state.settings_state.cava_enabled && !state.cava_screen.is_empty();
|
||||
|
||||
// Main layout:
|
||||
// [Header] - 1 line
|
||||
// [Cava] - ~40% (optional, only when cava is active)
|
||||
// [Page Content] - flexible
|
||||
// [Now Playing] - 7 lines
|
||||
// [Footer] - 1 line
|
||||
|
||||
let (header_area, cava_area, content_area, now_playing_area, footer_area) = if cava_active {
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Length(1), // Header
|
||||
Constraint::Percentage(40), // Cava visualizer — top half-ish
|
||||
Constraint::Min(10), // Page content
|
||||
Constraint::Length(7), // Now playing
|
||||
Constraint::Length(1), // Footer
|
||||
])
|
||||
.split(area);
|
||||
(chunks[0], Some(chunks[1]), chunks[2], chunks[3], chunks[4])
|
||||
} else {
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Length(1), // Header
|
||||
Constraint::Min(10), // Page content
|
||||
Constraint::Length(7), // Now playing
|
||||
Constraint::Length(1), // Footer
|
||||
])
|
||||
.split(area);
|
||||
(chunks[0], None, chunks[1], chunks[2], chunks[3])
|
||||
};
|
||||
|
||||
// Compute dual-pane splits for pages that use them
|
||||
let (content_left, content_right) = match state.page {
|
||||
Page::Artists | Page::Playlists => {
|
||||
let panes = Layout::horizontal([
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(60),
|
||||
])
|
||||
.split(content_area);
|
||||
(Some(panes[0]), Some(panes[1]))
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
// Store layout areas for mouse hit-testing
|
||||
state.layout = LayoutAreas {
|
||||
header: header_area,
|
||||
cava: cava_area,
|
||||
content: content_area,
|
||||
now_playing: now_playing_area,
|
||||
footer: footer_area,
|
||||
content_left,
|
||||
content_right,
|
||||
};
|
||||
|
||||
// Render header
|
||||
let colors = *state.settings_state.theme_colors();
|
||||
let header = Header::new(state.page, state.now_playing.state, colors);
|
||||
frame.render_widget(header, header_area);
|
||||
|
||||
// Render cava visualizer if active
|
||||
if let Some(cava_rect) = cava_area {
|
||||
let cava_widget = CavaWidget::new(&state.cava_screen);
|
||||
frame.render_widget(cava_widget, cava_rect);
|
||||
}
|
||||
|
||||
// Render current page
|
||||
match state.page {
|
||||
Page::Artists => {
|
||||
pages::artists::render(frame, content_area, state);
|
||||
}
|
||||
Page::Queue => {
|
||||
pages::queue::render(frame, content_area, state);
|
||||
}
|
||||
Page::Playlists => {
|
||||
pages::playlists::render(frame, content_area, state);
|
||||
}
|
||||
Page::Server => {
|
||||
pages::server::render(frame, content_area, state);
|
||||
}
|
||||
Page::Settings => {
|
||||
pages::settings::render(frame, content_area, state);
|
||||
}
|
||||
}
|
||||
|
||||
// Render now playing
|
||||
let now_playing = NowPlayingWidget::new(&state.now_playing, colors);
|
||||
frame.render_widget(now_playing, now_playing_area);
|
||||
|
||||
// Render footer
|
||||
let footer = Footer::new(state.page, colors)
|
||||
.sample_rate(state.now_playing.sample_rate)
|
||||
.notification(state.notification.as_ref());
|
||||
frame.render_widget(footer, footer_area);
|
||||
}
|
||||
Reference in New Issue
Block a user