Skip to content

Commit 218d185

Browse files
authored
Merge pull request #31 from dantleech/ranking
Add Support for Ranking
2 parents 3c31c64 + 92c74a8 commit 218d185

File tree

12 files changed

+250
-140
lines changed

12 files changed

+250
-140
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ CHANGELOG
33

44
## master
55

6+
Features:
7+
68
- Highlight and allow selection of current split in activity view.
9+
- Added activity ranking.

src/app.rs

Lines changed: 26 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ use std::{
44
time::{Duration, SystemTime},
55
};
66

7-
use strum::EnumIter;
8-
97
use tokio::sync::mpsc::{Receiver, Sender};
108
use tui::{
119
backend::{Backend, CrosstermBackend},
@@ -18,7 +16,7 @@ use crate::{
1816
component::activity_list::{ActivityListMode, ActivityListState, ActivityViewState},
1917
event::{input::EventSender, util::{table_state_prev, table_state_next}},
2018
input::InputEvent,
21-
store::{activity::{ActivityStore, Activities}},
19+
store::{activity::{ActivityStore, Activities, SortBy, SortOrder}},
2220
};
2321
use crate::{
2422
component::{activity_list, activity_view, unit_formatter::UnitFormatter},
@@ -34,6 +32,11 @@ pub struct ActivityFilters {
3432
pub filter: String,
3533
}
3634

35+
pub struct RankOptions {
36+
pub rank_by: SortBy,
37+
pub rank_order: SortOrder,
38+
}
39+
3740
impl ActivityFilters {
3841
pub fn anchor_tolerance_add(&mut self, delta: f64) {
3942
self.anchor_tolerance += delta;
@@ -73,12 +76,12 @@ pub struct App<'a> {
7376
pub activity_list: ActivityListState,
7477
pub activity_view: ActivityViewState,
7578
pub filters: ActivityFilters,
79+
pub ranking: RankOptions,
7680

7781
pub activity_type: Option<String>,
7882
pub activity: Option<Activity>,
7983
pub activity_anchored: Option<Activity>,
8084
pub activities: Activities,
81-
pub activities_filtered: Activities,
8285

8386
pub info_message: Option<Notification>,
8487
pub error_message: Option<Notification>,
@@ -96,39 +99,6 @@ pub enum ActivePage {
9699
Activity,
97100
}
98101

99-
#[derive(EnumIter)]
100-
pub enum SortBy {
101-
Date,
102-
Distance,
103-
Pace,
104-
HeartRate,
105-
Time,
106-
}
107-
108-
impl Display for SortBy {
109-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110-
write!(f, "{}", self.to_label())
111-
}
112-
}
113-
114-
pub enum SortOrder {
115-
Asc,
116-
Desc,
117-
}
118-
119-
impl Display for SortOrder {
120-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121-
write!(
122-
f,
123-
"{}",
124-
match self {
125-
SortOrder::Asc => "ascending",
126-
SortOrder::Desc => "descending",
127-
}
128-
)
129-
}
130-
}
131-
132102
impl App<'_> {
133103
pub fn new<'a>(
134104
store: &'a mut ActivityStore<'a>,
@@ -147,6 +117,7 @@ impl App<'_> {
147117
filter_text_area: Input::default(),
148118
filter_dialog: false,
149119
sort_dialog: false,
120+
rank_dialog: false,
150121
},
151122
activity_view: ActivityViewState {
152123
pace_table_state: TableState::default(),
@@ -158,10 +129,13 @@ impl App<'_> {
158129
filter: "".to_string(),
159130
anchor_tolerance: 0.005,
160131
},
132+
ranking: RankOptions {
133+
rank_by: SortBy::Pace,
134+
rank_order: SortOrder::Desc,
135+
},
161136
activity: None,
162137
activity_anchored: None,
163138
activities: Activities::new(),
164-
activities_filtered: Activities::new(),
165139
store,
166140

167141
activity_type: None,
@@ -177,8 +151,6 @@ impl App<'_> {
177151
&mut self,
178152
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
179153
) -> Result<(), anyhow::Error> {
180-
self.activities = self.store.activities().await;
181-
182154
loop {
183155
if self.quit {
184156
break;
@@ -226,23 +198,21 @@ impl App<'_> {
226198
}
227199

228200
pub async fn reload(&mut self) {
229-
self.activities = self.store.activities().await;
230-
self.activities_filtered = self.activities.where_title_contains(self.filters.filter.as_str());
201+
let mut activities = self.store.activities().await;
202+
activities = activities.where_title_contains(self.filters.filter.as_str());
231203
if let Some(activity_type) = self.activity_type.clone() {
232-
self.activities_filtered = self.activities_filtered.having_activity_type(activity_type);
204+
activities = activities.having_activity_type(activity_type);
233205
}
234206
if let Some(anchored) = &self.activity_anchored {
235-
self.activities_filtered = self.activities_filtered.withing_distance_of(anchored, self.filters.anchor_tolerance);
207+
activities = activities.withing_distance_of(anchored, self.filters.anchor_tolerance);
236208
}
209+
self.activities = activities
210+
.rank(&self.ranking.rank_by, &self.ranking.rank_order)
211+
.sort(&self.filters.sort_by, &self.filters.sort_order)
237212
}
238213

239-
pub fn unsorted_filtered_activities(&self) -> Activities {
240-
self.activities_filtered.clone()
241-
}
242-
243-
pub fn filtered_activities(&self) -> Activities {
244-
let activities = self.unsorted_filtered_activities();
245-
activities.sort(&self.filters.sort_by, &self.filters.sort_order)
214+
pub fn activities(&self) -> Activities {
215+
self.activities.clone()
246216
}
247217

248218
fn draw<B: Backend>(&mut self, f: &mut Frame<B>) -> Result<(), anyhow::Error> {
@@ -261,7 +231,7 @@ impl App<'_> {
261231
}
262232

263233
pub(crate) fn anchor_selected(&mut self) {
264-
let activities = self.filtered_activities();
234+
let activities = self.activities();
265235
if let Some(selected) = self.activity_list.table_state().selected() {
266236
if let Some(a) = activities.get(selected) {
267237
if self.activity_anchored.is_some() {
@@ -280,11 +250,11 @@ impl App<'_> {
280250
pub(crate) fn previous_activity(&mut self) {
281251
table_state_prev(
282252
self.activity_list.table_state(),
283-
self.activities_filtered.len(),
253+
self.activities.len(),
284254
false,
285255
);
286256
if let Some(selected) = self.activity_list.table_state().selected() {
287-
if let Some(a) = self.activities_filtered.get(selected) {
257+
if let Some(a) = self.activities.get(selected) {
288258
self.activity = Some(a.clone());
289259
}
290260
}
@@ -293,11 +263,11 @@ impl App<'_> {
293263
pub(crate) fn next_activity(&mut self) {
294264
table_state_next(
295265
self.activity_list.table_state(),
296-
self.activities_filtered.len(),
266+
self.activities.len(),
297267
false,
298268
);
299269
if let Some(selected) = self.activity_list.table_state().selected() {
300-
if let Some(a) = self.activities_filtered.get(selected) {
270+
if let Some(a) = self.activities.get(selected) {
301271
self.activity = Some(a.clone());
302272
}
303273
}

src/component/activity_list/chart.rs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use chrono::{NaiveDateTime};
1+
use chrono::NaiveDateTime;
22
use tui::{
33
backend::Backend,
44
layout::Constraint,
@@ -10,21 +10,20 @@ use tui::{
1010
};
1111

1212
use crate::{
13-
app::{App},
14-
event::{
15-
keymap::{MappedKey},
16-
},
13+
app::App,
14+
event::keymap::MappedKey,
15+
store::activity::{SortBy, SortOrder},
1716
};
1817

19-
20-
2118
pub fn handle(_app: &mut App, _key: MappedKey) {}
2219
pub fn draw<B: Backend>(
2320
app: &mut App,
2421
f: &mut Frame<B>,
2522
area: tui::layout::Rect,
2623
) -> Result<(), anyhow::Error> {
27-
let activities = &app.unsorted_filtered_activities();
24+
let activities = &app
25+
.activities()
26+
.sort(&SortBy::Date, &SortOrder::Asc);
2827
let times: Vec<i64> = activities.timestamps();
2928
let paces: Vec<i64> = activities.meter_per_hours();
3029
let tmax = times.iter().max();
@@ -41,7 +40,8 @@ pub fn draw<B: Backend>(
4140
}
4241
let pmin = pmin.unwrap();
4342
let pmax = pmax.unwrap();
44-
let data: Vec<(f64, f64)> = activities.to_vec()
43+
let data: Vec<(f64, f64)> = activities
44+
.to_vec()
4545
.iter()
4646
.map(|a| {
4747
let ts = a.start_date.unwrap().timestamp();
@@ -50,11 +50,11 @@ pub fn draw<B: Backend>(
5050
.collect();
5151
let mut current = vec![];
5252
if let Some(selected) = app.activity_list.table_state().selected() {
53-
let activities = app.filtered_activities();
53+
let activities = app.activities();
5454
if let Some(a) = activities.get(selected) {
55-
if let Some(a) = app.activities.find(a.id) {
56-
current.push((a.start_date.unwrap().timestamp() as f64, *pmin as f64));
57-
current.push((a.start_date.unwrap().timestamp() as f64, *pmax as f64));
55+
if let Some(a) = activities.find(a.id) {
56+
current.push((a.start_date.unwrap().timestamp() as f64, *pmin as f64));
57+
current.push((a.start_date.unwrap().timestamp() as f64, *pmax as f64));
5858
}
5959
}
6060
}
@@ -71,17 +71,17 @@ pub fn draw<B: Backend>(
7171
.marker(Marker::Braille)
7272
.graph_type(GraphType::Scatter)
7373
.style(Style::default().fg(Color::Magenta)),
74-
Dataset::default().data(&current)
74+
Dataset::default()
75+
.data(&current)
7576
.name("Selected")
7677
.marker(Marker::Braille)
7778
.graph_type(GraphType::Line)
7879
.style(Style::default().fg(Color::Green)),
7980
];
8081
let yaxisstep = (pdiff as f64 / area.height as f64) as usize;
81-
let yaxis =
82-
(*pmin..*pmax).step_by(if yaxisstep > 0 { yaxisstep } else { 1 });
82+
let yaxis = (*pmin..*pmax).step_by(if yaxisstep > 0 { yaxisstep } else { 1 });
8383
let xaxisstep = (tdiff as f64 / 5.0) as usize;
84-
let xaxis = (*tmin.unwrap()..*tmax.unwrap()).step_by(if xaxisstep > 0 { xaxisstep } else {1});
84+
let xaxis = (*tmin.unwrap()..*tmax.unwrap()).step_by(if xaxisstep > 0 { xaxisstep } else { 1 });
8585
let chart = Chart::new(datasets)
8686
.hidden_legend_constraints((Constraint::Max(1), Constraint::Max(1)))
8787
.block(Block::default().borders(Borders::all()))
@@ -105,7 +105,10 @@ pub fn draw<B: Backend>(
105105
Axis::default()
106106
.title(Span::styled("Pace", Style::default().fg(Color::Red)))
107107
.style(Style::default().fg(Color::White))
108-
.bounds([*pmin as f64, *pmax as f64 + (pdiff as f64 / activities.len() as f64)])
108+
.bounds([
109+
*pmin as f64,
110+
*pmax as f64 + (pdiff as f64 / activities.len() as f64),
111+
])
109112
.labels(
110113
yaxis
111114
.map(|p| Span::from(app.unit_formatter.pace(3600, p as f64)))

src/component/activity_list/list.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ use tui::{
1010
use tui_input::backend::crossterm::EventHandler;
1111

1212
use crate::{
13-
app::{App, SortOrder},
13+
app::App,
1414
event::{
1515
keymap::{MappedKey, StravaEvent}, input::InputEvent,
1616
},
17-
store::activity::{Activities},
17+
store::activity::{Activities, SortOrder},
1818
ui::{centered_rect_absolute, color::ColorTheme}, component::{table_status_select_current},
1919
};
2020

21-
use super::sort_dialog;
21+
use super::{sort_dialog, rank_dialog};
2222

2323
pub fn handle(app: &mut App, key: MappedKey) {
2424
if app.activity_list.filter_dialog {
@@ -46,6 +46,11 @@ pub fn handle(app: &mut App, key: MappedKey) {
4646

4747
return;
4848
}
49+
if app.activity_list.rank_dialog {
50+
rank_dialog::handle(app, key);
51+
52+
return;
53+
}
4954
match key.strava_event {
5055
StravaEvent::Quit => app.quit = true,
5156
StravaEvent::ToggleUnitSystem => {
@@ -55,12 +60,14 @@ pub fn handle(app: &mut App, key: MappedKey) {
5560
app.filters.sort_order = match app.filters.sort_order {
5661
SortOrder::Asc => SortOrder::Desc,
5762
SortOrder::Desc => SortOrder::Asc,
58-
}
63+
};
64+
app.send(InputEvent::Reload);
5965
}
6066
StravaEvent::Down => app.next_activity(),
6167
StravaEvent::Up => app.previous_activity(),
6268
StravaEvent::Filter => toggle_filter(app),
6369
StravaEvent::Sort => toggle_sort(app),
70+
StravaEvent::Rank => toggle_rank(app),
6471
StravaEvent::Enter => table_status_select_current(app),
6572
StravaEvent::Refresh => app.send(InputEvent::Sync),
6673
StravaEvent::IncreaseTolerance => {
@@ -86,13 +93,16 @@ fn toggle_filter(app: &mut App) {
8693
fn toggle_sort(app: &mut App) {
8794
app.activity_list.sort_dialog = !app.activity_list.sort_dialog;
8895
}
96+
fn toggle_rank(app: &mut App) {
97+
app.activity_list.rank_dialog = !app.activity_list.rank_dialog;
98+
}
8999

90100
pub fn draw<B: Backend>(
91101
app: &mut App,
92102
f: &mut Frame<B>,
93103
area: tui::layout::Rect,
94104
) -> Result<(), anyhow::Error> {
95-
let activities = &app.filtered_activities();
105+
let activities = &app.activities();
96106

97107
if app.activity_list.table_state().selected().is_none() && !activities.is_empty() {
98108
app.activity_list.table_state().select(Some(0));
@@ -129,6 +139,11 @@ pub fn draw<B: Backend>(
129139

130140
return Ok(());
131141
}
142+
if app.activity_list.rank_dialog {
143+
rank_dialog::draw(app, f, f.size())?;
144+
145+
return Ok(());
146+
}
132147

133148
Ok(())
134149
}
@@ -142,8 +157,9 @@ pub fn activity_list_table<'a>(app: &App, activities: &'a Activities) -> Table<'
142157
"Dst",
143158
"🕑 Time",
144159
"👣 Pace",
145-
"💓 Heart",
160+
"💓 Avg. Heart",
146161
"🌄 Elevation",
162+
"🪜 Rank",
147163
];
148164
let headers = header_names
149165
.iter()
@@ -169,6 +185,7 @@ pub fn activity_list_table<'a>(app: &App, activities: &'a Activities) -> Table<'
169185
.map_or_else(|| "n/a".to_string(), |v| format!("{:.2}", v)),
170186
),
171187
Cell::from(app.unit_formatter.elevation(activity.total_elevation_gain)),
188+
Cell::from(format!("{}", activity.rank)),
172189
]));
173190
}
174191

@@ -190,6 +207,7 @@ pub fn activity_list_table<'a>(app: &App, activities: &'a Activities) -> Table<'
190207
Constraint::Percentage(10),
191208
Constraint::Percentage(10),
192209
Constraint::Percentage(10),
210+
Constraint::Percentage(10),
193211
])
194212
}
195213

0 commit comments

Comments
 (0)