diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb06f5..340e91d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,4 +3,7 @@ CHANGELOG ## master +Features: + - Highlight and allow selection of current split in activity view. +- Added activity ranking. diff --git a/src/app.rs b/src/app.rs index 9c7b60c..0beee91 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,8 +4,6 @@ use std::{ time::{Duration, SystemTime}, }; -use strum::EnumIter; - use tokio::sync::mpsc::{Receiver, Sender}; use tui::{ backend::{Backend, CrosstermBackend}, @@ -18,7 +16,7 @@ use crate::{ component::activity_list::{ActivityListMode, ActivityListState, ActivityViewState}, event::{input::EventSender, util::{table_state_prev, table_state_next}}, input::InputEvent, - store::{activity::{ActivityStore, Activities}}, + store::{activity::{ActivityStore, Activities, SortBy, SortOrder}}, }; use crate::{ component::{activity_list, activity_view, unit_formatter::UnitFormatter}, @@ -34,6 +32,11 @@ pub struct ActivityFilters { pub filter: String, } +pub struct RankOptions { + pub rank_by: SortBy, + pub rank_order: SortOrder, +} + impl ActivityFilters { pub fn anchor_tolerance_add(&mut self, delta: f64) { self.anchor_tolerance += delta; @@ -73,12 +76,12 @@ pub struct App<'a> { pub activity_list: ActivityListState, pub activity_view: ActivityViewState, pub filters: ActivityFilters, + pub ranking: RankOptions, pub activity_type: Option, pub activity: Option, pub activity_anchored: Option, pub activities: Activities, - pub activities_filtered: Activities, pub info_message: Option, pub error_message: Option, @@ -96,39 +99,6 @@ pub enum ActivePage { Activity, } -#[derive(EnumIter)] -pub enum SortBy { - Date, - Distance, - Pace, - HeartRate, - Time, -} - -impl Display for SortBy { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.to_label()) - } -} - -pub enum SortOrder { - Asc, - Desc, -} - -impl Display for SortOrder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - SortOrder::Asc => "ascending", - SortOrder::Desc => "descending", - } - ) - } -} - impl App<'_> { pub fn new<'a>( store: &'a mut ActivityStore<'a>, @@ -147,6 +117,7 @@ impl App<'_> { filter_text_area: Input::default(), filter_dialog: false, sort_dialog: false, + rank_dialog: false, }, activity_view: ActivityViewState { pace_table_state: TableState::default(), @@ -158,10 +129,13 @@ impl App<'_> { filter: "".to_string(), anchor_tolerance: 0.005, }, + ranking: RankOptions { + rank_by: SortBy::Pace, + rank_order: SortOrder::Desc, + }, activity: None, activity_anchored: None, activities: Activities::new(), - activities_filtered: Activities::new(), store, activity_type: None, @@ -177,8 +151,6 @@ impl App<'_> { &mut self, terminal: &mut Terminal>, ) -> Result<(), anyhow::Error> { - self.activities = self.store.activities().await; - loop { if self.quit { break; @@ -226,23 +198,21 @@ impl App<'_> { } pub async fn reload(&mut self) { - self.activities = self.store.activities().await; - self.activities_filtered = self.activities.where_title_contains(self.filters.filter.as_str()); + let mut activities = self.store.activities().await; + activities = activities.where_title_contains(self.filters.filter.as_str()); if let Some(activity_type) = self.activity_type.clone() { - self.activities_filtered = self.activities_filtered.having_activity_type(activity_type); + activities = activities.having_activity_type(activity_type); } if let Some(anchored) = &self.activity_anchored { - self.activities_filtered = self.activities_filtered.withing_distance_of(anchored, self.filters.anchor_tolerance); + activities = activities.withing_distance_of(anchored, self.filters.anchor_tolerance); } + self.activities = activities + .rank(&self.ranking.rank_by, &self.ranking.rank_order) + .sort(&self.filters.sort_by, &self.filters.sort_order) } - pub fn unsorted_filtered_activities(&self) -> Activities { - self.activities_filtered.clone() - } - - pub fn filtered_activities(&self) -> Activities { - let activities = self.unsorted_filtered_activities(); - activities.sort(&self.filters.sort_by, &self.filters.sort_order) + pub fn activities(&self) -> Activities { + self.activities.clone() } fn draw(&mut self, f: &mut Frame) -> Result<(), anyhow::Error> { @@ -261,7 +231,7 @@ impl App<'_> { } pub(crate) fn anchor_selected(&mut self) { - let activities = self.filtered_activities(); + let activities = self.activities(); if let Some(selected) = self.activity_list.table_state().selected() { if let Some(a) = activities.get(selected) { if self.activity_anchored.is_some() { @@ -280,11 +250,11 @@ impl App<'_> { pub(crate) fn previous_activity(&mut self) { table_state_prev( self.activity_list.table_state(), - self.activities_filtered.len(), + self.activities.len(), false, ); if let Some(selected) = self.activity_list.table_state().selected() { - if let Some(a) = self.activities_filtered.get(selected) { + if let Some(a) = self.activities.get(selected) { self.activity = Some(a.clone()); } } @@ -293,11 +263,11 @@ impl App<'_> { pub(crate) fn next_activity(&mut self) { table_state_next( self.activity_list.table_state(), - self.activities_filtered.len(), + self.activities.len(), false, ); if let Some(selected) = self.activity_list.table_state().selected() { - if let Some(a) = self.activities_filtered.get(selected) { + if let Some(a) = self.activities.get(selected) { self.activity = Some(a.clone()); } } diff --git a/src/component/activity_list/chart.rs b/src/component/activity_list/chart.rs index c87b3da..f881263 100644 --- a/src/component/activity_list/chart.rs +++ b/src/component/activity_list/chart.rs @@ -1,4 +1,4 @@ -use chrono::{NaiveDateTime}; +use chrono::NaiveDateTime; use tui::{ backend::Backend, layout::Constraint, @@ -10,21 +10,20 @@ use tui::{ }; use crate::{ - app::{App}, - event::{ - keymap::{MappedKey}, - }, + app::App, + event::keymap::MappedKey, + store::activity::{SortBy, SortOrder}, }; - - pub fn handle(_app: &mut App, _key: MappedKey) {} pub fn draw( app: &mut App, f: &mut Frame, area: tui::layout::Rect, ) -> Result<(), anyhow::Error> { - let activities = &app.unsorted_filtered_activities(); + let activities = &app + .activities() + .sort(&SortBy::Date, &SortOrder::Asc); let times: Vec = activities.timestamps(); let paces: Vec = activities.meter_per_hours(); let tmax = times.iter().max(); @@ -41,7 +40,8 @@ pub fn draw( } let pmin = pmin.unwrap(); let pmax = pmax.unwrap(); - let data: Vec<(f64, f64)> = activities.to_vec() + let data: Vec<(f64, f64)> = activities + .to_vec() .iter() .map(|a| { let ts = a.start_date.unwrap().timestamp(); @@ -50,11 +50,11 @@ pub fn draw( .collect(); let mut current = vec![]; if let Some(selected) = app.activity_list.table_state().selected() { - let activities = app.filtered_activities(); + let activities = app.activities(); if let Some(a) = activities.get(selected) { - if let Some(a) = app.activities.find(a.id) { - current.push((a.start_date.unwrap().timestamp() as f64, *pmin as f64)); - current.push((a.start_date.unwrap().timestamp() as f64, *pmax as f64)); + if let Some(a) = activities.find(a.id) { + current.push((a.start_date.unwrap().timestamp() as f64, *pmin as f64)); + current.push((a.start_date.unwrap().timestamp() as f64, *pmax as f64)); } } } @@ -71,17 +71,17 @@ pub fn draw( .marker(Marker::Braille) .graph_type(GraphType::Scatter) .style(Style::default().fg(Color::Magenta)), - Dataset::default().data(¤t) + Dataset::default() + .data(¤t) .name("Selected") .marker(Marker::Braille) .graph_type(GraphType::Line) .style(Style::default().fg(Color::Green)), ]; let yaxisstep = (pdiff as f64 / area.height as f64) as usize; - let yaxis = - (*pmin..*pmax).step_by(if yaxisstep > 0 { yaxisstep } else { 1 }); + let yaxis = (*pmin..*pmax).step_by(if yaxisstep > 0 { yaxisstep } else { 1 }); let xaxisstep = (tdiff as f64 / 5.0) as usize; - let xaxis = (*tmin.unwrap()..*tmax.unwrap()).step_by(if xaxisstep > 0 { xaxisstep } else {1}); + let xaxis = (*tmin.unwrap()..*tmax.unwrap()).step_by(if xaxisstep > 0 { xaxisstep } else { 1 }); let chart = Chart::new(datasets) .hidden_legend_constraints((Constraint::Max(1), Constraint::Max(1))) .block(Block::default().borders(Borders::all())) @@ -105,7 +105,10 @@ pub fn draw( Axis::default() .title(Span::styled("Pace", Style::default().fg(Color::Red))) .style(Style::default().fg(Color::White)) - .bounds([*pmin as f64, *pmax as f64 + (pdiff as f64 / activities.len() as f64)]) + .bounds([ + *pmin as f64, + *pmax as f64 + (pdiff as f64 / activities.len() as f64), + ]) .labels( yaxis .map(|p| Span::from(app.unit_formatter.pace(3600, p as f64))) diff --git a/src/component/activity_list/list.rs b/src/component/activity_list/list.rs index 7d7d7f3..7728155 100644 --- a/src/component/activity_list/list.rs +++ b/src/component/activity_list/list.rs @@ -10,15 +10,15 @@ use tui::{ use tui_input::backend::crossterm::EventHandler; use crate::{ - app::{App, SortOrder}, + app::App, event::{ keymap::{MappedKey, StravaEvent}, input::InputEvent, }, - store::activity::{Activities}, + store::activity::{Activities, SortOrder}, ui::{centered_rect_absolute, color::ColorTheme}, component::{table_status_select_current}, }; -use super::sort_dialog; +use super::{sort_dialog, rank_dialog}; pub fn handle(app: &mut App, key: MappedKey) { if app.activity_list.filter_dialog { @@ -46,6 +46,11 @@ pub fn handle(app: &mut App, key: MappedKey) { return; } + if app.activity_list.rank_dialog { + rank_dialog::handle(app, key); + + return; + } match key.strava_event { StravaEvent::Quit => app.quit = true, StravaEvent::ToggleUnitSystem => { @@ -55,12 +60,14 @@ pub fn handle(app: &mut App, key: MappedKey) { app.filters.sort_order = match app.filters.sort_order { SortOrder::Asc => SortOrder::Desc, SortOrder::Desc => SortOrder::Asc, - } + }; + app.send(InputEvent::Reload); } StravaEvent::Down => app.next_activity(), StravaEvent::Up => app.previous_activity(), StravaEvent::Filter => toggle_filter(app), StravaEvent::Sort => toggle_sort(app), + StravaEvent::Rank => toggle_rank(app), StravaEvent::Enter => table_status_select_current(app), StravaEvent::Refresh => app.send(InputEvent::Sync), StravaEvent::IncreaseTolerance => { @@ -86,13 +93,16 @@ fn toggle_filter(app: &mut App) { fn toggle_sort(app: &mut App) { app.activity_list.sort_dialog = !app.activity_list.sort_dialog; } +fn toggle_rank(app: &mut App) { + app.activity_list.rank_dialog = !app.activity_list.rank_dialog; +} pub fn draw( app: &mut App, f: &mut Frame, area: tui::layout::Rect, ) -> Result<(), anyhow::Error> { - let activities = &app.filtered_activities(); + let activities = &app.activities(); if app.activity_list.table_state().selected().is_none() && !activities.is_empty() { app.activity_list.table_state().select(Some(0)); @@ -129,6 +139,11 @@ pub fn draw( return Ok(()); } + if app.activity_list.rank_dialog { + rank_dialog::draw(app, f, f.size())?; + + return Ok(()); + } Ok(()) } @@ -142,8 +157,9 @@ pub fn activity_list_table<'a>(app: &App, activities: &'a Activities) -> Table<' "Dst", "🕑 Time", "👣 Pace", - "💓 Heart", + "💓 Avg. Heart", "🌄 Elevation", + "🪜 Rank", ]; let headers = header_names .iter() @@ -169,6 +185,7 @@ pub fn activity_list_table<'a>(app: &App, activities: &'a Activities) -> Table<' .map_or_else(|| "n/a".to_string(), |v| format!("{:.2}", v)), ), Cell::from(app.unit_formatter.elevation(activity.total_elevation_gain)), + Cell::from(format!("{}", activity.rank)), ])); } @@ -190,6 +207,7 @@ pub fn activity_list_table<'a>(app: &App, activities: &'a Activities) -> Table<' Constraint::Percentage(10), Constraint::Percentage(10), Constraint::Percentage(10), + Constraint::Percentage(10), ]) } diff --git a/src/component/activity_list/mod.rs b/src/component/activity_list/mod.rs index 8a99224..e826fc6 100644 --- a/src/component/activity_list/mod.rs +++ b/src/component/activity_list/mod.rs @@ -1,6 +1,7 @@ pub mod list; pub mod chart; pub mod sort_dialog; +pub mod rank_dialog; use tui::{ backend::Backend, @@ -38,6 +39,7 @@ pub struct ActivityListState { pub filter_text_area: Input, pub filter_dialog: bool, pub sort_dialog: bool, + pub rank_dialog: bool, } pub struct ActivityViewState { diff --git a/src/component/activity_list/rank_dialog.rs b/src/component/activity_list/rank_dialog.rs new file mode 100644 index 0000000..5a549b6 --- /dev/null +++ b/src/component/activity_list/rank_dialog.rs @@ -0,0 +1,55 @@ + + +use tui::{ + backend::Backend, + widgets::{Clear}, + Frame, +}; + +use crate::{ + app::App, + event::{keymap::{MappedKey, StravaEvent}, input::InputEvent}, + ui::{centered_rect_absolute}, store::activity::{SortBy, SortOrder}, +}; + +use super::sort_dialog::sort_option_paragraph; + +pub fn handle(app: &mut App, key: MappedKey) { + let matched = match key.strava_event { + StravaEvent::Enter => { + app.activity_list.rank_dialog = false; + true + }, + StravaEvent::Escape => { + app.activity_list.rank_dialog = false; + true + } + _ => false, + }; + + if matched { + return; + } + + if let Some(sort) = SortBy::from_key(key.key_event.code) { + app.ranking.rank_by = sort; + app.ranking.rank_order = match app.ranking.rank_by { + SortBy::Time => SortOrder::Asc, + _ => SortOrder::Desc, + }; + app.activity_list.rank_dialog = false; + app.send(InputEvent::Reload); + } +} + +pub fn draw( + app: &mut App, + f: &mut Frame, + area: tui::layout::Rect, +) -> Result<(), anyhow::Error> { + let rect = centered_rect_absolute(64, 3, area); + f.render_widget(Clear, rect); + f.render_widget(sort_option_paragraph(app, "Rank".to_string()), rect); + + Ok(()) +} diff --git a/src/component/activity_list/sort_dialog.rs b/src/component/activity_list/sort_dialog.rs index 8f65673..b321e4d 100644 --- a/src/component/activity_list/sort_dialog.rs +++ b/src/component/activity_list/sort_dialog.rs @@ -1,4 +1,4 @@ -use crossterm::event::KeyCode; + use strum::IntoEnumIterator; use tui::{ backend::Backend, @@ -9,50 +9,21 @@ use tui::{ }; use crate::{ - app::{App, SortBy}, - event::keymap::{MappedKey, StravaEvent}, - ui::{centered_rect_absolute, color::ColorTheme}, + app::App, + event::{keymap::{MappedKey, StravaEvent}, input::InputEvent}, + ui::{centered_rect_absolute, color::ColorTheme}, store::activity::SortBy, }; -impl SortBy { - pub fn to_key(&self) -> char { - match *self { - SortBy::Date => 'd', - SortBy::Pace => 'p', - SortBy::HeartRate => 'h', - SortBy::Distance => 'D', - SortBy::Time => 't', - } - } - - pub fn to_label(&self) -> &str { - match *self { - SortBy::Date => "date", - SortBy::Pace => "pace", - SortBy::HeartRate => "heartrate", - SortBy::Distance => "distance", - SortBy::Time => "time", - } - } - - pub fn from_key(key: KeyCode) -> Option { - match key { - KeyCode::Char('d') => Some(SortBy::Date), - KeyCode::Char('p') => Some(SortBy::Pace), - KeyCode::Char('h') => Some(SortBy::HeartRate), - KeyCode::Char('D') => Some(SortBy::Distance), - KeyCode::Char('t') => Some(SortBy::Time), - _ => None, - } - } -} - pub fn handle(app: &mut App, key: MappedKey) { let matched = match key.strava_event { StravaEvent::Enter => { app.activity_list.sort_dialog = false; true } + StravaEvent::Escape => { + app.activity_list.sort_dialog = false; + true + } _ => false, }; @@ -63,6 +34,7 @@ pub fn handle(app: &mut App, key: MappedKey) { if let Some(sort) = SortBy::from_key(key.key_event.code) { app.filters.sort_by = sort; app.activity_list.sort_dialog = false; + app.send(InputEvent::Reload); } } @@ -73,17 +45,11 @@ pub fn draw( ) -> Result<(), anyhow::Error> { let rect = centered_rect_absolute(64, 3, area); f.render_widget(Clear, rect); - let block = Block::default() - .title("Sort".to_string()) - .borders(Borders::ALL) - .style(Style::default().fg(ColorTheme::Dialog.to_color())); - - f.render_widget(block, rect); - f.render_widget(sort_option_paragraph(app), rect); + f.render_widget(sort_option_paragraph(app, "Sort".to_string()), rect); Ok(()) } -fn sort_option_paragraph<'a>(_app: &'a mut App) -> Paragraph<'a> { +pub fn sort_option_paragraph<'a>(_app: &'a mut App, title: String) -> Paragraph<'a> { let strava = ColorTheme::Orange.to_color(); let mut sorts = vec![]; @@ -111,7 +77,9 @@ fn sort_option_paragraph<'a>(_app: &'a mut App) -> Paragraph<'a> { Paragraph::new(text).block( Block::default() + .title(title) .borders(Borders::ALL) + .border_style(Style::default().fg(ColorTheme::Dialog.to_color())) .style(Style::default()), ) } diff --git a/src/component/mod.rs b/src/component/mod.rs index 7a0c55d..57edb1d 100644 --- a/src/component/mod.rs +++ b/src/component/mod.rs @@ -9,7 +9,7 @@ pub mod stats; pub mod unit_formatter; fn table_status_select_current(app: &mut App) { - let activities = app.filtered_activities(); + let activities = app.activities(); if let Some(selected) = app.activity_list.table_state().selected() { if let Some(a) = activities.get(selected) { app.activity = Some(a.clone()); diff --git a/src/event/keymap.rs b/src/event/keymap.rs index f7a7a0e..35d419b 100644 --- a/src/event/keymap.rs +++ b/src/event/keymap.rs @@ -8,6 +8,7 @@ pub fn map_key(ke: KeyEvent) -> MappedKey { KeyCode::Char('o') => new_strava_key(ke, StravaEvent::ToggleSortOrder), KeyCode::Char('u') => new_strava_key(ke, StravaEvent::ToggleUnitSystem), KeyCode::Char('s') => new_strava_key(ke, StravaEvent::Sort), + KeyCode::Char('S') => new_strava_key(ke, StravaEvent::Rank), KeyCode::Char('f') => new_strava_key(ke, StravaEvent::Filter), KeyCode::Char('r') => new_strava_key(ke, StravaEvent::Refresh), KeyCode::Char('a') => new_strava_key(ke, StravaEvent::Anchor), @@ -34,6 +35,7 @@ pub struct MappedKey { } pub enum StravaEvent { + Rank, ToggleUnitSystem, ToggleSortOrder, Refresh, diff --git a/src/store/activity.rs b/src/store/activity.rs index 6832a3e..3df14fb 100644 --- a/src/store/activity.rs +++ b/src/store/activity.rs @@ -1,14 +1,80 @@ -use std::cmp::Ordering; +use std::{cmp::Ordering, fmt::Display}; use chrono::NaiveDateTime; +use crossterm::event::KeyCode; use geo_types::LineString; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool}; - -use crate::app::{SortBy, SortOrder}; +use strum::EnumIter; use super::polyline_compare::compare; +#[derive(EnumIter)] +pub enum SortBy { + Date, + Distance, + Pace, + HeartRate, + Time, +} + +impl Display for SortBy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_label()) + } +} + +impl SortBy { + pub fn to_key(&self) -> char { + match *self { + SortBy::Date => 'd', + SortBy::Pace => 'p', + SortBy::HeartRate => 'h', + SortBy::Distance => 'D', + SortBy::Time => 't', + } + } + + pub fn to_label(&self) -> &str { + match *self { + SortBy::Date => "date", + SortBy::Pace => "pace", + SortBy::HeartRate => "heartrate", + SortBy::Distance => "distance", + SortBy::Time => "time", + } + } + + pub fn from_key(key: KeyCode) -> Option { + match key { + KeyCode::Char('d') => Some(SortBy::Date), + KeyCode::Char('p') => Some(SortBy::Pace), + KeyCode::Char('h') => Some(SortBy::HeartRate), + KeyCode::Char('D') => Some(SortBy::Distance), + KeyCode::Char('t') => Some(SortBy::Time), + _ => None, + } + } +} + +pub enum SortOrder { + Asc, + Desc, +} + +impl Display for SortOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + SortOrder::Asc => "asc", + SortOrder::Desc => "desc", + } + ) + } +} + #[derive(Clone, Debug)] pub struct Activities { activities: Vec, @@ -30,7 +96,7 @@ impl Iterator for Activities { } impl Activities { - pub(crate) fn new() -> Activities { + pub fn new() -> Activities { Self { activities: vec![], offset: 0, @@ -47,21 +113,21 @@ impl Activities { self.activities.iter().find(|a|a.id == id) } - pub(crate) fn where_title_contains(&self, pattern: &str) -> Activities { + pub fn where_title_contains(&self, pattern: &str) -> Activities { self.activities.clone() .into_iter() .filter(|a| a.title.contains(pattern)) .collect() } - pub(crate) fn having_activity_type(&self, activity_type: String) -> Activities { + pub fn having_activity_type(&self, activity_type: String) -> Activities { self.activities.clone() .into_iter() .filter(|a| a.activity_type == activity_type) .collect() } - pub(crate) fn withing_distance_of(&self, anchored: &Activity, tolerant: f64) -> Activities { + pub fn withing_distance_of(&self, anchored: &Activity, tolerant: f64) -> Activities { self.activities.clone() .into_iter() .filter(|a| { @@ -73,7 +139,7 @@ impl Activities { .collect() } - pub(crate) fn sort( + pub fn sort( &self, sort_by: &SortBy, sort_order: &SortOrder, @@ -102,6 +168,20 @@ impl Activities { Activities::from(activities) } + pub fn rank(&self, rank_by: &SortBy, rank_order: &SortOrder) -> Activities { + let sorted = self.sort(rank_by, rank_order); + let s = sorted.to_vec(); + let mut rank = 0; + + let s = s.iter().cloned().map(|a| { + let mut aa= a; + rank+=1; + aa.rank = rank; + aa + }); + Activities::from(s.collect::>()) + } + pub fn is_empty(&self) -> bool { self.activities.is_empty() } @@ -174,6 +254,7 @@ pub struct Activity { pub location_city: Option, pub athletes: i64, pub splits: Vec, + pub rank: i64, } #[derive(Serialize, Deserialize, Debug, Clone, FromRow)] @@ -197,11 +278,11 @@ pub struct ActivityStore<'a> { } impl ActivityStore<'_> { - pub(crate) fn new(pool: &SqlitePool) -> ActivityStore<'_> { + pub fn new(pool: &SqlitePool) -> ActivityStore<'_> { ActivityStore { pool } } - pub(crate) async fn activities(&mut self) -> Activities { + pub async fn activities(&mut self) -> Activities { let activities = sqlx::query!( r#" SELECT * FROM activity ORDER BY start_date DESC @@ -241,6 +322,7 @@ impl ActivityStore<'_> { location_city: rec.location_city.clone(), athletes: rec.athletes, splits, + rank: 0, } }) .collect(); @@ -262,7 +344,7 @@ impl Activity { self.distance / (self.moving_time as f64 / 3600.0) } - pub(crate) fn activity_type_icon(&self) -> String { + pub fn activity_type_icon(&self) -> String { match self.activity_type.as_str() { "Ride" => "🚴".to_string(), "Run" => "🏃".to_string(), @@ -272,7 +354,7 @@ impl Activity { } } - pub(crate) fn polyline(&self) -> Result { + pub fn polyline(&self) -> Result { if let Some(p) = &self.summary_polyline { return polyline::decode_polyline(p.as_str(), 5); } diff --git a/src/sync/convert.rs b/src/sync/convert.rs index fc48509..6cd3935 100644 --- a/src/sync/convert.rs +++ b/src/sync/convert.rs @@ -63,7 +63,8 @@ impl ActivityConverter<'_> { location_state: listed.location_state.clone(), location_city: listed.location_city.clone(), athletes: listed.athlete_count, - splits: vec![] + splits: vec![], + rank: 0, }; sqlx::query!( diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bc03ae8..8b29538 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -59,6 +59,8 @@ fn header<'a>(_app: &'a mut App) -> Paragraph<'a> { Span::raw("ilter "), Span::styled("[s]", Style::default().fg(strava)), Span::raw("ort "), + Span::styled("[S]", Style::default().fg(strava)), + Span::raw("rank "), Span::styled("[o]", Style::default().fg(strava)), Span::raw("rder "), Span::styled("[r]", Style::default().fg(strava)), @@ -91,11 +93,15 @@ fn status_bar<'a>(app: &'a mut App) -> Paragraph<'a> { if app.filters.filter != *"" { status.push(format!("filtered by \"{}\"", app.filters.filter)) } - status.push(format!("{} activities", app.filtered_activities().len())); + status.push(format!("{} activities", app.activities().len())); status.push(format!( "sorted by {} {}", app.filters.sort_by, app.filters.sort_order )); + status.push(format!( + "ranked by {} {}", + app.ranking.rank_by, app.ranking.rank_order + )); status.push(format!("{} units", app.unit_formatter.system)); if let Some(anchored) = &app.activity_anchored { status.push(format!("anchored to \"{}\" ± {:.3}", anchored.title, app.filters.anchor_tolerance));