feat: página Markdown com explorador de ficheiros e preview
- Calendar movido para dentro da página Tasks (já não global) - Página Markdown: 30% file explorer + 70% preview - File explorer lista .md do diretório ~/dev/notes (configurável) - Ctrl+O altera diretório markdown - Ctrl+P Page Switcher navega entre Tasks e Markdown - Up/Down no file explorer carrega preview automaticamente - Focus::FileExplorer + Focus::MarkdownPreview no ciclo Tab - Bump ratatui 0.28 → 0.30; adicionado tui-markdown 0.3.7
This commit is contained in:
Generated
+1330
-77
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -4,7 +4,8 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ratatui = "0.28"
|
ratatui = "0.30"
|
||||||
|
tui-markdown = "0.3.7"
|
||||||
crossterm = "0.28"
|
crossterm = "0.28"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ Aplicação TUI de gestão de tarefas com sincronização Google Tasks.
|
|||||||
- Reordenação de tarefas com persistência
|
- Reordenação de tarefas com persistência
|
||||||
- Operações CRUD em listas e tarefas
|
- Operações CRUD em listas e tarefas
|
||||||
- Seleção múltipla e ações em lote
|
- Seleção múltipla e ações em lote
|
||||||
|
- **Página Markdown**: exploração e pré-visualização de ficheiros `.md`
|
||||||
|
- **Page Switcher**: alternar entre Tasks e Markdown
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- **UI:** ratatui + crossterm
|
- **UI:** ratatui 0.30 + crossterm
|
||||||
|
- **Markdown:** tui-markdown
|
||||||
- **Async:** tokio
|
- **Async:** tokio
|
||||||
- **DB:** rusqlite (SQLite)
|
- **DB:** rusqlite (SQLite)
|
||||||
- **Auth:** yup-oauth2 (OAuth 2.0)
|
- **Auth:** yup-oauth2 (OAuth 2.0)
|
||||||
@@ -54,4 +57,6 @@ cargo run
|
|||||||
| `Enter` | Completar/descompletar tarefa |
|
| `Enter` | Completar/descompletar tarefa |
|
||||||
| `t` | Definir data (d=today, t=tomorrow, w=week, m=month) |
|
| `t` | Definir data (d=today, t=tomorrow, w=week, m=month) |
|
||||||
| `Ctrl+r` | Sincronização forçada |
|
| `Ctrl+r` | Sincronização forçada |
|
||||||
|
| `Ctrl+p` | Abrir Page Switcher (Tasks / Markdown) |
|
||||||
|
| `Ctrl+o` | Alterar diretório de ficheiros Markdown |
|
||||||
| `q` | Sair |
|
| `q` | Sair |
|
||||||
|
|||||||
+105
-6
@@ -10,6 +10,8 @@ use tokio::sync::mpsc;
|
|||||||
use crate::domain::models::*;
|
use crate::domain::models::*;
|
||||||
use crate::infrastructure::api::ApiClient;
|
use crate::infrastructure::api::ApiClient;
|
||||||
use crate::infrastructure::db::Db;
|
use crate::infrastructure::db::Db;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::ui::{Focus, NetworkStatus, Page, Popup};
|
use crate::ui::{Focus, NetworkStatus, Page, Popup};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -42,6 +44,7 @@ pub struct App {
|
|||||||
pub task_list_scroll: u16,
|
pub task_list_scroll: u16,
|
||||||
pub detail_scroll: u16,
|
pub detail_scroll: u16,
|
||||||
pub notes_scroll: u16,
|
pub notes_scroll: u16,
|
||||||
|
pub markdown_scroll: u16,
|
||||||
pub calendar_scrolls: [u16; 4],
|
pub calendar_scrolls: [u16; 4],
|
||||||
pub calendar_active_week: usize,
|
pub calendar_active_week: usize,
|
||||||
pub db: Arc<Db>,
|
pub db: Arc<Db>,
|
||||||
@@ -60,6 +63,11 @@ pub struct App {
|
|||||||
pub popup_list_indices: Vec<(String, String)>,
|
pub popup_list_indices: Vec<(String, String)>,
|
||||||
pub popup_list_selected: usize,
|
pub popup_list_selected: usize,
|
||||||
pub page_switcher_selected: usize,
|
pub page_switcher_selected: usize,
|
||||||
|
pub markdown_dir: String,
|
||||||
|
pub markdown_files: Vec<String>,
|
||||||
|
markdown_file_paths: Vec<PathBuf>,
|
||||||
|
pub markdown_selected_file: usize,
|
||||||
|
pub markdown_content: String,
|
||||||
auth_tx: std_mpsc::Sender<AuthEvent>,
|
auth_tx: std_mpsc::Sender<AuthEvent>,
|
||||||
auth_rx: std_mpsc::Receiver<AuthEvent>,
|
auth_rx: std_mpsc::Receiver<AuthEvent>,
|
||||||
sync_tx: mpsc::Sender<SyncCommand>,
|
sync_tx: mpsc::Sender<SyncCommand>,
|
||||||
@@ -122,6 +130,7 @@ impl App {
|
|||||||
task_list_scroll: 0,
|
task_list_scroll: 0,
|
||||||
detail_scroll: 0,
|
detail_scroll: 0,
|
||||||
notes_scroll: 0,
|
notes_scroll: 0,
|
||||||
|
markdown_scroll: 0,
|
||||||
calendar_scrolls: [0; 4],
|
calendar_scrolls: [0; 4],
|
||||||
calendar_active_week: 0,
|
calendar_active_week: 0,
|
||||||
db,
|
db,
|
||||||
@@ -139,6 +148,11 @@ impl App {
|
|||||||
popup_list_indices: Vec::new(),
|
popup_list_indices: Vec::new(),
|
||||||
popup_list_selected: 0,
|
popup_list_selected: 0,
|
||||||
page_switcher_selected: 0,
|
page_switcher_selected: 0,
|
||||||
|
markdown_dir: expand_tilde("~/dev/notes"),
|
||||||
|
markdown_files: Vec::new(),
|
||||||
|
markdown_file_paths: Vec::new(),
|
||||||
|
markdown_selected_file: 0,
|
||||||
|
markdown_content: String::new(),
|
||||||
auth_tx,
|
auth_tx,
|
||||||
auth_rx,
|
auth_rx,
|
||||||
sync_tx,
|
sync_tx,
|
||||||
@@ -342,6 +356,13 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if key.code == KeyCode::Char('o') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
|
self.popup_input = self.markdown_dir.clone();
|
||||||
|
self.popup_cursor = self.popup_input.len();
|
||||||
|
self.show_popup = Some(Popup::Input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
match self.focus {
|
match self.focus {
|
||||||
@@ -350,14 +371,21 @@ impl App {
|
|||||||
self.calendar_active_week += 1;
|
self.calendar_active_week += 1;
|
||||||
} else {
|
} else {
|
||||||
self.calendar_active_week = 0;
|
self.calendar_active_week = 0;
|
||||||
self.focus = Focus::TaskList;
|
self.focus = match self.current_page {
|
||||||
|
Page::Tasks => Focus::TaskList,
|
||||||
|
Page::Markdown => Focus::FileExplorer,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
self.focus = match self.focus {
|
self.focus = match (&self.current_page, &self.focus) {
|
||||||
Focus::TaskList => Focus::Detail,
|
(Page::Tasks, Focus::TaskList) => Focus::Detail,
|
||||||
Focus::Detail => Focus::Calendar,
|
(Page::Tasks, Focus::Detail) => Focus::Calendar,
|
||||||
Focus::Calendar => Focus::TaskList,
|
(Page::Tasks, Focus::Calendar) => Focus::TaskList,
|
||||||
|
(Page::Tasks, _) => Focus::TaskList,
|
||||||
|
(Page::Markdown, Focus::FileExplorer) => Focus::MarkdownPreview,
|
||||||
|
(Page::Markdown, Focus::MarkdownPreview) => Focus::FileExplorer,
|
||||||
|
(Page::Markdown, _) => Focus::FileExplorer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,6 +430,15 @@ impl App {
|
|||||||
Focus::Calendar => {
|
Focus::Calendar => {
|
||||||
self.calendar_scrolls[self.calendar_active_week] = self.calendar_scrolls[self.calendar_active_week].saturating_sub(1);
|
self.calendar_scrolls[self.calendar_active_week] = self.calendar_scrolls[self.calendar_active_week].saturating_sub(1);
|
||||||
}
|
}
|
||||||
|
Focus::FileExplorer => {
|
||||||
|
if self.markdown_selected_file > 0 {
|
||||||
|
self.markdown_selected_file -= 1;
|
||||||
|
self.load_markdown_file();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Focus::MarkdownPreview => {
|
||||||
|
self.markdown_scroll = self.markdown_scroll.saturating_sub(1);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
KeyCode::Down => match self.focus {
|
KeyCode::Down => match self.focus {
|
||||||
Focus::TaskList => {
|
Focus::TaskList => {
|
||||||
@@ -417,6 +454,15 @@ impl App {
|
|||||||
Focus::Calendar => {
|
Focus::Calendar => {
|
||||||
self.calendar_scrolls[self.calendar_active_week] = self.calendar_scrolls[self.calendar_active_week].saturating_add(1);
|
self.calendar_scrolls[self.calendar_active_week] = self.calendar_scrolls[self.calendar_active_week].saturating_add(1);
|
||||||
}
|
}
|
||||||
|
Focus::FileExplorer => {
|
||||||
|
if self.markdown_selected_file + 1 < self.markdown_files.len() {
|
||||||
|
self.markdown_selected_file += 1;
|
||||||
|
self.load_markdown_file();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Focus::MarkdownPreview => {
|
||||||
|
self.markdown_scroll += 1;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
KeyCode::Right => {
|
KeyCode::Right => {
|
||||||
match self.focus {
|
match self.focus {
|
||||||
@@ -568,7 +614,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let input = self.popup_input.trim().to_string();
|
let input = self.popup_input.trim().to_string();
|
||||||
if self.pending_bulk_move && !input.is_empty() {
|
if self.current_page == Page::Markdown && !input.is_empty() {
|
||||||
|
self.markdown_dir = expand_tilde(&input);
|
||||||
|
self.scan_markdown_files();
|
||||||
|
self.show_popup = None;
|
||||||
|
} else if self.pending_bulk_move && !input.is_empty() {
|
||||||
self.pending_bulk_move = false;
|
self.pending_bulk_move = false;
|
||||||
self.bulk_move_to_new_list(&input);
|
self.bulk_move_to_new_list(&input);
|
||||||
self.show_popup = None;
|
self.show_popup = None;
|
||||||
@@ -956,6 +1006,13 @@ impl App {
|
|||||||
0 => Page::Tasks,
|
0 => Page::Tasks,
|
||||||
_ => Page::Markdown,
|
_ => Page::Markdown,
|
||||||
};
|
};
|
||||||
|
self.focus = match self.current_page {
|
||||||
|
Page::Tasks => Focus::TaskList,
|
||||||
|
Page::Markdown => Focus::FileExplorer,
|
||||||
|
};
|
||||||
|
if let Page::Markdown = self.current_page {
|
||||||
|
self.scan_markdown_files();
|
||||||
|
}
|
||||||
self.show_popup = None;
|
self.show_popup = None;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -1257,6 +1314,39 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scan_markdown_files(&mut self) {
|
||||||
|
let dir = PathBuf::from(&self.markdown_dir);
|
||||||
|
self.markdown_files.clear();
|
||||||
|
self.markdown_file_paths.clear();
|
||||||
|
self.markdown_selected_file = 0;
|
||||||
|
self.markdown_content.clear();
|
||||||
|
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().map_or(false, |e| e == "md") {
|
||||||
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
self.markdown_files.push(name.to_string());
|
||||||
|
self.markdown_file_paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.markdown_files.sort();
|
||||||
|
self.markdown_file_paths.sort();
|
||||||
|
self.load_markdown_file();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_markdown_file(&mut self) {
|
||||||
|
self.markdown_content.clear();
|
||||||
|
if self.markdown_selected_file < self.markdown_file_paths.len() {
|
||||||
|
let path = &self.markdown_file_paths[self.markdown_selected_file];
|
||||||
|
if let Ok(content) = std::fs::read_to_string(path) {
|
||||||
|
self.markdown_content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn load_lists(&mut self) {
|
fn load_lists(&mut self) {
|
||||||
self.lists = self.db.get_lists();
|
self.lists = self.db.get_lists();
|
||||||
let max_scroll = self.lists.len().saturating_sub(4);
|
let max_scroll = self.lists.len().saturating_sub(4);
|
||||||
@@ -1298,6 +1388,15 @@ fn sort_tasks(tasks: &mut Vec<Task>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn expand_tilde(path: &str) -> String {
|
||||||
|
if let Some(rest) = path.strip_prefix("~/") {
|
||||||
|
if let Ok(home) = std::env::var("HOME") {
|
||||||
|
return format!("{}/{}", home, rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn uuid_v4() -> String {
|
fn uuid_v4() -> String {
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ fn main() -> io::Result<()> {
|
|||||||
task_list_scroll: app.task_list_scroll,
|
task_list_scroll: app.task_list_scroll,
|
||||||
detail_scroll: app.detail_scroll,
|
detail_scroll: app.detail_scroll,
|
||||||
notes_scroll: app.notes_scroll,
|
notes_scroll: app.notes_scroll,
|
||||||
|
markdown_scroll: app.markdown_scroll,
|
||||||
calendar_scrolls: &app.calendar_scrolls,
|
calendar_scrolls: &app.calendar_scrolls,
|
||||||
calendar_active_week: app.calendar_active_week,
|
calendar_active_week: app.calendar_active_week,
|
||||||
auth_error: app.auth_error.as_deref(),
|
auth_error: app.auth_error.as_deref(),
|
||||||
@@ -183,6 +184,9 @@ fn main() -> io::Result<()> {
|
|||||||
popup_list_indices: &app.popup_list_indices,
|
popup_list_indices: &app.popup_list_indices,
|
||||||
popup_list_selected: app.popup_list_selected,
|
popup_list_selected: app.popup_list_selected,
|
||||||
page_switcher_selected: app.page_switcher_selected,
|
page_switcher_selected: app.page_switcher_selected,
|
||||||
|
markdown_files: &app.markdown_files,
|
||||||
|
markdown_selected_file: app.markdown_selected_file,
|
||||||
|
markdown_content: &app.markdown_content,
|
||||||
};
|
};
|
||||||
draw(frame, view);
|
draw(frame, view);
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
+89
-7
@@ -47,22 +47,104 @@ pub fn render_page_menu(frame: &mut Frame, area: Rect, current_page: &Page) {
|
|||||||
let inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
frame.render_widget(&block, area);
|
frame.render_widget(&block, area);
|
||||||
|
|
||||||
let active = current_page == &Page::Tasks;
|
let items = [(Page::Tasks, " Tasks "), (Page::Markdown, " Markdown ")];
|
||||||
|
|
||||||
|
let mut spans = vec![Span::raw(" ")];
|
||||||
|
for (i, (page, label)) in items.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
spans.push(Span::raw(" | "));
|
||||||
|
}
|
||||||
|
let active = current_page == page;
|
||||||
let style = if active {
|
let style = if active {
|
||||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::Cyan)
|
Style::default().fg(Color::Cyan)
|
||||||
};
|
};
|
||||||
|
spans.push(Span::styled(*label, style));
|
||||||
|
}
|
||||||
|
|
||||||
let text = Line::from(vec![
|
let paragraph = Paragraph::new(Line::from(spans));
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled(" Tasks ", style),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let paragraph = Paragraph::new(text);
|
|
||||||
frame.render_widget(paragraph, inner);
|
frame.render_widget(paragraph, inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_file_explorer(
|
||||||
|
frame: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
files: &[String],
|
||||||
|
selected: usize,
|
||||||
|
focused: bool,
|
||||||
|
) {
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(if focused { FOCUS_COLOR } else { Color::DarkGray }))
|
||||||
|
.title(" Files ")
|
||||||
|
.title_alignment(Alignment::Left);
|
||||||
|
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(&block, area);
|
||||||
|
|
||||||
|
let items: Vec<ListItem> = files
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, name)| {
|
||||||
|
let style = if i == selected {
|
||||||
|
Style::default()
|
||||||
|
.fg(FOCUS_COLOR)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Cyan)
|
||||||
|
};
|
||||||
|
ListItem::new(Line::from(Span::styled(format!(" {}", name), style)))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(items)
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.bg(if focused { Color::DarkGray } else { Color::Black })
|
||||||
|
.fg(SELECTED_COLOR)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
.highlight_symbol("> ");
|
||||||
|
|
||||||
|
frame.render_stateful_widget(
|
||||||
|
list,
|
||||||
|
inner,
|
||||||
|
&mut ratatui::widgets::ListState::default().with_selected(Some(selected)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_markdown_preview(
|
||||||
|
frame: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
content: &str,
|
||||||
|
focused: bool,
|
||||||
|
scroll: u16,
|
||||||
|
) {
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(if focused { FOCUS_COLOR } else { Color::DarkGray }))
|
||||||
|
.title(" Preview ")
|
||||||
|
.title_alignment(Alignment::Left);
|
||||||
|
|
||||||
|
if content.is_empty() {
|
||||||
|
let text = Text::from(Line::from(Span::styled(
|
||||||
|
"Select a file to preview",
|
||||||
|
Style::default().fg(Color::DarkGray),
|
||||||
|
)));
|
||||||
|
let paragraph = Paragraph::new(text).block(block).alignment(Alignment::Center);
|
||||||
|
frame.render_widget(paragraph, area);
|
||||||
|
} else {
|
||||||
|
let md_text = tui_markdown::from_str(content);
|
||||||
|
let lines: Vec<Line> = md_text.lines.into_iter().map(|l| l).collect();
|
||||||
|
let paragraph = Paragraph::new(Text::from(lines))
|
||||||
|
.block(block)
|
||||||
|
.scroll((scroll, 0))
|
||||||
|
.wrap(Wrap { trim: false });
|
||||||
|
frame.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_task_list(
|
pub fn render_task_list(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
|
|||||||
+41
-26
@@ -2,10 +2,7 @@ pub mod components;
|
|||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout};
|
use ratatui::layout::{Constraint, Direction, Layout};
|
||||||
use ratatui::style::{Color, Style};
|
|
||||||
use ratatui::text::{Line, Span, Text};
|
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::SyncStats;
|
use crate::app::SyncStats;
|
||||||
@@ -17,6 +14,8 @@ pub enum Focus {
|
|||||||
TaskList,
|
TaskList,
|
||||||
Detail,
|
Detail,
|
||||||
Calendar,
|
Calendar,
|
||||||
|
FileExplorer,
|
||||||
|
MarkdownPreview,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -62,6 +61,7 @@ pub struct AppView<'a> {
|
|||||||
pub task_list_scroll: u16,
|
pub task_list_scroll: u16,
|
||||||
pub detail_scroll: u16,
|
pub detail_scroll: u16,
|
||||||
pub notes_scroll: u16,
|
pub notes_scroll: u16,
|
||||||
|
pub markdown_scroll: u16,
|
||||||
pub calendar_events: &'a [CalendarEvent],
|
pub calendar_events: &'a [CalendarEvent],
|
||||||
pub calendar_scrolls: &'a [u16; 4],
|
pub calendar_scrolls: &'a [u16; 4],
|
||||||
pub calendar_active_week: usize,
|
pub calendar_active_week: usize,
|
||||||
@@ -72,6 +72,9 @@ pub struct AppView<'a> {
|
|||||||
pub popup_list_indices: &'a [(String, String)],
|
pub popup_list_indices: &'a [(String, String)],
|
||||||
pub popup_list_selected: usize,
|
pub popup_list_selected: usize,
|
||||||
pub page_switcher_selected: usize,
|
pub page_switcher_selected: usize,
|
||||||
|
pub markdown_files: &'a [String],
|
||||||
|
pub markdown_selected_file: usize,
|
||||||
|
pub markdown_content: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(frame: &mut Frame, view: AppView) {
|
pub fn draw(frame: &mut Frame, view: AppView) {
|
||||||
@@ -82,24 +85,27 @@ pub fn draw(frame: &mut Frame, view: AppView) {
|
|||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
Constraint::Min(0),
|
Constraint::Min(0),
|
||||||
Constraint::Length(12),
|
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let menu_area = main_layout[0];
|
let menu_area = main_layout[0];
|
||||||
let body_area = main_layout[1];
|
let body_area = main_layout[1];
|
||||||
let calendar_area = main_layout[2];
|
let status_area = main_layout[2];
|
||||||
let status_area = main_layout[3];
|
|
||||||
|
|
||||||
render_page_menu(frame, menu_area, view.current_page);
|
render_page_menu(frame, menu_area, view.current_page);
|
||||||
|
|
||||||
match view.current_page {
|
match view.current_page {
|
||||||
Page::Tasks => {
|
Page::Tasks => {
|
||||||
|
let tasks_cal = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(0), Constraint::Length(12)])
|
||||||
|
.split(body_area);
|
||||||
|
|
||||||
let body_layout = Layout::default()
|
let body_layout = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
.split(body_area);
|
.split(tasks_cal[0]);
|
||||||
|
|
||||||
let is_task_list_focused = view.focus == Focus::TaskList;
|
let is_task_list_focused = view.focus == Focus::TaskList;
|
||||||
render_task_list(
|
render_task_list(
|
||||||
@@ -123,33 +129,42 @@ pub fn draw(frame: &mut Frame, view: AppView) {
|
|||||||
is_detail_focused,
|
is_detail_focused,
|
||||||
view.detail_scroll,
|
view.detail_scroll,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
Page::Markdown => {
|
|
||||||
let block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::DarkGray))
|
|
||||||
.title(" Markdown ")
|
|
||||||
.title_alignment(Alignment::Left);
|
|
||||||
let text = Text::from(Line::from(Span::styled(
|
|
||||||
"Markdown preview coming soon",
|
|
||||||
Style::default().fg(Color::DarkGray),
|
|
||||||
)));
|
|
||||||
let paragraph = Paragraph::new(text)
|
|
||||||
.block(block)
|
|
||||||
.alignment(Alignment::Center);
|
|
||||||
frame.render_widget(paragraph, body_area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_calendar_focused = view.focus == Focus::Calendar;
|
let is_calendar_focused = view.focus == Focus::Calendar;
|
||||||
render_calendar_panel(
|
render_calendar_panel(
|
||||||
frame,
|
frame,
|
||||||
calendar_area,
|
tasks_cal[1],
|
||||||
view.calendar_events,
|
view.calendar_events,
|
||||||
is_calendar_focused,
|
is_calendar_focused,
|
||||||
view.calendar_scrolls,
|
view.calendar_scrolls,
|
||||||
view.calendar_active_week,
|
view.calendar_active_week,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
Page::Markdown => {
|
||||||
|
let md_layout = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||||
|
.split(body_area);
|
||||||
|
|
||||||
|
let is_explorer_focused = view.focus == Focus::FileExplorer;
|
||||||
|
render_file_explorer(
|
||||||
|
frame,
|
||||||
|
md_layout[0],
|
||||||
|
view.markdown_files,
|
||||||
|
view.markdown_selected_file,
|
||||||
|
is_explorer_focused,
|
||||||
|
);
|
||||||
|
|
||||||
|
let is_preview_focused = view.focus == Focus::MarkdownPreview;
|
||||||
|
render_markdown_preview(
|
||||||
|
frame,
|
||||||
|
md_layout[1],
|
||||||
|
view.markdown_content,
|
||||||
|
is_preview_focused,
|
||||||
|
view.markdown_scroll,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render_status_bar(frame, status_area, view.network_status, view.sync_stats);
|
render_status_bar(frame, status_area, view.network_status, view.sync_stats);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user