10350 lines
223 KiB
C
10350 lines
223 KiB
C
/**
|
||
* @file tui.c
|
||
* @brief First usable Trainlog ncurses interface.
|
||
*/
|
||
|
||
#include "trainlog/tui.h"
|
||
|
||
#include <ctype.h>
|
||
#include <fcntl.h>
|
||
#include <locale.h>
|
||
#include <limits.h>
|
||
#include <stdbool.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <time.h>
|
||
#include <sys/wait.h>
|
||
#include <wchar.h>
|
||
#include <unistd.h>
|
||
|
||
#include <curses.h>
|
||
|
||
#include "trainlog/bodyviz.h"
|
||
#include "trainlog/catalog.h"
|
||
#include "trainlog/duration.h"
|
||
#include "trainlog/id.h"
|
||
#include "trainlog/measured_max.h"
|
||
#include "trainlog/mtp.h"
|
||
#include "trainlog/sync.h"
|
||
#include "trainlog/reps.h"
|
||
#include "trainlog/theme.h"
|
||
#include "trainlog/timeutil.h"
|
||
#include "trainlog/usb.h"
|
||
|
||
#define MAX_EXERCISES 128U
|
||
#define MAX_SESSION_EXERCISES 32U
|
||
#define MAX_SETS_PER_EXERCISE 64U
|
||
#define MAX_SESSIONS 128U
|
||
#define MAX_WEIGHT_POINTS 256U
|
||
|
||
#define MAX_BODY_METRIC_POINTS 256U
|
||
|
||
|
||
/* TRAINLOG_TUI_V02_POLISH */
|
||
/* TRAINLOG_TUI_PROFILED_EXERCISE_CREATION */
|
||
/* TRAINLOG_VARIABLE_SET_REPS_V1 */
|
||
/* TRAINLOG_SYNC_RESPONSIVE_CACHE */
|
||
/* TRAINLOG_SYNC_LARGE_LAYOUT_S_FIX */
|
||
/* TRAINLOG_SYNC_HISTORY_BIDIRECTIONAL_V1 */
|
||
/* TRAINLOG_SYNC_PC_TO_ANDROID_DIAGNOSTICS */
|
||
/* TRAINLOG_SYNC_FULL_MTP_SILENCE */
|
||
|
||
typedef enum DashboardAction {
|
||
DASHBOARD_NEW_SESSION = 0,
|
||
DASHBOARD_HISTORY,
|
||
DASHBOARD_EXERCISES,
|
||
DASHBOARD_BODY,
|
||
DASHBOARD_SYNC,
|
||
DASHBOARD_QUIT
|
||
} DashboardAction;
|
||
|
||
/* TRAINLOG_DASHBOARD_FORWARD_DECLARATIONS */
|
||
static DashboardAction screen_dashboard(
|
||
TrainlogDatabase *database
|
||
);
|
||
|
||
static void screen_exercises(
|
||
TrainlogDatabase *database
|
||
);
|
||
|
||
static void screen_sync(
|
||
TrainlogDatabase *database
|
||
);
|
||
|
||
static void dashboard_panel(
|
||
int top,
|
||
int left,
|
||
int bottom,
|
||
int right,
|
||
const char *label
|
||
);
|
||
|
||
static void dashboard_ascii_header(void);
|
||
|
||
static void draw_dashboard_body_graph(
|
||
TrainlogDatabase *database
|
||
);
|
||
|
||
static void focused_panel(
|
||
int top,
|
||
int left,
|
||
int bottom,
|
||
int right,
|
||
const char *label,
|
||
bool active
|
||
);
|
||
|
||
static void primary_top_navbar(
|
||
int active_page,
|
||
int selected_page,
|
||
bool focused
|
||
);
|
||
|
||
static bool primary_top_nav_forward(int key);
|
||
static bool primary_top_nav_activate(int selected_page);
|
||
|
||
static void section_ascii_header(const char *subtitle);
|
||
|
||
|
||
static void session_history_datetime(
|
||
const char *timestamp,
|
||
char output[17]
|
||
);
|
||
|
||
|
||
static void draw_shell(const char *heading, const char *footer)
|
||
{
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
attron(A_BOLD | trainlog_theme_attribute(TRAINLOG_COLOR_ACCENT));
|
||
mvprintw(1, 2, " %s ", heading);
|
||
attroff(A_BOLD | trainlog_theme_attribute(TRAINLOG_COLOR_ACCENT));
|
||
|
||
attron(trainlog_theme_attribute(TRAINLOG_COLOR_MUTED));
|
||
mvprintw(LINES - 2, 2, "%-*s", COLS - 4, footer);
|
||
attroff(trainlog_theme_attribute(TRAINLOG_COLOR_MUTED));
|
||
}
|
||
|
||
static void wait_key(void)
|
||
{
|
||
attron(trainlog_theme_attribute(TRAINLOG_COLOR_MUTED));
|
||
mvprintw(LINES - 2, 2, "Appuyez sur une touche pour continuer...");
|
||
attroff(trainlog_theme_attribute(TRAINLOG_COLOR_MUTED));
|
||
refresh();
|
||
(void)getch();
|
||
}
|
||
|
||
static void status_line(const char *text, TrainlogColorRole role)
|
||
{
|
||
attron(trainlog_theme_attribute(role));
|
||
mvprintw(LINES - 3, 2, "%-*s", COLS - 4, text);
|
||
attroff(trainlog_theme_attribute(role));
|
||
}
|
||
|
||
static bool prompt_text(
|
||
int row,
|
||
const char *label,
|
||
char *output,
|
||
size_t output_size,
|
||
bool allow_empty
|
||
)
|
||
{
|
||
size_t used = 0U;
|
||
int input_column;
|
||
int cursor_row;
|
||
bool accepted = false;
|
||
|
||
if (label == NULL ||
|
||
output == NULL ||
|
||
output_size < 2U) {
|
||
return false;
|
||
}
|
||
|
||
output[0] = '\0';
|
||
|
||
mvprintw(
|
||
row,
|
||
2,
|
||
"%s",
|
||
label
|
||
);
|
||
|
||
getyx(
|
||
stdscr,
|
||
cursor_row,
|
||
input_column
|
||
);
|
||
|
||
(void)cursor_row;
|
||
|
||
noecho();
|
||
(void)curs_set(1);
|
||
|
||
for (;;) {
|
||
wint_t value;
|
||
int rc;
|
||
|
||
move(
|
||
row,
|
||
input_column
|
||
);
|
||
|
||
clrtoeol();
|
||
|
||
if (used > 0U) {
|
||
addnstr(
|
||
output,
|
||
(int)used
|
||
);
|
||
}
|
||
|
||
refresh();
|
||
|
||
rc = get_wch(&value);
|
||
|
||
if (rc == ERR) {
|
||
break;
|
||
}
|
||
|
||
if (rc == KEY_CODE_YES) {
|
||
int key =
|
||
(int)value;
|
||
|
||
if (key == KEY_ENTER) {
|
||
if (allow_empty ||
|
||
used > 0U) {
|
||
accepted = true;
|
||
break;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_BACKSPACE ||
|
||
key == KEY_DC) {
|
||
if (used > 0U) {
|
||
do {
|
||
--used;
|
||
} while (
|
||
used > 0U &&
|
||
(((unsigned char)output[used] &
|
||
0xc0U) == 0x80U)
|
||
);
|
||
|
||
output[used] = '\0';
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (value == (wint_t)27) {
|
||
accepted = false;
|
||
break;
|
||
}
|
||
|
||
if (value == (wint_t)'\n' ||
|
||
value == (wint_t)'\r') {
|
||
if (allow_empty ||
|
||
used > 0U) {
|
||
accepted = true;
|
||
break;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (value == (wint_t)8 ||
|
||
value == (wint_t)127) {
|
||
if (used > 0U) {
|
||
do {
|
||
--used;
|
||
} while (
|
||
used > 0U &&
|
||
(((unsigned char)output[used] &
|
||
0xc0U) == 0x80U)
|
||
);
|
||
|
||
output[used] = '\0';
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (value >= (wint_t)32) {
|
||
char encoded[MB_LEN_MAX];
|
||
mbstate_t state;
|
||
size_t encoded_size;
|
||
|
||
(void)memset(
|
||
&state,
|
||
0,
|
||
sizeof(state)
|
||
);
|
||
|
||
encoded_size =
|
||
wcrtomb(
|
||
encoded,
|
||
(wchar_t)value,
|
||
&state
|
||
);
|
||
|
||
if (encoded_size ==
|
||
(size_t)-1 ||
|
||
encoded_size == 0U ||
|
||
encoded_size >
|
||
output_size - used - 1U) {
|
||
continue;
|
||
}
|
||
|
||
(void)memcpy(
|
||
output + used,
|
||
encoded,
|
||
encoded_size
|
||
);
|
||
|
||
used += encoded_size;
|
||
output[used] = '\0';
|
||
}
|
||
}
|
||
|
||
noecho();
|
||
(void)curs_set(0);
|
||
|
||
return accepted;
|
||
}
|
||
|
||
static bool parse_int(const char *text, int minimum, int maximum, int *output)
|
||
{
|
||
char *end = NULL;
|
||
long value;
|
||
|
||
if (text == NULL || output == NULL || text[0] == '\0') {
|
||
return false;
|
||
}
|
||
|
||
value = strtol(text, &end, 10);
|
||
if (end == text || *end != '\0' ||
|
||
value < (long)minimum || value > (long)maximum) {
|
||
return false;
|
||
}
|
||
|
||
*output = (int)value;
|
||
return true;
|
||
}
|
||
|
||
static bool parse_double_positive(const char *text, double *output)
|
||
{
|
||
char *end = NULL;
|
||
double value;
|
||
|
||
if (text == NULL || output == NULL || text[0] == '\0') {
|
||
return false;
|
||
}
|
||
|
||
value = strtod(text, &end);
|
||
if (end == text || *end != '\0' || value <= 0.0) {
|
||
return false;
|
||
}
|
||
|
||
*output = value;
|
||
return true;
|
||
}
|
||
|
||
static bool prompt_int_value(
|
||
int row,
|
||
const char *label,
|
||
int minimum,
|
||
int maximum,
|
||
int default_value,
|
||
int *output
|
||
)
|
||
{
|
||
char buffer[64];
|
||
|
||
for (;;) {
|
||
char decorated[128];
|
||
|
||
(void)snprintf(
|
||
decorated,
|
||
sizeof(decorated),
|
||
"%s [%d]: ",
|
||
label,
|
||
default_value
|
||
);
|
||
|
||
if (!prompt_text(row, decorated, buffer, sizeof(buffer), true)) {
|
||
return false;
|
||
}
|
||
|
||
if (buffer[0] == '\0') {
|
||
*output = default_value;
|
||
return true;
|
||
}
|
||
|
||
if (parse_int(buffer, minimum, maximum, output)) {
|
||
return true;
|
||
}
|
||
|
||
status_line("Valeur entière invalide.", TRAINLOG_COLOR_ERROR);
|
||
refresh();
|
||
}
|
||
}
|
||
|
||
static bool prompt_optional_double(
|
||
int row,
|
||
const char *label,
|
||
bool *present,
|
||
double *output
|
||
)
|
||
{
|
||
char buffer[64];
|
||
|
||
for (;;) {
|
||
if (!prompt_text(row, label, buffer, sizeof(buffer), true)) {
|
||
return false;
|
||
}
|
||
|
||
if (buffer[0] == '\0') {
|
||
*present = false;
|
||
*output = 0.0;
|
||
return true;
|
||
}
|
||
|
||
if (parse_double_positive(buffer, output)) {
|
||
*present = true;
|
||
return true;
|
||
}
|
||
|
||
status_line("Nombre positif invalide.", TRAINLOG_COLOR_ERROR);
|
||
refresh();
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_EXERCISE_PERFORMANCE_TUI */
|
||
|
||
#define EXERCISE_GRAPH_POINTS 12U
|
||
|
||
static const char *exercise_load_mode_label(
|
||
TrainlogLoadMode mode
|
||
)
|
||
{
|
||
switch (mode) {
|
||
case TRAINLOG_LOAD_EXTERNAL:
|
||
return "charge externe";
|
||
|
||
case TRAINLOG_LOAD_ASSISTANCE:
|
||
return "assistance";
|
||
|
||
case TRAINLOG_LOAD_NONE:
|
||
default:
|
||
return "sans charge";
|
||
}
|
||
}
|
||
|
||
static void exercise_short_date(
|
||
const char *timestamp,
|
||
char output[11]
|
||
)
|
||
{
|
||
if (timestamp == NULL ||
|
||
strlen(timestamp) < 10U) {
|
||
(void)snprintf(
|
||
output,
|
||
11U,
|
||
"%s",
|
||
"----------"
|
||
);
|
||
return;
|
||
}
|
||
|
||
(void)memcpy(
|
||
output,
|
||
timestamp,
|
||
10U
|
||
);
|
||
|
||
output[10] = '\0';
|
||
}
|
||
|
||
static void exercise_format_performance(
|
||
const TrainlogExercisePerformancePoint *point,
|
||
char *output,
|
||
size_t output_size
|
||
)
|
||
{
|
||
if (point == NULL ||
|
||
output == NULL ||
|
||
output_size == 0U) {
|
||
return;
|
||
}
|
||
|
||
if (point->has_performance == 0) {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%s",
|
||
"aucune série réussie"
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (point->tracking_mode ==
|
||
TRAINLOG_TRACKING_DURATION) {
|
||
char duration[64];
|
||
|
||
if (trainlog_duration_format(
|
||
point->metric_value,
|
||
duration,
|
||
sizeof(duration)
|
||
) != TRAINLOG_STATUS_OK) {
|
||
(void)snprintf(
|
||
duration,
|
||
sizeof(duration),
|
||
"%d s",
|
||
point->metric_value
|
||
);
|
||
}
|
||
|
||
if (point->load_mode ==
|
||
TRAINLOG_LOAD_EXTERNAL) {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%.1f kg × %s",
|
||
point->weight_kg,
|
||
duration
|
||
);
|
||
} else if (
|
||
point->load_mode ==
|
||
TRAINLOG_LOAD_ASSISTANCE
|
||
) {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%.1f kg aide × %s",
|
||
point->weight_kg,
|
||
duration
|
||
);
|
||
} else {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%s",
|
||
duration
|
||
);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (point->load_mode ==
|
||
TRAINLOG_LOAD_EXTERNAL) {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%.1f kg × %d reps",
|
||
point->weight_kg,
|
||
point->metric_value
|
||
);
|
||
} else if (
|
||
point->load_mode ==
|
||
TRAINLOG_LOAD_ASSISTANCE
|
||
) {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%.1f kg aide × %d reps",
|
||
point->weight_kg,
|
||
point->metric_value
|
||
);
|
||
} else {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%d reps",
|
||
point->metric_value
|
||
);
|
||
}
|
||
}
|
||
|
||
static bool exercise_point_better(
|
||
const TrainlogExercisePerformancePoint *candidate,
|
||
const TrainlogExercisePerformancePoint *current
|
||
)
|
||
{
|
||
if (candidate == NULL ||
|
||
candidate->has_performance == 0) {
|
||
return false;
|
||
}
|
||
|
||
if (current == NULL ||
|
||
current->has_performance == 0) {
|
||
return true;
|
||
}
|
||
|
||
if (candidate->load_mode !=
|
||
current->load_mode) {
|
||
return false;
|
||
}
|
||
|
||
switch (candidate->load_mode) {
|
||
case TRAINLOG_LOAD_EXTERNAL:
|
||
if (candidate->weight_kg >
|
||
current->weight_kg) {
|
||
return true;
|
||
}
|
||
|
||
return
|
||
candidate->weight_kg ==
|
||
current->weight_kg &&
|
||
candidate->metric_value >
|
||
current->metric_value;
|
||
|
||
case TRAINLOG_LOAD_ASSISTANCE:
|
||
if (candidate->weight_kg <
|
||
current->weight_kg) {
|
||
return true;
|
||
}
|
||
|
||
return
|
||
candidate->weight_kg ==
|
||
current->weight_kg &&
|
||
candidate->metric_value >
|
||
current->metric_value;
|
||
|
||
case TRAINLOG_LOAD_NONE:
|
||
default:
|
||
return
|
||
candidate->metric_value >
|
||
current->metric_value;
|
||
}
|
||
}
|
||
|
||
static double exercise_graph_value(
|
||
const TrainlogExercisePerformancePoint *point
|
||
)
|
||
{
|
||
if (point->load_mode ==
|
||
TRAINLOG_LOAD_NONE) {
|
||
return (double)point->metric_value;
|
||
}
|
||
|
||
return point->weight_kg;
|
||
}
|
||
|
||
static void draw_exercise_performance_graph(
|
||
const TrainlogExercisePerformancePoint *points,
|
||
size_t count,
|
||
TrainlogLoadMode mode,
|
||
TrainlogTrackingMode tracking_mode,
|
||
int top,
|
||
int height,
|
||
bool measured_max
|
||
)
|
||
{
|
||
size_t indices[EXERCISE_GRAPH_POINTS];
|
||
size_t selected_count = 0U;
|
||
size_t index;
|
||
double minimum = 0.0;
|
||
double maximum = 0.0;
|
||
const int left = 11;
|
||
int width =
|
||
COLS - left - 4;
|
||
|
||
if (points == NULL ||
|
||
count == 0U ||
|
||
height < 3 ||
|
||
width < 12) {
|
||
return;
|
||
}
|
||
|
||
for (index = 0U;
|
||
index < count &&
|
||
selected_count < EXERCISE_GRAPH_POINTS;
|
||
++index) {
|
||
if (points[index].has_performance != 0 &&
|
||
points[index].load_mode == mode) {
|
||
indices[selected_count] = index;
|
||
++selected_count;
|
||
}
|
||
}
|
||
|
||
if (selected_count == 0U) {
|
||
mvprintw(
|
||
top + 1,
|
||
4,
|
||
"Aucune performance réussie pour ce mode."
|
||
);
|
||
return;
|
||
}
|
||
|
||
minimum =
|
||
exercise_graph_value(
|
||
&points[indices[0]]
|
||
);
|
||
|
||
maximum = minimum;
|
||
|
||
for (index = 1U;
|
||
index < selected_count;
|
||
++index) {
|
||
double value =
|
||
exercise_graph_value(
|
||
&points[indices[index]]
|
||
);
|
||
|
||
if (value < minimum) {
|
||
minimum = value;
|
||
}
|
||
|
||
if (value > maximum) {
|
||
maximum = value;
|
||
}
|
||
}
|
||
|
||
if (maximum == minimum) {
|
||
minimum -= 1.0;
|
||
maximum += 1.0;
|
||
}
|
||
|
||
if (mode == TRAINLOG_LOAD_ASSISTANCE) {
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_WARNING
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
top,
|
||
left,
|
||
measured_max
|
||
? "Assistance mesurée (kg) — moins = mieux"
|
||
: "Assistance (kg) — moins = mieux"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_WARNING
|
||
)
|
||
);
|
||
} else if (mode ==
|
||
TRAINLOG_LOAD_EXTERNAL) {
|
||
mvprintw(
|
||
top,
|
||
left,
|
||
measured_max
|
||
? "Max mesuré (kg)"
|
||
: "Charge du meilleur set (kg)"
|
||
);
|
||
} else if (
|
||
tracking_mode ==
|
||
TRAINLOG_TRACKING_DURATION
|
||
) {
|
||
mvprintw(
|
||
top,
|
||
left,
|
||
measured_max
|
||
? "Durée max mesurée"
|
||
: "Meilleure durée"
|
||
);
|
||
} else {
|
||
mvprintw(
|
||
top,
|
||
left,
|
||
measured_max
|
||
? "Répétitions max mesurées"
|
||
: "Meilleures répétitions"
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
top + 1,
|
||
2,
|
||
"%.1f",
|
||
maximum
|
||
);
|
||
|
||
mvprintw(
|
||
top + height - 1,
|
||
2,
|
||
"%.1f",
|
||
minimum
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_GRAPH
|
||
)
|
||
);
|
||
|
||
{
|
||
int previous_x = -1;
|
||
int previous_y = -1;
|
||
size_t order;
|
||
|
||
for (order = selected_count;
|
||
order > 0U;
|
||
--order) {
|
||
size_t chronological =
|
||
selected_count - order;
|
||
|
||
const TrainlogExercisePerformancePoint *point =
|
||
&points[indices[order - 1U]];
|
||
|
||
double value =
|
||
exercise_graph_value(point);
|
||
|
||
double ratio =
|
||
(value - minimum) /
|
||
(maximum - minimum);
|
||
|
||
int x =
|
||
selected_count == 1U
|
||
? left + (width / 2)
|
||
: left +
|
||
(int)(
|
||
(chronological *
|
||
(size_t)(width - 1)) /
|
||
(selected_count - 1U)
|
||
);
|
||
|
||
int y =
|
||
top +
|
||
height -
|
||
1 -
|
||
(int)(
|
||
ratio *
|
||
(double)(height - 1)
|
||
);
|
||
|
||
if (previous_x >= 0 &&
|
||
x > previous_x) {
|
||
int line_x;
|
||
|
||
for (line_x = previous_x + 1;
|
||
line_x < x;
|
||
++line_x) {
|
||
int line_y =
|
||
previous_y +
|
||
(((y - previous_y) *
|
||
(line_x - previous_x)) /
|
||
(x - previous_x));
|
||
|
||
mvaddch(
|
||
line_y,
|
||
line_x,
|
||
(chtype)'.'
|
||
);
|
||
}
|
||
}
|
||
|
||
mvaddch(
|
||
y,
|
||
x,
|
||
chronological + 1U ==
|
||
selected_count
|
||
? (chtype)'O'
|
||
: (chtype)'*'
|
||
);
|
||
|
||
previous_x = x;
|
||
previous_y = y;
|
||
}
|
||
}
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_GRAPH
|
||
)
|
||
);
|
||
|
||
{
|
||
char oldest[11];
|
||
char newest[11];
|
||
|
||
exercise_short_date(
|
||
points[indices[selected_count - 1U]]
|
||
.started_at,
|
||
oldest
|
||
);
|
||
|
||
exercise_short_date(
|
||
points[indices[0]].started_at,
|
||
newest
|
||
);
|
||
|
||
mvprintw(
|
||
top + height,
|
||
left,
|
||
"%s",
|
||
oldest
|
||
);
|
||
|
||
mvprintw(
|
||
top + height,
|
||
left + width - 10,
|
||
"%s",
|
||
newest
|
||
);
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_EXERCISE_FRAMES */
|
||
|
||
static void exercise_panel(
|
||
int top,
|
||
int left,
|
||
int bottom,
|
||
int right,
|
||
const char *label
|
||
)
|
||
{
|
||
WINDOW *panel;
|
||
int height;
|
||
int width;
|
||
|
||
if (top < 0 ||
|
||
left < 0 ||
|
||
bottom <= top ||
|
||
right <= left ||
|
||
bottom >= LINES ||
|
||
right >= COLS) {
|
||
return;
|
||
}
|
||
|
||
height = bottom - top + 1;
|
||
width = right - left + 1;
|
||
|
||
panel = derwin(
|
||
stdscr,
|
||
height,
|
||
width,
|
||
top,
|
||
left
|
||
);
|
||
|
||
if (panel == NULL) {
|
||
return;
|
||
}
|
||
|
||
box(panel, 0, 0);
|
||
|
||
if (label != NULL &&
|
||
label[0] != '\0' &&
|
||
width > 8) {
|
||
wattron(
|
||
panel,
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvwprintw(
|
||
panel,
|
||
0,
|
||
2,
|
||
" %.*s ",
|
||
width - 6,
|
||
label
|
||
);
|
||
|
||
wattroff(
|
||
panel,
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
syncok(panel, TRUE);
|
||
wsyncup(panel);
|
||
delwin(panel);
|
||
}
|
||
|
||
static void screen_exercise_performance(
|
||
TrainlogDatabase *database,
|
||
const TrainlogExercise *exercise
|
||
)
|
||
{
|
||
TrainlogExercisePerformancePoint
|
||
points[MAX_SESSIONS];
|
||
|
||
size_t count = 0U;
|
||
size_t index;
|
||
size_t history_limit;
|
||
|
||
TrainlogLoadMode graph_mode =
|
||
TRAINLOG_LOAD_NONE;
|
||
|
||
const TrainlogExercisePerformancePoint *latest =
|
||
NULL;
|
||
|
||
const TrainlogExercisePerformancePoint *best =
|
||
NULL;
|
||
|
||
char latest_text[128];
|
||
char best_text[128];
|
||
|
||
bool decorated =
|
||
COLS >= 100 &&
|
||
LINES >= 36;
|
||
|
||
int summary_top =
|
||
decorated ? 8 : 3;
|
||
|
||
int summary_bottom =
|
||
decorated ? 14 : 9;
|
||
|
||
int graph_top =
|
||
decorated ? 15 : 11;
|
||
|
||
int graph_bottom =
|
||
decorated ? 25 : 20;
|
||
|
||
int history_top =
|
||
decorated ? 26 : 22;
|
||
|
||
int history_bottom =
|
||
LINES - 4;
|
||
|
||
if (database == NULL ||
|
||
exercise == NULL) {
|
||
return;
|
||
}
|
||
|
||
if (trainlog_database_list_exercise_performance(
|
||
database,
|
||
exercise->exercise_id,
|
||
points,
|
||
MAX_SESSIONS,
|
||
&count
|
||
) != TRAINLOG_STATUS_OK) {
|
||
draw_shell(
|
||
"Performance exercice",
|
||
"Une touche pour revenir"
|
||
);
|
||
|
||
status_line(
|
||
"Impossible de lire l'historique.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
|
||
wait_key();
|
||
return;
|
||
}
|
||
|
||
for (index = 0U;
|
||
index < count;
|
||
++index) {
|
||
if (points[index].has_performance != 0) {
|
||
latest = &points[index];
|
||
graph_mode =
|
||
points[index].load_mode;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (latest != NULL) {
|
||
for (index = 0U;
|
||
index < count;
|
||
++index) {
|
||
if (points[index].has_performance != 0 &&
|
||
points[index].load_mode ==
|
||
graph_mode &&
|
||
exercise_point_better(
|
||
&points[index],
|
||
best
|
||
)) {
|
||
best = &points[index];
|
||
}
|
||
}
|
||
}
|
||
|
||
exercise_format_performance(
|
||
latest,
|
||
latest_text,
|
||
sizeof(latest_text)
|
||
);
|
||
|
||
exercise_format_performance(
|
||
best,
|
||
best_text,
|
||
sizeof(best_text)
|
||
);
|
||
|
||
if (decorated) {
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
section_ascii_header(
|
||
":: P E R F O R M A N C E E X E R C I C E ::"
|
||
);
|
||
|
||
dashboard_panel(
|
||
summary_top,
|
||
2,
|
||
summary_bottom,
|
||
COLS - 3,
|
||
"PERFORMANCE"
|
||
);
|
||
|
||
dashboard_panel(
|
||
graph_top,
|
||
2,
|
||
graph_bottom,
|
||
COLS - 3,
|
||
"EVOLUTION"
|
||
);
|
||
|
||
if (history_bottom >
|
||
history_top + 2) {
|
||
focused_panel(
|
||
history_top,
|
||
2,
|
||
history_bottom,
|
||
COLS - 3,
|
||
"HISTORIQUE",
|
||
true
|
||
);
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"b/Échap retour"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"TRAINLOG — Performance exercice",
|
||
"b/Échap retour"
|
||
);
|
||
|
||
exercise_panel(
|
||
summary_top,
|
||
2,
|
||
summary_bottom,
|
||
COLS - 3,
|
||
"PERFORMANCE"
|
||
);
|
||
|
||
exercise_panel(
|
||
graph_top,
|
||
2,
|
||
graph_bottom,
|
||
COLS - 3,
|
||
"EVOLUTION"
|
||
);
|
||
|
||
if (history_bottom >
|
||
history_top + 2) {
|
||
exercise_panel(
|
||
history_top,
|
||
2,
|
||
history_bottom,
|
||
COLS - 3,
|
||
"HISTORIQUE"
|
||
);
|
||
}
|
||
}
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 1,
|
||
5,
|
||
"%s",
|
||
exercise->name
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 2,
|
||
5,
|
||
"Séances enregistrées : %zu",
|
||
count
|
||
);
|
||
|
||
if (latest == NULL) {
|
||
mvprintw(
|
||
summary_top + 3,
|
||
5,
|
||
"Aucune série réussie enregistrée."
|
||
);
|
||
} else {
|
||
mvprintw(
|
||
summary_top + 3,
|
||
5,
|
||
"Mode suivi : %s",
|
||
exercise_load_mode_label(
|
||
graph_mode
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 4,
|
||
5,
|
||
"Dernier meilleur set : %.*s",
|
||
COLS - 30,
|
||
latest_text
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 5,
|
||
5,
|
||
"Meilleur set enregistré : %.*s",
|
||
COLS - 33,
|
||
best_text
|
||
);
|
||
}
|
||
|
||
if (latest != NULL) {
|
||
int graph_content_top =
|
||
graph_top + 1;
|
||
|
||
int graph_height =
|
||
graph_bottom -
|
||
graph_top -
|
||
2;
|
||
|
||
draw_exercise_performance_graph(
|
||
points,
|
||
count,
|
||
graph_mode,
|
||
exercise->tracking_mode,
|
||
graph_content_top,
|
||
graph_height,
|
||
false
|
||
);
|
||
}
|
||
|
||
if (history_bottom >
|
||
history_top + 2) {
|
||
int history_first_row =
|
||
history_top + 1;
|
||
|
||
history_limit =
|
||
history_bottom >
|
||
history_first_row
|
||
? (size_t)(
|
||
history_bottom -
|
||
history_first_row
|
||
)
|
||
: 0U;
|
||
|
||
for (index = 0U;
|
||
index < count &&
|
||
index < history_limit;
|
||
++index) {
|
||
char date[11];
|
||
char summary[128];
|
||
|
||
exercise_short_date(
|
||
points[index].started_at,
|
||
date
|
||
);
|
||
|
||
exercise_format_performance(
|
||
&points[index],
|
||
summary,
|
||
sizeof(summary)
|
||
);
|
||
|
||
mvprintw(
|
||
history_first_row +
|
||
(int)index,
|
||
5,
|
||
"%s %-13s %.*s",
|
||
date,
|
||
exercise_load_mode_label(
|
||
points[index].load_mode
|
||
),
|
||
COLS - 40,
|
||
summary
|
||
);
|
||
}
|
||
}
|
||
|
||
refresh();
|
||
|
||
for (;;) {
|
||
int key = getch();
|
||
|
||
if (key == 'b' ||
|
||
key == 'B' ||
|
||
key == 27 ||
|
||
key == '\n' ||
|
||
key == KEY_ENTER) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_MEASURED_MAX_TUI_V1 */
|
||
|
||
static void screen_exercise_measured_max(
|
||
TrainlogDatabase *database,
|
||
const TrainlogExercise *exercise
|
||
)
|
||
{
|
||
static const double increments[] = {
|
||
0.5,
|
||
1.0,
|
||
2.5,
|
||
5.0
|
||
};
|
||
|
||
static const double percentages[] = {
|
||
60.0,
|
||
70.0,
|
||
80.0,
|
||
90.0
|
||
};
|
||
|
||
size_t increment_index = 2U;
|
||
|
||
if (
|
||
database == NULL ||
|
||
exercise == NULL
|
||
) {
|
||
return;
|
||
}
|
||
|
||
for (;;) {
|
||
TrainlogExercisePerformancePoint
|
||
points[MAX_SESSIONS];
|
||
|
||
TrainlogExercisePerformancePoint
|
||
max_points[MAX_SESSIONS];
|
||
|
||
TrainlogMeasuredMaxSummary summary;
|
||
|
||
size_t count = 0U;
|
||
size_t max_count = 0U;
|
||
size_t index;
|
||
size_t history_limit;
|
||
|
||
bool decorated =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
int summary_top =
|
||
decorated ? 8 : 3;
|
||
|
||
int summary_bottom =
|
||
decorated ? 15 : 9;
|
||
|
||
int graph_top =
|
||
decorated ? 16 : 10;
|
||
|
||
int graph_bottom =
|
||
decorated ? 23 : 17;
|
||
|
||
int history_top =
|
||
decorated ? 24 : 18;
|
||
|
||
int history_bottom =
|
||
LINES - 4;
|
||
|
||
int key;
|
||
|
||
if (
|
||
trainlog_database_list_exercise_performance(
|
||
database,
|
||
exercise->exercise_id,
|
||
points,
|
||
MAX_SESSIONS,
|
||
&count
|
||
) != TRAINLOG_STATUS_OK ||
|
||
trainlog_measured_max_summarize(
|
||
points,
|
||
count,
|
||
&summary
|
||
) != TRAINLOG_STATUS_OK
|
||
) {
|
||
draw_shell(
|
||
"TRAINLOG — Max mesuré",
|
||
"Une touche pour revenir"
|
||
);
|
||
|
||
status_line(
|
||
"Impossible de lire les tests de max.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
|
||
wait_key();
|
||
return;
|
||
}
|
||
|
||
for (
|
||
index = 0U;
|
||
index < count &&
|
||
max_count < MAX_SESSIONS;
|
||
++index
|
||
) {
|
||
if (
|
||
points[index].session_type ==
|
||
TRAINLOG_SESSION_MAX_TEST
|
||
) {
|
||
max_points[max_count] =
|
||
points[index];
|
||
|
||
++max_count;
|
||
}
|
||
}
|
||
|
||
erase();
|
||
box(
|
||
stdscr,
|
||
0,
|
||
0
|
||
);
|
||
|
||
if (decorated) {
|
||
section_ascii_header(
|
||
":: M A X M E S U R E ::"
|
||
);
|
||
|
||
dashboard_panel(
|
||
summary_top,
|
||
2,
|
||
summary_bottom,
|
||
COLS - 3,
|
||
"MAX MESURE"
|
||
);
|
||
|
||
dashboard_panel(
|
||
graph_top,
|
||
2,
|
||
graph_bottom,
|
||
COLS - 3,
|
||
"EVOLUTION DES TESTS MAX"
|
||
);
|
||
|
||
if (
|
||
history_bottom >
|
||
history_top + 1
|
||
) {
|
||
exercise_panel(
|
||
history_top,
|
||
2,
|
||
history_bottom,
|
||
COLS - 3,
|
||
"HISTORIQUE TESTS MAX"
|
||
);
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"r arrondi charge b/Échap retour"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"TRAINLOG — Max mesuré",
|
||
"r arrondi charge b/Échap retour"
|
||
);
|
||
|
||
exercise_panel(
|
||
summary_top,
|
||
2,
|
||
summary_bottom,
|
||
COLS - 3,
|
||
"MAX MESURE"
|
||
);
|
||
|
||
exercise_panel(
|
||
graph_top,
|
||
2,
|
||
graph_bottom,
|
||
COLS - 3,
|
||
"EVOLUTION"
|
||
);
|
||
|
||
if (
|
||
history_bottom >
|
||
history_top + 1
|
||
) {
|
||
exercise_panel(
|
||
history_top,
|
||
2,
|
||
history_bottom,
|
||
COLS - 3,
|
||
"HISTORIQUE"
|
||
);
|
||
}
|
||
}
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 1,
|
||
5,
|
||
"%s",
|
||
exercise->name
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 2,
|
||
5,
|
||
"Tests max : %zu · réussis : %zu",
|
||
summary.test_count,
|
||
summary.successful_test_count
|
||
);
|
||
|
||
if (!summary.found) {
|
||
mvprintw(
|
||
summary_top + 3,
|
||
5,
|
||
"Aucun max mesuré réussi."
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 4,
|
||
5,
|
||
"Seules les séances explicitement « Test de max » comptent."
|
||
);
|
||
} else {
|
||
char current_text[128];
|
||
char record_text[128];
|
||
char current_date[11];
|
||
char record_date[11];
|
||
|
||
exercise_format_performance(
|
||
&summary.current,
|
||
current_text,
|
||
sizeof(current_text)
|
||
);
|
||
|
||
exercise_format_performance(
|
||
&summary.record,
|
||
record_text,
|
||
sizeof(record_text)
|
||
);
|
||
|
||
exercise_short_date(
|
||
summary.current.started_at,
|
||
current_date
|
||
);
|
||
|
||
exercise_short_date(
|
||
summary.record.started_at,
|
||
record_date
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 3,
|
||
5,
|
||
"Actuel : %s · %.*s",
|
||
current_date,
|
||
COLS - 32,
|
||
current_text
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 4,
|
||
5,
|
||
"Record même mode : %s · %.*s",
|
||
record_date,
|
||
COLS - 42,
|
||
record_text
|
||
);
|
||
|
||
if (
|
||
summary.current.load_mode ==
|
||
TRAINLOG_LOAD_EXTERNAL
|
||
) {
|
||
double working[4];
|
||
bool valid = true;
|
||
|
||
for (
|
||
index = 0U;
|
||
index < 4U;
|
||
++index
|
||
) {
|
||
if (
|
||
trainlog_measured_max_working_load(
|
||
&summary.current,
|
||
percentages[index],
|
||
increments[increment_index],
|
||
&working[index]
|
||
) != TRAINLOG_STATUS_OK
|
||
) {
|
||
valid = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (valid) {
|
||
mvprintw(
|
||
summary_top + 5,
|
||
5,
|
||
"Travail : 60%% %.1f · 70%% %.1f · 80%% %.1f · 90%% %.1f kg",
|
||
working[0],
|
||
working[1],
|
||
working[2],
|
||
working[3]
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 6,
|
||
5,
|
||
"Arrondi : %.1f kg (r pour changer) · aucun 1RM estimé",
|
||
increments[increment_index]
|
||
);
|
||
}
|
||
} else if (
|
||
summary.current.load_mode ==
|
||
TRAINLOG_LOAD_ASSISTANCE
|
||
) {
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_WARNING
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 5,
|
||
5,
|
||
"Assistance : moins de kg = mieux."
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 6,
|
||
5,
|
||
"Pourcentages de charge non applicables à l'assistance."
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_WARNING
|
||
)
|
||
);
|
||
} else {
|
||
mvprintw(
|
||
summary_top + 5,
|
||
5,
|
||
"Sans charge externe : pourcentages non applicables."
|
||
);
|
||
|
||
mvprintw(
|
||
summary_top + 6,
|
||
5,
|
||
"Le max reste une valeur réellement réalisée, jamais estimée."
|
||
);
|
||
}
|
||
}
|
||
|
||
if (
|
||
summary.found &&
|
||
max_count > 0U
|
||
) {
|
||
int graph_content_top =
|
||
graph_top + 1;
|
||
|
||
int graph_height =
|
||
graph_bottom -
|
||
graph_top -
|
||
2;
|
||
|
||
draw_exercise_performance_graph(
|
||
max_points,
|
||
max_count,
|
||
summary.current.load_mode,
|
||
exercise->tracking_mode,
|
||
graph_content_top,
|
||
graph_height,
|
||
true
|
||
);
|
||
} else {
|
||
mvprintw(
|
||
graph_top + 2,
|
||
5,
|
||
"Aucun point de max mesuré à tracer."
|
||
);
|
||
}
|
||
|
||
if (
|
||
history_bottom >
|
||
history_top + 1
|
||
) {
|
||
int first_row =
|
||
history_top + 1;
|
||
|
||
history_limit =
|
||
history_bottom >
|
||
first_row
|
||
? (size_t)(
|
||
history_bottom -
|
||
first_row
|
||
)
|
||
: 0U;
|
||
|
||
for (
|
||
index = 0U;
|
||
index < max_count &&
|
||
index < history_limit;
|
||
++index
|
||
) {
|
||
char date[11];
|
||
char text[128];
|
||
|
||
exercise_short_date(
|
||
max_points[index].started_at,
|
||
date
|
||
);
|
||
|
||
exercise_format_performance(
|
||
&max_points[index],
|
||
text,
|
||
sizeof(text)
|
||
);
|
||
|
||
mvprintw(
|
||
first_row +
|
||
(int)index,
|
||
5,
|
||
"%s %-13s %.*s",
|
||
date,
|
||
exercise_load_mode_label(
|
||
max_points[index]
|
||
.load_mode
|
||
),
|
||
COLS - 40,
|
||
text
|
||
);
|
||
}
|
||
}
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (
|
||
key == 'b' ||
|
||
key == 'B' ||
|
||
key == 27 ||
|
||
key == '\n' ||
|
||
key == KEY_ENTER
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (
|
||
key == 'r' ||
|
||
key == 'R'
|
||
) {
|
||
increment_index =
|
||
(
|
||
increment_index + 1U
|
||
) %
|
||
(
|
||
sizeof(increments) /
|
||
sizeof(increments[0])
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_SECTION_ASCII_HEADER */
|
||
|
||
static void section_ascii_header(
|
||
const char *subtitle
|
||
)
|
||
{
|
||
static const char *const logo[] = {
|
||
"TTTTT RRRR AAA IIIII N N L OOO GGG ",
|
||
" T R R A A I NN N L O O G ",
|
||
" T RRRR AAAAA I N N N L O O G GG",
|
||
" T R R A A I N NN L O O G G",
|
||
" T R R A A IIIII N N LLLLL OOO GGG "
|
||
};
|
||
|
||
const size_t line_count =
|
||
sizeof(logo) /
|
||
sizeof(logo[0]);
|
||
|
||
size_t index;
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
for (index = 0U;
|
||
index < line_count;
|
||
++index) {
|
||
int width =
|
||
(int)strlen(logo[index]);
|
||
|
||
int column =
|
||
(COLS - width) / 2;
|
||
|
||
if (column < 2) {
|
||
column = 2;
|
||
}
|
||
|
||
mvprintw(
|
||
1 + (int)index,
|
||
column,
|
||
"%.*s",
|
||
COLS - column - 2,
|
||
logo[index]
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
if (subtitle != NULL &&
|
||
subtitle[0] != '\0') {
|
||
int width =
|
||
(int)strlen(subtitle);
|
||
|
||
int column =
|
||
(COLS - width) / 2;
|
||
|
||
if (column < 2) {
|
||
column = 2;
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
6,
|
||
column,
|
||
"%s",
|
||
subtitle
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
static void section_scrollbar(
|
||
int top,
|
||
int bottom,
|
||
int column,
|
||
size_t selected,
|
||
size_t count,
|
||
size_t visible
|
||
)
|
||
{
|
||
int track_height;
|
||
int row;
|
||
int thumb;
|
||
|
||
if (bottom <= top ||
|
||
count <= visible ||
|
||
count <= 1U) {
|
||
return;
|
||
}
|
||
|
||
track_height =
|
||
bottom - top + 1;
|
||
|
||
if (track_height < 2) {
|
||
return;
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
for (row = top;
|
||
row <= bottom;
|
||
++row) {
|
||
mvaddch(
|
||
row,
|
||
column,
|
||
ACS_VLINE
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
thumb =
|
||
top +
|
||
(int)(
|
||
(selected *
|
||
(size_t)(track_height - 1)) /
|
||
(count - 1U)
|
||
);
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvaddch(
|
||
thumb,
|
||
column,
|
||
ACS_CKBOARD
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
static void screen_exercises(
|
||
TrainlogDatabase *database
|
||
)
|
||
{
|
||
TrainlogExercise exercises[MAX_EXERCISES];
|
||
size_t selected = 0U;
|
||
int nav_selected = 3;
|
||
int focus = 1;
|
||
|
||
for (;;) {
|
||
size_t count = 0U;
|
||
size_t top = 0U;
|
||
size_t index;
|
||
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
bool framed =
|
||
COLS >= 90 &&
|
||
LINES >= 24;
|
||
|
||
int list_top =
|
||
large_layout
|
||
? 11
|
||
: 3;
|
||
|
||
int list_bottom =
|
||
LINES - 4;
|
||
|
||
int first_row =
|
||
list_top + 1;
|
||
|
||
int visible_rows =
|
||
framed
|
||
? list_bottom -
|
||
first_row
|
||
: LINES - 7;
|
||
|
||
int key;
|
||
|
||
if (visible_rows < 1) {
|
||
return;
|
||
}
|
||
|
||
if (trainlog_database_list_exercises(
|
||
database,
|
||
exercises,
|
||
MAX_EXERCISES,
|
||
&count
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
selected >= count) {
|
||
selected =
|
||
count - 1U;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
selected >=
|
||
(size_t)visible_rows) {
|
||
top =
|
||
selected -
|
||
(size_t)visible_rows +
|
||
1U;
|
||
}
|
||
|
||
if (large_layout) {
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
section_ascii_header(
|
||
":: E X E R C I C E S ::"
|
||
);
|
||
|
||
primary_top_navbar(
|
||
3,
|
||
nav_selected,
|
||
focus == 0
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"Tab zone ↑↓/PgUp/PgDn catalogue ←→ menu Entrée ouvrir m max mesuré a ajouter 0/Home accueil F1-F4 direct b/Échap retour"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"TRAINLOG — Exercices",
|
||
"↑↓ naviguer Entrée performance m max mesuré a ajouter b/Échap retour"
|
||
);
|
||
}
|
||
|
||
if (framed) {
|
||
if (large_layout) {
|
||
focused_panel(
|
||
list_top,
|
||
2,
|
||
list_bottom,
|
||
COLS - 3,
|
||
"CATALOGUE",
|
||
focus == 1
|
||
);
|
||
} else {
|
||
exercise_panel(
|
||
list_top,
|
||
2,
|
||
list_bottom,
|
||
COLS - 3,
|
||
"CATALOGUE"
|
||
);
|
||
}
|
||
}
|
||
|
||
if (count == 0U) {
|
||
mvprintw(
|
||
first_row + 1,
|
||
framed ? 5 : 4,
|
||
"Aucun exercice."
|
||
);
|
||
}
|
||
|
||
for (index = 0U;
|
||
index < (size_t)visible_rows &&
|
||
top + index < count;
|
||
++index) {
|
||
size_t absolute =
|
||
top + index;
|
||
|
||
int item_row =
|
||
first_row +
|
||
(int)index;
|
||
|
||
int item_col =
|
||
framed ? 5 : 4;
|
||
|
||
if (focus == 1 &&
|
||
absolute == selected) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
item_row,
|
||
item_col,
|
||
" %-42s [%s] ",
|
||
exercises[absolute].name,
|
||
exercises[absolute].tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS
|
||
? "reps"
|
||
: "durée"
|
||
);
|
||
|
||
if (focus == 1 &&
|
||
absolute == selected) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
if (large_layout) {
|
||
section_scrollbar(
|
||
first_row,
|
||
list_bottom - 1,
|
||
COLS - 5,
|
||
selected,
|
||
count,
|
||
(size_t)visible_rows
|
||
);
|
||
}
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (large_layout &&
|
||
(key == '\t' ||
|
||
key == KEY_BTAB)) {
|
||
focus =
|
||
focus == 0
|
||
? 1
|
||
: 0;
|
||
continue;
|
||
}
|
||
|
||
if (large_layout &&
|
||
primary_top_nav_forward(key)) {
|
||
return;
|
||
}
|
||
|
||
if (key == 'b' ||
|
||
key == 'B' ||
|
||
key == 27) {
|
||
return;
|
||
}
|
||
|
||
if (large_layout &&
|
||
focus == 0) {
|
||
if (key == KEY_LEFT) {
|
||
nav_selected =
|
||
nav_selected > 0
|
||
? nav_selected - 1
|
||
: 5;
|
||
} else if (key == KEY_RIGHT) {
|
||
nav_selected =
|
||
nav_selected < 5
|
||
? nav_selected + 1
|
||
: 0;
|
||
} else if (
|
||
key == '\n' ||
|
||
key == KEY_ENTER
|
||
) {
|
||
if (primary_top_nav_activate(
|
||
nav_selected
|
||
)) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
key == KEY_UP) {
|
||
selected =
|
||
selected > 0U
|
||
? selected - 1U
|
||
: 0U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
key == KEY_DOWN) {
|
||
selected =
|
||
selected + 1U < count
|
||
? selected + 1U
|
||
: count - 1U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
key == KEY_PPAGE) {
|
||
size_t jump =
|
||
(size_t)visible_rows;
|
||
|
||
selected =
|
||
selected > jump
|
||
? selected - jump
|
||
: 0U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
key == KEY_NPAGE) {
|
||
size_t jump =
|
||
(size_t)visible_rows;
|
||
|
||
selected =
|
||
selected + jump < count
|
||
? selected + jump
|
||
: count - 1U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
(key == 'm' ||
|
||
key == 'M')) {
|
||
screen_exercise_measured_max(
|
||
database,
|
||
&exercises[selected]
|
||
);
|
||
|
||
continue;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
(key == '\n' ||
|
||
key == KEY_ENTER)) {
|
||
screen_exercise_performance(
|
||
database,
|
||
&exercises[selected]
|
||
);
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == 'a' ||
|
||
key == 'A') {
|
||
char name[TRAINLOG_NAME_MAX + 1U];
|
||
int mode = 1;
|
||
int organization = 1;
|
||
int wants_speed = 0;
|
||
int wants_distance = 0;
|
||
TrainlogExerciseDataFields data_fields = 0U;
|
||
TrainlogExercise created;
|
||
TrainlogStatus status;
|
||
|
||
draw_shell(
|
||
"Nouvel exercice",
|
||
"Entrée valide chaque champ · Échap annule"
|
||
);
|
||
|
||
if (!prompt_text(
|
||
4,
|
||
"Nom : ",
|
||
name,
|
||
sizeof(name),
|
||
false
|
||
)) {
|
||
continue;
|
||
}
|
||
|
||
if (!prompt_int_value(
|
||
5,
|
||
"Mode 1=reps 2=durée",
|
||
1,
|
||
2,
|
||
1,
|
||
&mode
|
||
)) {
|
||
continue;
|
||
}
|
||
|
||
if (!prompt_int_value(
|
||
8,
|
||
"Organisation (1 séries, 2 continu)",
|
||
1,
|
||
2,
|
||
1,
|
||
&organization
|
||
)) {
|
||
continue;
|
||
}
|
||
|
||
if (organization == 2) {
|
||
mode = 2;
|
||
|
||
if (!prompt_int_value(
|
||
10,
|
||
"Mesurer la vitesse km/h ? (0 non, 1 oui)",
|
||
0,
|
||
1,
|
||
1,
|
||
&wants_speed
|
||
)) {
|
||
continue;
|
||
}
|
||
|
||
if (!prompt_int_value(
|
||
12,
|
||
"Mesurer la distance km ? (0 non, 1 oui)",
|
||
0,
|
||
1,
|
||
0,
|
||
&wants_distance
|
||
)) {
|
||
continue;
|
||
}
|
||
|
||
if (wants_speed != 0) {
|
||
data_fields |=
|
||
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||
}
|
||
|
||
if (wants_distance != 0) {
|
||
data_fields |=
|
||
TRAINLOG_EXERCISE_DATA_DISTANCE_KM;
|
||
}
|
||
}
|
||
status =
|
||
trainlog_catalog_create_exercise_profiled(
|
||
database,
|
||
name,
|
||
mode == 1
|
||
? TRAINLOG_TRACKING_REPS
|
||
: TRAINLOG_TRACKING_DURATION,
|
||
organization == 2
|
||
? TRAINLOG_RECORDING_CONTINUOUS
|
||
: TRAINLOG_RECORDING_SETS,
|
||
data_fields,
|
||
&created
|
||
);
|
||
|
||
if (status ==
|
||
TRAINLOG_STATUS_OK) {
|
||
status_line(
|
||
"✓ Exercice ajouté.",
|
||
TRAINLOG_COLOR_SUCCESS
|
||
);
|
||
} else if (
|
||
status ==
|
||
TRAINLOG_STATUS_CONFLICT
|
||
) {
|
||
status_line(
|
||
"Doublon détecté.",
|
||
TRAINLOG_COLOR_WARNING
|
||
);
|
||
} else {
|
||
status_line(
|
||
"Impossible d'ajouter l'exercice.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
}
|
||
|
||
wait_key();
|
||
}
|
||
}
|
||
}
|
||
|
||
static bool create_exercise_inline(
|
||
TrainlogDatabase *database
|
||
)
|
||
{
|
||
char name[TRAINLOG_NAME_MAX + 1U];
|
||
int mode = 1;
|
||
int organization = 1;
|
||
int wants_speed = 0;
|
||
int wants_distance = 0;
|
||
TrainlogExerciseDataFields data_fields = 0U;
|
||
TrainlogExercise created;
|
||
TrainlogStatus status;
|
||
|
||
draw_shell(
|
||
"Nouvel exercice",
|
||
"Entrée valide chaque champ · Échap annule"
|
||
);
|
||
|
||
if (!prompt_text(
|
||
4,
|
||
"Nom : ",
|
||
name,
|
||
sizeof(name),
|
||
false
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (!prompt_int_value(
|
||
5,
|
||
"Mode 1=reps 2=durée",
|
||
1,
|
||
2,
|
||
1,
|
||
&mode
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (!prompt_int_value(
|
||
8,
|
||
"Organisation (1 séries, 2 continu)",
|
||
1,
|
||
2,
|
||
1,
|
||
&organization
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (organization == 2) {
|
||
mode = 2;
|
||
|
||
if (!prompt_int_value(
|
||
10,
|
||
"Mesurer la vitesse km/h ? (0 non, 1 oui)",
|
||
0,
|
||
1,
|
||
1,
|
||
&wants_speed
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (!prompt_int_value(
|
||
12,
|
||
"Mesurer la distance km ? (0 non, 1 oui)",
|
||
0,
|
||
1,
|
||
0,
|
||
&wants_distance
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (wants_speed != 0) {
|
||
data_fields |=
|
||
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||
}
|
||
|
||
if (wants_distance != 0) {
|
||
data_fields |=
|
||
TRAINLOG_EXERCISE_DATA_DISTANCE_KM;
|
||
}
|
||
}
|
||
status =
|
||
trainlog_catalog_create_exercise_profiled(
|
||
database,
|
||
name,
|
||
mode == 1
|
||
? TRAINLOG_TRACKING_REPS
|
||
: TRAINLOG_TRACKING_DURATION,
|
||
organization == 2
|
||
? TRAINLOG_RECORDING_CONTINUOUS
|
||
: TRAINLOG_RECORDING_SETS,
|
||
data_fields,
|
||
&created
|
||
);
|
||
|
||
if (status == TRAINLOG_STATUS_OK) {
|
||
status_line(
|
||
"✓ Exercice ajouté au catalogue.",
|
||
TRAINLOG_COLOR_SUCCESS
|
||
);
|
||
} else if (
|
||
status == TRAINLOG_STATUS_CONFLICT
|
||
) {
|
||
status_line(
|
||
"Doublon détecté.",
|
||
TRAINLOG_COLOR_WARNING
|
||
);
|
||
} else {
|
||
status_line(
|
||
"Impossible d'ajouter l'exercice.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
}
|
||
|
||
wait_key();
|
||
|
||
return status == TRAINLOG_STATUS_OK;
|
||
}
|
||
|
||
static bool choose_exercise(
|
||
TrainlogDatabase *database,
|
||
TrainlogExercise *output
|
||
)
|
||
{
|
||
TrainlogExercise exercises[MAX_EXERCISES];
|
||
size_t selected = 0U;
|
||
|
||
for (;;) {
|
||
size_t count = 0U;
|
||
size_t top = 0U;
|
||
size_t index;
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
int list_top =
|
||
large_layout ? 8 : 3;
|
||
int list_bottom =
|
||
LINES - 4;
|
||
int first_row =
|
||
list_top + 1;
|
||
int visible_rows =
|
||
list_bottom - first_row;
|
||
int key;
|
||
|
||
if (visible_rows < 1) {
|
||
return false;
|
||
}
|
||
|
||
if (trainlog_database_list_exercises(
|
||
database,
|
||
exercises,
|
||
MAX_EXERCISES,
|
||
&count
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return false;
|
||
}
|
||
|
||
if (count == 0U) {
|
||
draw_shell(
|
||
"Choisir un exercice",
|
||
"a ajouter un exercice · Échap annuler"
|
||
);
|
||
|
||
mvprintw(
|
||
4,
|
||
4,
|
||
"Aucun exercice disponible."
|
||
);
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (key == 'a' ||
|
||
key == 'A') {
|
||
(void)create_exercise_inline(
|
||
database
|
||
);
|
||
continue;
|
||
}
|
||
|
||
if (key == 27) {
|
||
return false;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (selected >= count) {
|
||
selected =
|
||
count - 1U;
|
||
}
|
||
|
||
if (selected >=
|
||
(size_t)visible_rows) {
|
||
top =
|
||
selected -
|
||
(size_t)visible_rows +
|
||
1U;
|
||
}
|
||
|
||
if (large_layout) {
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
section_ascii_header(
|
||
":: C H O I S I R E X E R C I C E ::"
|
||
);
|
||
|
||
focused_panel(
|
||
list_top,
|
||
2,
|
||
list_bottom,
|
||
COLS - 3,
|
||
"CATALOGUE",
|
||
true
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"↑↓ choisir Entrée sélectionner a créer un exercice Échap annuler"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"Choisir un exercice",
|
||
"↑↓ naviguer Entrée choisir a ajouter Échap annuler"
|
||
);
|
||
}
|
||
|
||
for (index = 0U;
|
||
index < (size_t)visible_rows &&
|
||
top + index < count;
|
||
++index) {
|
||
size_t absolute =
|
||
top + index;
|
||
|
||
int item_row =
|
||
first_row +
|
||
(int)index;
|
||
|
||
if (absolute == selected) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
item_row,
|
||
large_layout ? 5 : 4,
|
||
" %-42s [%s] ",
|
||
exercises[absolute].name,
|
||
exercises[absolute].tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS
|
||
? "reps"
|
||
: "durée"
|
||
);
|
||
|
||
if (absolute == selected) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
if (large_layout) {
|
||
section_scrollbar(
|
||
first_row,
|
||
list_bottom - 1,
|
||
COLS - 5,
|
||
selected,
|
||
count,
|
||
(size_t)visible_rows
|
||
);
|
||
}
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (key == KEY_UP) {
|
||
selected =
|
||
selected > 0U
|
||
? selected - 1U
|
||
: count - 1U;
|
||
} else if (key == KEY_DOWN) {
|
||
selected =
|
||
selected + 1U < count
|
||
? selected + 1U
|
||
: 0U;
|
||
} else if (
|
||
key == '\n' ||
|
||
key == KEY_ENTER
|
||
) {
|
||
*output =
|
||
exercises[selected];
|
||
|
||
return true;
|
||
} else if (
|
||
key == 'a' ||
|
||
key == 'A'
|
||
) {
|
||
(void)create_exercise_inline(
|
||
database
|
||
);
|
||
} else if (key == 27) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_DURATION_BODY_GRAPH_HELPERS */
|
||
|
||
typedef struct TrainlogBodyMetricView {
|
||
TrainlogBodyMetric metric;
|
||
const char *label;
|
||
const char *unit;
|
||
} TrainlogBodyMetricView;
|
||
|
||
static const TrainlogBodyMetricView BODY_METRICS[] = {
|
||
{TRAINLOG_BODY_METRIC_WEIGHT, "Poids", "kg"},
|
||
{TRAINLOG_BODY_METRIC_NECK, "Cou", "cm"},
|
||
{TRAINLOG_BODY_METRIC_SHOULDERS, "Épaules", "cm"},
|
||
{TRAINLOG_BODY_METRIC_CHEST, "Poitrine", "cm"},
|
||
{TRAINLOG_BODY_METRIC_WAIST, "Tour de taille", "cm"},
|
||
{TRAINLOG_BODY_METRIC_HIPS, "Hanches", "cm"},
|
||
{TRAINLOG_BODY_METRIC_LEFT_ARM, "Bras gauche", "cm"},
|
||
{TRAINLOG_BODY_METRIC_RIGHT_ARM, "Bras droit", "cm"},
|
||
{TRAINLOG_BODY_METRIC_LEFT_FOREARM, "Avant-bras gauche", "cm"},
|
||
{TRAINLOG_BODY_METRIC_RIGHT_FOREARM, "Avant-bras droit", "cm"},
|
||
{TRAINLOG_BODY_METRIC_LEFT_THIGH, "Cuisse gauche", "cm"},
|
||
{TRAINLOG_BODY_METRIC_RIGHT_THIGH, "Cuisse droite", "cm"},
|
||
{TRAINLOG_BODY_METRIC_LEFT_CALF, "Mollet gauche", "cm"},
|
||
{TRAINLOG_BODY_METRIC_RIGHT_CALF, "Mollet droit", "cm"}
|
||
};
|
||
|
||
static bool prompt_duration_value(
|
||
int row,
|
||
const char *label,
|
||
int minimum_seconds,
|
||
int maximum_seconds,
|
||
int default_seconds,
|
||
int *output_seconds
|
||
)
|
||
{
|
||
char buffer[64];
|
||
char default_text[64];
|
||
char decorated[192];
|
||
|
||
if (trainlog_duration_format(
|
||
default_seconds,
|
||
default_text,
|
||
sizeof(default_text)
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return false;
|
||
}
|
||
|
||
for (;;) {
|
||
int parsed;
|
||
|
||
(void)snprintf(
|
||
decorated,
|
||
sizeof(decorated),
|
||
"%s [%s]: ",
|
||
label,
|
||
default_text
|
||
);
|
||
|
||
if (!prompt_text(
|
||
row,
|
||
decorated,
|
||
buffer,
|
||
sizeof(buffer),
|
||
true
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (buffer[0] == '\0') {
|
||
*output_seconds = default_seconds;
|
||
return true;
|
||
}
|
||
|
||
if (trainlog_duration_parse(buffer, &parsed) ==
|
||
TRAINLOG_STATUS_OK &&
|
||
parsed >= minimum_seconds &&
|
||
parsed <= maximum_seconds) {
|
||
*output_seconds = parsed;
|
||
return true;
|
||
}
|
||
|
||
status_line(
|
||
"Durée invalide : 90, 90s, 1:30, 1m30, 2m.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
refresh();
|
||
}
|
||
}
|
||
|
||
static void draw_body_metric_graph(
|
||
int row,
|
||
int height,
|
||
const TrainlogBodyMetricPoint *points,
|
||
size_t count,
|
||
const char *unit
|
||
)
|
||
{
|
||
double minimum;
|
||
double maximum;
|
||
size_t start;
|
||
size_t index;
|
||
int width;
|
||
|
||
if (count == 0U) {
|
||
mvprintw(row, 4, "Aucune donnée pour cette mesure.");
|
||
return;
|
||
}
|
||
|
||
width = COLS - 14;
|
||
if (width < 10 || height < 3) {
|
||
return;
|
||
}
|
||
|
||
start = count > (size_t)width
|
||
? count - (size_t)width
|
||
: 0U;
|
||
|
||
minimum = points[start].value;
|
||
maximum = points[start].value;
|
||
|
||
for (index = start + 1U; index < count; ++index) {
|
||
if (points[index].value < minimum) {
|
||
minimum = points[index].value;
|
||
}
|
||
if (points[index].value > maximum) {
|
||
maximum = points[index].value;
|
||
}
|
||
}
|
||
|
||
if (maximum == minimum) {
|
||
int graph_row = row + (height / 2);
|
||
|
||
mvprintw(graph_row, 2, "%.1f %s", minimum, unit);
|
||
|
||
attron(trainlog_theme_attribute(TRAINLOG_COLOR_GRAPH));
|
||
|
||
for (index = start; index < count; ++index) {
|
||
int x = 12 + (int)(index - start);
|
||
|
||
if (x < COLS - 2) {
|
||
mvaddch(graph_row, x, (chtype)'*');
|
||
}
|
||
}
|
||
|
||
attroff(trainlog_theme_attribute(TRAINLOG_COLOR_GRAPH));
|
||
return;
|
||
}
|
||
|
||
mvprintw(row, 2, "%.1f", maximum);
|
||
mvprintw(row + height - 1, 2, "%.1f", minimum);
|
||
|
||
attron(trainlog_theme_attribute(TRAINLOG_COLOR_GRAPH));
|
||
|
||
for (index = start; index < count; ++index) {
|
||
double ratio =
|
||
(points[index].value - minimum) /
|
||
(maximum - minimum);
|
||
|
||
int y = row + height - 1 -
|
||
(int)(ratio * (double)(height - 1));
|
||
|
||
int x = 10 + (int)(index - start);
|
||
|
||
if (y < row) {
|
||
y = row;
|
||
}
|
||
if (y > row + height - 1) {
|
||
y = row + height - 1;
|
||
}
|
||
|
||
if (x < COLS - 2) {
|
||
mvaddch(y, x, (chtype)'*');
|
||
}
|
||
}
|
||
|
||
attroff(trainlog_theme_attribute(TRAINLOG_COLOR_GRAPH));
|
||
}
|
||
|
||
static void add_body_observation(TrainlogDatabase *database)
|
||
{
|
||
TrainlogBodyObservationInput observation;
|
||
char id[TRAINLOG_GENERATED_ID_CAPACITY];
|
||
char timestamp[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||
int row;
|
||
|
||
(void)memset(&observation, 0, sizeof(observation));
|
||
|
||
if (trainlog_id_generate("bo", id, sizeof(id)) != TRAINLOG_STATUS_OK ||
|
||
trainlog_time_now_rfc3339(
|
||
timestamp,
|
||
sizeof(timestamp)
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return;
|
||
}
|
||
|
||
(void)snprintf(
|
||
observation.observation_id,
|
||
sizeof(observation.observation_id),
|
||
"%s",
|
||
id
|
||
);
|
||
|
||
(void)snprintf(
|
||
observation.observed_at,
|
||
sizeof(observation.observed_at),
|
||
"%s",
|
||
timestamp
|
||
);
|
||
|
||
draw_shell(
|
||
"Nouvelle mesure — 1/2",
|
||
"Entrée valider · Échap annuler · vide = mesure non faite"
|
||
);
|
||
|
||
row = 4;
|
||
|
||
#define BODY_PROMPT(label_, flag_, value_) \
|
||
do { \
|
||
if (!prompt_optional_double( \
|
||
row++, \
|
||
(label_), \
|
||
&(flag_), \
|
||
&(value_) \
|
||
)) { \
|
||
status_line( \
|
||
"Saisie annulée : aucune mesure enregistrée.", \
|
||
TRAINLOG_COLOR_MUTED \
|
||
); \
|
||
wait_key(); \
|
||
return; \
|
||
} \
|
||
} while (0)
|
||
|
||
BODY_PROMPT(
|
||
"Poids kg : ",
|
||
observation.has_body_weight,
|
||
observation.body_weight_kg
|
||
);
|
||
BODY_PROMPT(
|
||
"Cou cm : ",
|
||
observation.has_neck,
|
||
observation.neck_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Épaules cm : ",
|
||
observation.has_shoulders,
|
||
observation.shoulders_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Poitrine cm : ",
|
||
observation.has_chest,
|
||
observation.chest_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Tour de taille cm : ",
|
||
observation.has_waist,
|
||
observation.waist_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Hanches cm : ",
|
||
observation.has_hips,
|
||
observation.hips_cm
|
||
);
|
||
|
||
draw_shell(
|
||
"Nouvelle mesure — 2/2",
|
||
"Entrée valider · Échap annuler · vide = mesure non faite"
|
||
);
|
||
|
||
row = 4;
|
||
|
||
BODY_PROMPT(
|
||
"Bras gauche cm : ",
|
||
observation.has_left_arm,
|
||
observation.left_arm_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Bras droit cm : ",
|
||
observation.has_right_arm,
|
||
observation.right_arm_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Avant-bras gauche cm : ",
|
||
observation.has_left_forearm,
|
||
observation.left_forearm_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Avant-bras droit cm : ",
|
||
observation.has_right_forearm,
|
||
observation.right_forearm_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Cuisse gauche cm : ",
|
||
observation.has_left_thigh,
|
||
observation.left_thigh_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Cuisse droite cm : ",
|
||
observation.has_right_thigh,
|
||
observation.right_thigh_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Mollet gauche cm : ",
|
||
observation.has_left_calf,
|
||
observation.left_calf_cm
|
||
);
|
||
BODY_PROMPT(
|
||
"Mollet droit cm : ",
|
||
observation.has_right_calf,
|
||
observation.right_calf_cm
|
||
);
|
||
|
||
#undef BODY_PROMPT
|
||
|
||
if (trainlog_database_insert_body_observation(
|
||
database,
|
||
&observation
|
||
) == TRAINLOG_STATUS_OK) {
|
||
status_line(
|
||
"✓ Mesures enregistrées.",
|
||
TRAINLOG_COLOR_SUCCESS
|
||
);
|
||
} else {
|
||
status_line(
|
||
"Aucune mesure valide enregistrée.",
|
||
TRAINLOG_COLOR_WARNING
|
||
);
|
||
}
|
||
|
||
wait_key();
|
||
}
|
||
|
||
/* TRAINLOG_GLOBAL_BODY_OVERLAY_HELPERS */
|
||
|
||
#define MAX_GLOBAL_BODY_DATES \
|
||
(MAX_BODY_METRIC_POINTS * 14U)
|
||
|
||
typedef struct TrainlogGlobalBodySeries {
|
||
TrainlogBodyMetricPoint points[MAX_BODY_METRIC_POINTS];
|
||
size_t count;
|
||
double baseline;
|
||
double latest_percent;
|
||
char symbol;
|
||
TrainlogColorRole role;
|
||
} TrainlogGlobalBodySeries;
|
||
|
||
static const char GLOBAL_BODY_SYMBOLS[] = {
|
||
'P', 'N', 'E', 'C', 'T', 'H', 'A',
|
||
'B', 'F', 'G', 'Q', 'R', 'M', 'D'
|
||
};
|
||
|
||
static const TrainlogColorRole GLOBAL_BODY_ROLES[] = {
|
||
TRAINLOG_COLOR_ACCENT,
|
||
TRAINLOG_COLOR_SUCCESS,
|
||
TRAINLOG_COLOR_WARNING,
|
||
TRAINLOG_COLOR_ERROR,
|
||
TRAINLOG_COLOR_GRAPH,
|
||
TRAINLOG_COLOR_MUTED,
|
||
TRAINLOG_COLOR_ACCENT,
|
||
TRAINLOG_COLOR_SUCCESS,
|
||
TRAINLOG_COLOR_WARNING,
|
||
TRAINLOG_COLOR_ERROR,
|
||
TRAINLOG_COLOR_GRAPH,
|
||
TRAINLOG_COLOR_MUTED,
|
||
TRAINLOG_COLOR_ACCENT,
|
||
TRAINLOG_COLOR_SUCCESS
|
||
};
|
||
|
||
static int global_date_compare(
|
||
const void *left,
|
||
const void *right
|
||
)
|
||
{
|
||
return strcmp(
|
||
(const char *)left,
|
||
(const char *)right
|
||
);
|
||
}
|
||
|
||
static bool global_date_add(
|
||
char dates[MAX_GLOBAL_BODY_DATES][TRAINLOG_TIMESTAMP_MAX + 1U],
|
||
size_t *date_count,
|
||
const char *value
|
||
)
|
||
{
|
||
size_t index;
|
||
|
||
if (dates == NULL ||
|
||
date_count == NULL ||
|
||
value == NULL) {
|
||
return false;
|
||
}
|
||
|
||
for (index = 0U; index < *date_count; ++index) {
|
||
if (strcmp(dates[index], value) == 0) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
if (*date_count >= MAX_GLOBAL_BODY_DATES) {
|
||
return false;
|
||
}
|
||
|
||
(void)snprintf(
|
||
dates[*date_count],
|
||
sizeof(dates[*date_count]),
|
||
"%s",
|
||
value
|
||
);
|
||
|
||
++(*date_count);
|
||
return true;
|
||
}
|
||
|
||
static size_t global_date_index(
|
||
char dates[MAX_GLOBAL_BODY_DATES][TRAINLOG_TIMESTAMP_MAX + 1U],
|
||
size_t date_count,
|
||
const char *value
|
||
)
|
||
{
|
||
size_t low = 0U;
|
||
size_t high = date_count;
|
||
|
||
while (low < high) {
|
||
size_t middle =
|
||
low + ((high - low) / 2U);
|
||
|
||
int comparison =
|
||
strcmp(dates[middle], value);
|
||
|
||
if (comparison < 0) {
|
||
low = middle + 1U;
|
||
} else {
|
||
high = middle;
|
||
}
|
||
}
|
||
|
||
return low < date_count
|
||
? low
|
||
: date_count - 1U;
|
||
}
|
||
|
||
static void global_plot_point(
|
||
int row,
|
||
int column,
|
||
char symbol,
|
||
TrainlogColorRole role
|
||
)
|
||
{
|
||
chtype current;
|
||
chtype character;
|
||
|
||
if (row < 0 ||
|
||
row >= LINES ||
|
||
column < 0 ||
|
||
column >= COLS) {
|
||
return;
|
||
}
|
||
|
||
current =
|
||
mvinch(row, column) &
|
||
A_CHARTEXT;
|
||
|
||
character =
|
||
(chtype)(unsigned char)symbol;
|
||
|
||
if (current != (chtype)' ' &&
|
||
current != (chtype)'.' &&
|
||
current != character) {
|
||
character = (chtype)'#';
|
||
}
|
||
|
||
attron(trainlog_theme_attribute(role));
|
||
mvaddch(row, column, character);
|
||
attroff(trainlog_theme_attribute(role));
|
||
}
|
||
|
||
static void global_plot_segment(
|
||
int x1,
|
||
int y1,
|
||
int x2,
|
||
int y2,
|
||
char symbol,
|
||
TrainlogColorRole role
|
||
)
|
||
{
|
||
int x;
|
||
|
||
if (x2 < x1) {
|
||
return;
|
||
}
|
||
|
||
if (x1 == x2) {
|
||
global_plot_point(
|
||
y2,
|
||
x2,
|
||
symbol,
|
||
role
|
||
);
|
||
return;
|
||
}
|
||
|
||
for (x = x1; x <= x2; ++x) {
|
||
int y =
|
||
y1 +
|
||
(((y2 - y1) * (x - x1)) /
|
||
(x2 - x1));
|
||
|
||
global_plot_point(
|
||
y,
|
||
x,
|
||
symbol,
|
||
role
|
||
);
|
||
}
|
||
}
|
||
|
||
static int normalized_graph_row(
|
||
double value,
|
||
double minimum,
|
||
double maximum,
|
||
int graph_top,
|
||
int graph_height
|
||
)
|
||
{
|
||
double ratio;
|
||
int row;
|
||
|
||
if (maximum <= minimum) {
|
||
return graph_top +
|
||
(graph_height / 2);
|
||
}
|
||
|
||
ratio =
|
||
(value - minimum) /
|
||
(maximum - minimum);
|
||
|
||
row =
|
||
graph_top +
|
||
graph_height -
|
||
1 -
|
||
(int)(
|
||
ratio *
|
||
(double)(graph_height - 1)
|
||
);
|
||
|
||
if (row < graph_top) {
|
||
row = graph_top;
|
||
}
|
||
|
||
if (row >
|
||
graph_top + graph_height - 1) {
|
||
row =
|
||
graph_top +
|
||
graph_height -
|
||
1;
|
||
}
|
||
|
||
return row;
|
||
}
|
||
|
||
static void draw_global_body_overlay(
|
||
TrainlogDatabase *database
|
||
)
|
||
{
|
||
static TrainlogGlobalBodySeries series[14];
|
||
|
||
static char dates[MAX_GLOBAL_BODY_DATES]
|
||
[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||
|
||
const size_t metric_count =
|
||
sizeof(BODY_METRICS) /
|
||
sizeof(BODY_METRICS[0]);
|
||
|
||
size_t date_count = 0U;
|
||
size_t metric_index;
|
||
double minimum = 100.0;
|
||
double maximum = 100.0;
|
||
const int graph_top = 5;
|
||
const int graph_height = 5;
|
||
const int graph_left = 8;
|
||
int graph_width =
|
||
COLS - graph_left - 3;
|
||
|
||
(void)memset(
|
||
series,
|
||
0,
|
||
sizeof(series)
|
||
);
|
||
|
||
(void)memset(
|
||
dates,
|
||
0,
|
||
sizeof(dates)
|
||
);
|
||
|
||
for (metric_index = 0U;
|
||
metric_index < metric_count;
|
||
++metric_index) {
|
||
TrainlogGlobalBodySeries *item =
|
||
&series[metric_index];
|
||
|
||
size_t point_index;
|
||
|
||
item->symbol =
|
||
GLOBAL_BODY_SYMBOLS[metric_index];
|
||
|
||
item->role =
|
||
GLOBAL_BODY_ROLES[metric_index];
|
||
|
||
if (trainlog_database_list_body_metric_points(
|
||
database,
|
||
BODY_METRICS[metric_index].metric,
|
||
item->points,
|
||
MAX_BODY_METRIC_POINTS,
|
||
&item->count
|
||
) != TRAINLOG_STATUS_OK ||
|
||
item->count == 0U) {
|
||
continue;
|
||
}
|
||
|
||
item->baseline =
|
||
item->points[0].value;
|
||
|
||
(void)trainlog_body_percent_change(
|
||
item->baseline,
|
||
item->points[item->count - 1U].value,
|
||
&item->latest_percent
|
||
);
|
||
|
||
for (point_index = 0U;
|
||
point_index < item->count;
|
||
++point_index) {
|
||
double normalized = 100.0;
|
||
|
||
(void)global_date_add(
|
||
dates,
|
||
&date_count,
|
||
item->points[point_index].observed_at
|
||
);
|
||
|
||
if (trainlog_body_index100(
|
||
item->baseline,
|
||
item->points[point_index].value,
|
||
&normalized
|
||
) == TRAINLOG_STATUS_OK) {
|
||
if (normalized < minimum) {
|
||
minimum = normalized;
|
||
}
|
||
|
||
if (normalized > maximum) {
|
||
maximum = normalized;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (date_count == 0U) {
|
||
mvprintw(
|
||
4,
|
||
4,
|
||
"Aucune mensuration disponible pour la vue globale."
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (graph_width < 10) {
|
||
return;
|
||
}
|
||
|
||
qsort(
|
||
dates,
|
||
date_count,
|
||
sizeof(dates[0]),
|
||
global_date_compare
|
||
);
|
||
|
||
mvprintw(
|
||
3,
|
||
4,
|
||
"Vue globale — première mesure de chaque série = 100"
|
||
);
|
||
|
||
if (minimum == maximum) {
|
||
minimum = 99.0;
|
||
maximum = 101.0;
|
||
}
|
||
|
||
mvprintw(
|
||
graph_top,
|
||
2,
|
||
"%.1f",
|
||
maximum
|
||
);
|
||
|
||
mvprintw(
|
||
graph_top + graph_height - 1,
|
||
2,
|
||
"%.1f",
|
||
minimum
|
||
);
|
||
|
||
if (100.0 >= minimum &&
|
||
100.0 <= maximum) {
|
||
int baseline_row =
|
||
normalized_graph_row(
|
||
100.0,
|
||
minimum,
|
||
maximum,
|
||
graph_top,
|
||
graph_height
|
||
);
|
||
|
||
int column;
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
for (column = graph_left;
|
||
column <
|
||
graph_left + graph_width;
|
||
++column) {
|
||
mvaddch(
|
||
baseline_row,
|
||
column,
|
||
(chtype)'.'
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
}
|
||
|
||
for (metric_index = 0U;
|
||
metric_index < metric_count;
|
||
++metric_index) {
|
||
TrainlogGlobalBodySeries *item =
|
||
&series[metric_index];
|
||
|
||
size_t point_index;
|
||
int previous_x = -1;
|
||
int previous_y = -1;
|
||
|
||
if (item->count == 0U) {
|
||
continue;
|
||
}
|
||
|
||
for (point_index = 0U;
|
||
point_index < item->count;
|
||
++point_index) {
|
||
size_t date_index =
|
||
global_date_index(
|
||
dates,
|
||
date_count,
|
||
item->points[point_index].observed_at
|
||
);
|
||
|
||
double normalized = 100.0;
|
||
int x;
|
||
int y;
|
||
|
||
if (trainlog_body_index100(
|
||
item->baseline,
|
||
item->points[point_index].value,
|
||
&normalized
|
||
) != TRAINLOG_STATUS_OK) {
|
||
continue;
|
||
}
|
||
|
||
if (date_count == 1U) {
|
||
x =
|
||
graph_left +
|
||
(graph_width / 2);
|
||
} else {
|
||
x =
|
||
graph_left +
|
||
(int)(
|
||
(date_index *
|
||
(size_t)(graph_width - 1)) /
|
||
(date_count - 1U)
|
||
);
|
||
}
|
||
|
||
y =
|
||
normalized_graph_row(
|
||
normalized,
|
||
minimum,
|
||
maximum,
|
||
graph_top,
|
||
graph_height
|
||
);
|
||
|
||
if (previous_x >= 0 &&
|
||
previous_y >= 0) {
|
||
global_plot_segment(
|
||
previous_x,
|
||
previous_y,
|
||
x,
|
||
y,
|
||
item->symbol,
|
||
item->role
|
||
);
|
||
} else {
|
||
global_plot_point(
|
||
y,
|
||
x,
|
||
item->symbol,
|
||
item->role
|
||
);
|
||
}
|
||
|
||
previous_x = x;
|
||
previous_y = y;
|
||
}
|
||
}
|
||
|
||
{
|
||
size_t row_index = 0U;
|
||
|
||
for (metric_index = 0U;
|
||
metric_index < metric_count;
|
||
++metric_index) {
|
||
TrainlogGlobalBodySeries *item =
|
||
&series[metric_index];
|
||
|
||
int column;
|
||
int row;
|
||
|
||
if (item->count == 0U) {
|
||
continue;
|
||
}
|
||
|
||
row =
|
||
11 +
|
||
(int)(row_index % 7U);
|
||
|
||
column =
|
||
row_index < 7U
|
||
? 4
|
||
: (COLS / 2);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
item->role
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
row,
|
||
column,
|
||
"%c %-16s %+.1f%%",
|
||
item->symbol,
|
||
BODY_METRICS[metric_index].label,
|
||
item->latest_percent
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
item->role
|
||
)
|
||
);
|
||
|
||
++row_index;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_DASHBOARD_GRAPH_ONLY */
|
||
|
||
/* TRAINLOG_DASHBOARD_12_MONTHS */
|
||
|
||
#define DASHBOARD_MONTH_COUNT 12U
|
||
|
||
typedef struct TrainlogDashboardMonth {
|
||
int year;
|
||
int month;
|
||
} TrainlogDashboardMonth;
|
||
|
||
typedef struct TrainlogDashboardMonthValue {
|
||
bool present;
|
||
double value;
|
||
} TrainlogDashboardMonthValue;
|
||
|
||
static bool dashboard_parse_year_month(
|
||
const char *timestamp,
|
||
int *year,
|
||
int *month
|
||
)
|
||
{
|
||
int parsed_year;
|
||
int parsed_month;
|
||
|
||
if (timestamp == NULL ||
|
||
year == NULL ||
|
||
month == NULL ||
|
||
strlen(timestamp) < 7U ||
|
||
timestamp[4] != '-' ||
|
||
timestamp[0] < '0' || timestamp[0] > '9' ||
|
||
timestamp[1] < '0' || timestamp[1] > '9' ||
|
||
timestamp[2] < '0' || timestamp[2] > '9' ||
|
||
timestamp[3] < '0' || timestamp[3] > '9' ||
|
||
timestamp[5] < '0' || timestamp[5] > '9' ||
|
||
timestamp[6] < '0' || timestamp[6] > '9') {
|
||
return false;
|
||
}
|
||
|
||
parsed_year =
|
||
((timestamp[0] - '0') * 1000) +
|
||
((timestamp[1] - '0') * 100) +
|
||
((timestamp[2] - '0') * 10) +
|
||
(timestamp[3] - '0');
|
||
|
||
parsed_month =
|
||
((timestamp[5] - '0') * 10) +
|
||
(timestamp[6] - '0');
|
||
|
||
if (parsed_year < 1 ||
|
||
parsed_month < 1 ||
|
||
parsed_month > 12) {
|
||
return false;
|
||
}
|
||
|
||
*year = parsed_year;
|
||
*month = parsed_month;
|
||
return true;
|
||
}
|
||
|
||
static long dashboard_month_key(
|
||
int year,
|
||
int month
|
||
)
|
||
{
|
||
return ((long)year * 12L) +
|
||
(long)(month - 1);
|
||
}
|
||
|
||
static void dashboard_month_from_key(
|
||
long key,
|
||
TrainlogDashboardMonth *output
|
||
)
|
||
{
|
||
long year;
|
||
long month_zero;
|
||
|
||
year = key / 12L;
|
||
month_zero = key % 12L;
|
||
|
||
if (month_zero < 0L) {
|
||
month_zero += 12L;
|
||
--year;
|
||
}
|
||
|
||
output->year = (int)year;
|
||
output->month = (int)month_zero + 1;
|
||
}
|
||
|
||
static bool dashboard_current_month_key(
|
||
long *output_key
|
||
)
|
||
{
|
||
time_t now;
|
||
struct tm local_time;
|
||
|
||
if (output_key == NULL) {
|
||
return false;
|
||
}
|
||
|
||
now = time(NULL);
|
||
if (now == (time_t)-1) {
|
||
return false;
|
||
}
|
||
|
||
if (localtime_r(&now, &local_time) == NULL) {
|
||
return false;
|
||
}
|
||
|
||
*output_key =
|
||
dashboard_month_key(
|
||
local_time.tm_year + 1900,
|
||
local_time.tm_mon + 1
|
||
);
|
||
|
||
return true;
|
||
}
|
||
|
||
static int dashboard_month_x(
|
||
size_t month_index,
|
||
int graph_left,
|
||
int graph_width
|
||
)
|
||
{
|
||
if (DASHBOARD_MONTH_COUNT <= 1U) {
|
||
return graph_left;
|
||
}
|
||
|
||
return graph_left +
|
||
(int)(
|
||
(month_index *
|
||
(size_t)(graph_width - 1)) /
|
||
(DASHBOARD_MONTH_COUNT - 1U)
|
||
);
|
||
}
|
||
|
||
static void dashboard_draw_month_axis(
|
||
const TrainlogDashboardMonth months[DASHBOARD_MONTH_COUNT],
|
||
int row,
|
||
int graph_left,
|
||
int graph_width
|
||
)
|
||
{
|
||
size_t index;
|
||
int spacing =
|
||
graph_width /
|
||
(int)(DASHBOARD_MONTH_COUNT - 1U);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
for (index = 0U;
|
||
index < DASHBOARD_MONTH_COUNT;
|
||
++index) {
|
||
int x =
|
||
dashboard_month_x(
|
||
index,
|
||
graph_left,
|
||
graph_width
|
||
);
|
||
|
||
if (spacing >= 6) {
|
||
char label[8];
|
||
|
||
(void)snprintf(
|
||
label,
|
||
sizeof(label),
|
||
"%02d/%02d",
|
||
months[index].month,
|
||
months[index].year % 100
|
||
);
|
||
|
||
/*
|
||
* Center the month label on its slot, then clamp it inside
|
||
* the terminal so the first and last visible months are not
|
||
* silently lost at the borders.
|
||
*/
|
||
x -= 2;
|
||
|
||
if (x < 2) {
|
||
x = 2;
|
||
}
|
||
|
||
if (x > COLS - 8) {
|
||
x = COLS - 8;
|
||
}
|
||
|
||
mvprintw(
|
||
row,
|
||
x,
|
||
"%s",
|
||
label
|
||
);
|
||
} else {
|
||
if (x + 2 < COLS - 1) {
|
||
mvprintw(
|
||
row,
|
||
x,
|
||
"%02d",
|
||
months[index].month
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
}
|
||
|
||
/* TRAINLOG_DASHBOARD_ASCII_PANELS */
|
||
|
||
static void dashboard_panel(
|
||
int top,
|
||
int left,
|
||
int bottom,
|
||
int right,
|
||
const char *label
|
||
)
|
||
{
|
||
WINDOW *panel;
|
||
int height;
|
||
int width;
|
||
|
||
if (top < 0 ||
|
||
left < 0 ||
|
||
bottom <= top ||
|
||
right <= left ||
|
||
bottom >= LINES ||
|
||
right >= COLS) {
|
||
return;
|
||
}
|
||
|
||
height = bottom - top + 1;
|
||
width = right - left + 1;
|
||
|
||
panel = derwin(
|
||
stdscr,
|
||
height,
|
||
width,
|
||
top,
|
||
left
|
||
);
|
||
|
||
if (panel == NULL) {
|
||
return;
|
||
}
|
||
|
||
box(panel, 0, 0);
|
||
|
||
if (label != NULL &&
|
||
label[0] != '\0' &&
|
||
width > 8) {
|
||
wattron(
|
||
panel,
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvwprintw(
|
||
panel,
|
||
0,
|
||
2,
|
||
" %.*s ",
|
||
width - 6,
|
||
label
|
||
);
|
||
|
||
wattroff(
|
||
panel,
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
/*
|
||
* derwin() shares the parent screen storage. syncok()+wsyncup() makes
|
||
* the panel border part of stdscr, so later dashboard content and one
|
||
* final refresh() compose cleanly.
|
||
*/
|
||
syncok(panel, TRUE);
|
||
wsyncup(panel);
|
||
delwin(panel);
|
||
}
|
||
|
||
/* TRAINLOG_PRIMARY_TOP_NAVIGATION */
|
||
|
||
/* TRAINLOG_FOCUSED_PANELS */
|
||
|
||
static void focused_panel(
|
||
int top,
|
||
int left,
|
||
int bottom,
|
||
int right,
|
||
const char *label,
|
||
bool active
|
||
)
|
||
{
|
||
WINDOW *panel;
|
||
int height;
|
||
int width;
|
||
chtype border_attribute;
|
||
|
||
if (top < 0 ||
|
||
left < 0 ||
|
||
bottom <= top ||
|
||
right <= left ||
|
||
bottom >= LINES ||
|
||
right >= COLS) {
|
||
return;
|
||
}
|
||
|
||
height = bottom - top + 1;
|
||
width = right - left + 1;
|
||
|
||
panel = derwin(
|
||
stdscr,
|
||
height,
|
||
width,
|
||
top,
|
||
left
|
||
);
|
||
|
||
if (panel == NULL) {
|
||
return;
|
||
}
|
||
|
||
border_attribute =
|
||
active
|
||
? A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_WARNING
|
||
)
|
||
: trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
);
|
||
|
||
wattron(
|
||
panel,
|
||
border_attribute
|
||
);
|
||
|
||
box(panel, 0, 0);
|
||
|
||
if (label != NULL &&
|
||
label[0] != '\0' &&
|
||
width > 8) {
|
||
mvwprintw(
|
||
panel,
|
||
0,
|
||
2,
|
||
" %.*s ",
|
||
width - 6,
|
||
label
|
||
);
|
||
}
|
||
|
||
wattroff(
|
||
panel,
|
||
border_attribute
|
||
);
|
||
|
||
syncok(panel, TRUE);
|
||
wsyncup(panel);
|
||
delwin(panel);
|
||
}
|
||
|
||
static void primary_top_navbar(
|
||
int active_page,
|
||
int selected_page,
|
||
bool focused
|
||
)
|
||
{
|
||
static const char *const labels[] = {
|
||
"0 Accueil",
|
||
"1 Séance",
|
||
"2 Historique",
|
||
"3 Exercices",
|
||
"4 Corps",
|
||
"5 Sync"
|
||
};
|
||
|
||
int column = 5;
|
||
int index;
|
||
|
||
focused_panel(
|
||
8,
|
||
2,
|
||
10,
|
||
COLS - 3,
|
||
"NAVIGATION",
|
||
focused
|
||
);
|
||
|
||
for (index = 0;
|
||
index < 6;
|
||
++index) {
|
||
int width =
|
||
(int)strlen(labels[index]) + 4;
|
||
|
||
if (index == selected_page) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
} else if (index == active_page) {
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
9,
|
||
column,
|
||
" %s ",
|
||
labels[index]
|
||
);
|
||
|
||
if (index == selected_page) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
} else if (index == active_page) {
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
column += width + 2;
|
||
}
|
||
}
|
||
|
||
static bool primary_top_nav_activate(
|
||
int selected_page
|
||
)
|
||
{
|
||
if (selected_page < 0 ||
|
||
selected_page > 5) {
|
||
return false;
|
||
}
|
||
|
||
if (selected_page == 0) {
|
||
return true;
|
||
}
|
||
|
||
return ungetch(
|
||
'0' + selected_page
|
||
) != ERR;
|
||
}
|
||
|
||
/*
|
||
* Primary screens return to the main dispatcher. Requeueing the numeric
|
||
* shortcut lets the dashboard consume it immediately, so cross-page
|
||
* navigation does not recursively nest TUI screens.
|
||
*/
|
||
static bool primary_top_nav_forward(
|
||
int key
|
||
)
|
||
{
|
||
int forwarded = 0;
|
||
|
||
switch (key) {
|
||
case '0':
|
||
case KEY_HOME:
|
||
return true;
|
||
|
||
case KEY_F(1):
|
||
case '1':
|
||
forwarded = '1';
|
||
break;
|
||
|
||
case KEY_F(2):
|
||
case '2':
|
||
forwarded = '2';
|
||
break;
|
||
|
||
case KEY_F(3):
|
||
case '3':
|
||
forwarded = '3';
|
||
break;
|
||
|
||
case KEY_F(4):
|
||
case '4':
|
||
forwarded = '4';
|
||
break;
|
||
|
||
case KEY_F(5):
|
||
case '5':
|
||
forwarded = '5';
|
||
break;
|
||
|
||
default:
|
||
return false;
|
||
}
|
||
|
||
return ungetch(forwarded) != ERR;
|
||
}
|
||
|
||
static void dashboard_ascii_header(void)
|
||
{
|
||
static const char *const logo[] = {
|
||
"TTTTT RRRR AAA IIIII N N L OOO GGG ",
|
||
" T R R A A I NN N L O O G ",
|
||
" T RRRR AAAAA I N N N L O O G GG",
|
||
" T R R A A I N NN L O O G G",
|
||
" T R R A A IIIII N N LLLLL OOO GGG "
|
||
};
|
||
|
||
const size_t line_count =
|
||
sizeof(logo) /
|
||
sizeof(logo[0]);
|
||
|
||
size_t index;
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
for (index = 0U;
|
||
index < line_count;
|
||
++index) {
|
||
int width =
|
||
(int)strlen(logo[index]);
|
||
|
||
int column =
|
||
(COLS - width) / 2;
|
||
|
||
if (column < 2) {
|
||
column = 2;
|
||
}
|
||
|
||
mvprintw(
|
||
1 + (int)index,
|
||
column,
|
||
"%.*s",
|
||
COLS - column - 2,
|
||
logo[index]
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
{
|
||
const char *label =
|
||
":: D A S H B O A R D ::";
|
||
int width =
|
||
(int)strlen(label);
|
||
int column =
|
||
(COLS - width) / 2;
|
||
|
||
if (column < 2) {
|
||
column = 2;
|
||
}
|
||
|
||
mvprintw(
|
||
6,
|
||
column,
|
||
"%s",
|
||
label
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
}
|
||
|
||
static void draw_dashboard_body_graph(
|
||
TrainlogDatabase *database
|
||
)
|
||
{
|
||
static TrainlogGlobalBodySeries series[14];
|
||
|
||
static TrainlogDashboardMonthValue
|
||
monthly[14][DASHBOARD_MONTH_COUNT];
|
||
|
||
TrainlogDashboardMonth
|
||
months[DASHBOARD_MONTH_COUNT];
|
||
|
||
const size_t metric_count =
|
||
sizeof(BODY_METRICS) /
|
||
sizeof(BODY_METRICS[0]);
|
||
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
int graph_panel_top =
|
||
large_layout ? 11 : 2;
|
||
|
||
int graph_panel_bottom =
|
||
large_layout ? 20 : 9;
|
||
|
||
int graph_top =
|
||
graph_panel_top + 1;
|
||
|
||
int graph_height =
|
||
graph_panel_bottom -
|
||
graph_panel_top -
|
||
3;
|
||
|
||
int graph_left =
|
||
large_layout ? 10 : 8;
|
||
|
||
int graph_right =
|
||
COLS - 5;
|
||
|
||
int graph_width =
|
||
graph_right -
|
||
graph_left +
|
||
1;
|
||
|
||
int axis_row =
|
||
graph_panel_bottom - 1;
|
||
|
||
int legend_panel_top =
|
||
large_layout
|
||
? graph_panel_bottom + 1
|
||
: 10;
|
||
|
||
int legend_rows =
|
||
large_layout ? 5 : 6;
|
||
|
||
int legend_panel_bottom =
|
||
legend_panel_top +
|
||
legend_rows +
|
||
1;
|
||
|
||
int legend_top =
|
||
legend_panel_top + 1;
|
||
|
||
size_t metric_index;
|
||
size_t plotted_series = 0U;
|
||
double minimum = 0.0;
|
||
double maximum = 0.0;
|
||
long current_key;
|
||
long first_key;
|
||
|
||
if (large_layout) {
|
||
dashboard_panel(
|
||
graph_panel_top,
|
||
2,
|
||
graph_panel_bottom,
|
||
COLS - 3,
|
||
"EVOLUTION CORPORELLE - 12 MOIS"
|
||
);
|
||
|
||
dashboard_panel(
|
||
legend_panel_top,
|
||
2,
|
||
legend_panel_bottom,
|
||
COLS - 3,
|
||
"MESURES"
|
||
);
|
||
} else {
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
1,
|
||
3,
|
||
"TRAINLOG :: DASHBOARD"
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
if (!dashboard_current_month_key(
|
||
¤t_key
|
||
)) {
|
||
mvprintw(
|
||
graph_top + 1,
|
||
5,
|
||
"Impossible de déterminer le mois courant."
|
||
);
|
||
return;
|
||
}
|
||
|
||
first_key =
|
||
current_key -
|
||
(long)(DASHBOARD_MONTH_COUNT - 1U);
|
||
|
||
for (metric_index = 0U;
|
||
metric_index < DASHBOARD_MONTH_COUNT;
|
||
++metric_index) {
|
||
dashboard_month_from_key(
|
||
first_key + (long)metric_index,
|
||
&months[metric_index]
|
||
);
|
||
}
|
||
|
||
(void)memset(series, 0, sizeof(series));
|
||
(void)memset(monthly, 0, sizeof(monthly));
|
||
|
||
for (metric_index = 0U;
|
||
metric_index < metric_count;
|
||
++metric_index) {
|
||
TrainlogGlobalBodySeries *item =
|
||
&series[metric_index];
|
||
|
||
size_t point_index;
|
||
size_t visible_count = 0U;
|
||
size_t first_visible = 0U;
|
||
size_t latest_visible = 0U;
|
||
bool have_first = false;
|
||
|
||
item->symbol =
|
||
GLOBAL_BODY_SYMBOLS[metric_index];
|
||
|
||
item->role =
|
||
GLOBAL_BODY_ROLES[metric_index];
|
||
|
||
if (trainlog_database_list_body_metric_points(
|
||
database,
|
||
BODY_METRICS[metric_index].metric,
|
||
item->points,
|
||
MAX_BODY_METRIC_POINTS,
|
||
&item->count
|
||
) != TRAINLOG_STATUS_OK ||
|
||
item->count == 0U) {
|
||
continue;
|
||
}
|
||
|
||
/*
|
||
* The dashboard is monthly: multiple observations in one month keep
|
||
* the last actual value, while absent months remain absent.
|
||
*/
|
||
for (point_index = 0U;
|
||
point_index < item->count;
|
||
++point_index) {
|
||
int year;
|
||
int month;
|
||
long key;
|
||
long offset;
|
||
|
||
if (!dashboard_parse_year_month(
|
||
item->points[point_index].observed_at,
|
||
&year,
|
||
&month
|
||
)) {
|
||
continue;
|
||
}
|
||
|
||
key =
|
||
dashboard_month_key(
|
||
year,
|
||
month
|
||
);
|
||
|
||
offset = key - first_key;
|
||
|
||
if (offset < 0L ||
|
||
offset >=
|
||
(long)DASHBOARD_MONTH_COUNT) {
|
||
continue;
|
||
}
|
||
|
||
monthly[metric_index][(size_t)offset]
|
||
.present = true;
|
||
|
||
monthly[metric_index][(size_t)offset]
|
||
.value =
|
||
item->points[point_index].value;
|
||
}
|
||
|
||
for (point_index = 0U;
|
||
point_index < DASHBOARD_MONTH_COUNT;
|
||
++point_index) {
|
||
if (!monthly[metric_index][point_index]
|
||
.present) {
|
||
continue;
|
||
}
|
||
|
||
if (!have_first) {
|
||
first_visible = point_index;
|
||
have_first = true;
|
||
}
|
||
|
||
latest_visible = point_index;
|
||
++visible_count;
|
||
}
|
||
|
||
if (!have_first) {
|
||
item->count = 0U;
|
||
continue;
|
||
}
|
||
|
||
item->baseline =
|
||
monthly[metric_index][first_visible]
|
||
.value;
|
||
|
||
if (trainlog_body_percent_change(
|
||
item->baseline,
|
||
monthly[metric_index][latest_visible]
|
||
.value,
|
||
&item->latest_percent
|
||
) != TRAINLOG_STATUS_OK) {
|
||
item->count = 0U;
|
||
continue;
|
||
}
|
||
|
||
item->count = visible_count;
|
||
|
||
if (visible_count < 2U) {
|
||
continue;
|
||
}
|
||
|
||
++plotted_series;
|
||
|
||
for (point_index = 0U;
|
||
point_index < DASHBOARD_MONTH_COUNT;
|
||
++point_index) {
|
||
double percent = 0.0;
|
||
|
||
if (!monthly[metric_index][point_index]
|
||
.present) {
|
||
continue;
|
||
}
|
||
|
||
if (trainlog_body_percent_change(
|
||
item->baseline,
|
||
monthly[metric_index][point_index]
|
||
.value,
|
||
&percent
|
||
) == TRAINLOG_STATUS_OK) {
|
||
if (percent < minimum) {
|
||
minimum = percent;
|
||
}
|
||
|
||
if (percent > maximum) {
|
||
maximum = percent;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (graph_width < 12 ||
|
||
graph_height < 3) {
|
||
return;
|
||
}
|
||
|
||
if (plotted_series == 0U) {
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
graph_top + 1,
|
||
graph_left,
|
||
"Premières courbes après 2 mois relevés pour une même mesure."
|
||
);
|
||
|
||
mvprintw(
|
||
graph_top + 2,
|
||
graph_left,
|
||
"Un mois sans relevé reste vide."
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
dashboard_draw_month_axis(
|
||
months,
|
||
axis_row,
|
||
graph_left,
|
||
graph_width
|
||
);
|
||
} else {
|
||
if (minimum == maximum) {
|
||
minimum = -1.0;
|
||
maximum = 1.0;
|
||
}
|
||
|
||
mvprintw(
|
||
graph_top,
|
||
3,
|
||
"%+.1f%%",
|
||
maximum
|
||
);
|
||
|
||
mvprintw(
|
||
graph_top + graph_height - 1,
|
||
3,
|
||
"%+.1f%%",
|
||
minimum
|
||
);
|
||
|
||
{
|
||
int zero_row =
|
||
normalized_graph_row(
|
||
0.0,
|
||
minimum,
|
||
maximum,
|
||
graph_top,
|
||
graph_height
|
||
);
|
||
|
||
int column;
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
for (column = graph_left;
|
||
column <= graph_right;
|
||
++column) {
|
||
mvaddch(
|
||
zero_row,
|
||
column,
|
||
(chtype)'.'
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
}
|
||
|
||
for (metric_index = 0U;
|
||
metric_index < metric_count;
|
||
++metric_index) {
|
||
TrainlogGlobalBodySeries *item =
|
||
&series[metric_index];
|
||
|
||
size_t month_index;
|
||
int previous_x = -1;
|
||
int previous_y = -1;
|
||
long previous_month = -2L;
|
||
|
||
if (item->count < 2U) {
|
||
continue;
|
||
}
|
||
|
||
for (month_index = 0U;
|
||
month_index < DASHBOARD_MONTH_COUNT;
|
||
++month_index) {
|
||
double percent = 0.0;
|
||
int x;
|
||
int y;
|
||
|
||
if (!monthly[metric_index][month_index]
|
||
.present) {
|
||
previous_x = -1;
|
||
previous_y = -1;
|
||
previous_month = -2L;
|
||
continue;
|
||
}
|
||
|
||
if (trainlog_body_percent_change(
|
||
item->baseline,
|
||
monthly[metric_index][month_index]
|
||
.value,
|
||
&percent
|
||
) != TRAINLOG_STATUS_OK) {
|
||
continue;
|
||
}
|
||
|
||
x =
|
||
dashboard_month_x(
|
||
month_index,
|
||
graph_left,
|
||
graph_width
|
||
);
|
||
|
||
y =
|
||
normalized_graph_row(
|
||
percent,
|
||
minimum,
|
||
maximum,
|
||
graph_top,
|
||
graph_height
|
||
);
|
||
|
||
if (previous_x >= 0 &&
|
||
previous_y >= 0 &&
|
||
previous_month + 1L ==
|
||
(long)month_index) {
|
||
global_plot_segment(
|
||
previous_x,
|
||
previous_y,
|
||
x,
|
||
y,
|
||
item->symbol,
|
||
item->role
|
||
);
|
||
} else {
|
||
global_plot_point(
|
||
y,
|
||
x,
|
||
item->symbol,
|
||
item->role
|
||
);
|
||
}
|
||
|
||
previous_x = x;
|
||
previous_y = y;
|
||
previous_month =
|
||
(long)month_index;
|
||
}
|
||
}
|
||
|
||
dashboard_draw_month_axis(
|
||
months,
|
||
axis_row,
|
||
graph_left,
|
||
graph_width
|
||
);
|
||
}
|
||
|
||
{
|
||
size_t legend_index = 0U;
|
||
|
||
int columns =
|
||
large_layout ? 3 : 2;
|
||
|
||
int cell_width =
|
||
(COLS - 8) /
|
||
columns;
|
||
|
||
for (metric_index = 0U;
|
||
metric_index < metric_count;
|
||
++metric_index) {
|
||
TrainlogGlobalBodySeries *item =
|
||
&series[metric_index];
|
||
|
||
char label[96];
|
||
char evolution[16];
|
||
int logical_column;
|
||
int logical_row;
|
||
int row;
|
||
int column;
|
||
int label_width;
|
||
size_t month_index;
|
||
size_t latest_month = 0U;
|
||
bool found = false;
|
||
double latest = 0.0;
|
||
|
||
if (item->count == 0U) {
|
||
continue;
|
||
}
|
||
|
||
for (month_index = 0U;
|
||
month_index < DASHBOARD_MONTH_COUNT;
|
||
++month_index) {
|
||
if (monthly[metric_index][month_index]
|
||
.present) {
|
||
latest_month = month_index;
|
||
found = true;
|
||
}
|
||
}
|
||
|
||
if (!found) {
|
||
continue;
|
||
}
|
||
|
||
logical_column =
|
||
(int)(
|
||
legend_index /
|
||
(size_t)legend_rows
|
||
);
|
||
|
||
logical_row =
|
||
(int)(
|
||
legend_index %
|
||
(size_t)legend_rows
|
||
);
|
||
|
||
if (logical_column >= columns) {
|
||
break;
|
||
}
|
||
|
||
latest =
|
||
monthly[metric_index][latest_month]
|
||
.value;
|
||
|
||
row =
|
||
legend_top +
|
||
logical_row;
|
||
|
||
column =
|
||
(large_layout ? 4 : 2) +
|
||
(logical_column * cell_width);
|
||
|
||
label_width =
|
||
cell_width - 18;
|
||
|
||
if (label_width < 7) {
|
||
label_width = 7;
|
||
}
|
||
|
||
(void)snprintf(
|
||
label,
|
||
sizeof(label),
|
||
"%s (%s)",
|
||
BODY_METRICS[metric_index].label,
|
||
BODY_METRICS[metric_index].unit
|
||
);
|
||
|
||
if (item->count < 2U) {
|
||
(void)snprintf(
|
||
evolution,
|
||
sizeof(evolution),
|
||
"%s",
|
||
"réf."
|
||
);
|
||
} else {
|
||
(void)snprintf(
|
||
evolution,
|
||
sizeof(evolution),
|
||
"%+.1f%%",
|
||
item->latest_percent
|
||
);
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
item->role
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
row,
|
||
column,
|
||
"%c %-*.*s %6.1f",
|
||
item->symbol,
|
||
label_width,
|
||
label_width,
|
||
label,
|
||
latest
|
||
);
|
||
|
||
mvprintw(
|
||
row,
|
||
column + cell_width - 7,
|
||
"%6s",
|
||
evolution
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
item->role
|
||
)
|
||
);
|
||
|
||
++legend_index;
|
||
}
|
||
}
|
||
}
|
||
|
||
static DashboardAction screen_dashboard(
|
||
TrainlogDatabase *database
|
||
)
|
||
{
|
||
static const char *const labels[] = {
|
||
"0 Accueil",
|
||
"1 Séance",
|
||
"2 Historique",
|
||
"3 Exercices",
|
||
"4 Corps",
|
||
"5 Sync"
|
||
};
|
||
|
||
static const char *const footer =
|
||
"←→ naviguer Entrée ouvrir 0 accueil F1-F4 accès direct q quitter";
|
||
|
||
int selected = 0;
|
||
|
||
for (;;) {
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
int nav_top =
|
||
large_layout ? 8 : LINES - 4;
|
||
|
||
int nav_bottom =
|
||
large_layout ? 10 : LINES - 3;
|
||
|
||
int key;
|
||
int index;
|
||
int column;
|
||
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
if (large_layout) {
|
||
dashboard_ascii_header();
|
||
|
||
focused_panel(
|
||
nav_top,
|
||
2,
|
||
nav_bottom,
|
||
COLS - 3,
|
||
"NAVIGATION",
|
||
true
|
||
);
|
||
}
|
||
|
||
draw_dashboard_body_graph(database);
|
||
|
||
column =
|
||
large_layout ? 5 : 3;
|
||
|
||
for (index = 0;
|
||
index < 6;
|
||
++index) {
|
||
int width =
|
||
(int)strlen(labels[index]) + 4;
|
||
|
||
if (index == selected) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
large_layout
|
||
? nav_top + 1
|
||
: LINES - 3,
|
||
column,
|
||
" %s ",
|
||
labels[index]
|
||
);
|
||
|
||
if (index == selected) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
column += width + 2;
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
footer
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
switch (key) {
|
||
case KEY_UP:
|
||
case KEY_LEFT:
|
||
selected =
|
||
selected > 0
|
||
? selected - 1
|
||
: 5;
|
||
break;
|
||
|
||
case KEY_DOWN:
|
||
case KEY_RIGHT:
|
||
selected =
|
||
selected < 5
|
||
? selected + 1
|
||
: 0;
|
||
break;
|
||
|
||
case '\n':
|
||
case KEY_ENTER:
|
||
switch (selected) {
|
||
case 0:
|
||
break;
|
||
case 1:
|
||
return DASHBOARD_NEW_SESSION;
|
||
case 2:
|
||
return DASHBOARD_HISTORY;
|
||
case 3:
|
||
return DASHBOARD_EXERCISES;
|
||
case 4:
|
||
return DASHBOARD_BODY;
|
||
case 5:
|
||
return DASHBOARD_SYNC;
|
||
default:
|
||
break;
|
||
}
|
||
break;
|
||
|
||
case '0':
|
||
case KEY_HOME:
|
||
selected = 0;
|
||
break;
|
||
|
||
case KEY_F(1):
|
||
case '1':
|
||
return DASHBOARD_NEW_SESSION;
|
||
|
||
case KEY_F(2):
|
||
case '2':
|
||
return DASHBOARD_HISTORY;
|
||
|
||
case KEY_F(3):
|
||
case '3':
|
||
return DASHBOARD_EXERCISES;
|
||
|
||
case KEY_F(4):
|
||
case '4':
|
||
return DASHBOARD_BODY;
|
||
|
||
case KEY_F(5):
|
||
case '5':
|
||
return DASHBOARD_SYNC;
|
||
|
||
case 'q':
|
||
case 'Q':
|
||
return DASHBOARD_QUIT;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
static bool build_session_exercise(
|
||
TrainlogDatabase *database,
|
||
TrainlogSessionExerciseInput *output,
|
||
TrainlogSetInput *set_storage,
|
||
size_t set_capacity
|
||
)
|
||
{
|
||
TrainlogExercise exercise;
|
||
int load_mode = 1;
|
||
int target_sets = 3;
|
||
int target_metric;
|
||
int rest_seconds = 60;
|
||
int actual_sets = 0;
|
||
int rep_values[MAX_SETS_PER_EXERCISE];
|
||
size_t rep_count = 0U;
|
||
char rep_sequence[512];
|
||
bool target_has_weight = false;
|
||
double target_weight = 0.0;
|
||
size_t set_index;
|
||
|
||
if (!choose_exercise(
|
||
database,
|
||
&exercise
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
(void)memset(
|
||
output,
|
||
0,
|
||
sizeof(*output)
|
||
);
|
||
|
||
(void)snprintf(
|
||
output->exercise_id,
|
||
sizeof(output->exercise_id),
|
||
"%s",
|
||
exercise.exercise_id
|
||
);
|
||
|
||
output->recording_mode =
|
||
exercise.recording_mode;
|
||
|
||
output->data_fields =
|
||
exercise.data_fields;
|
||
|
||
if (exercise.recording_mode ==
|
||
TRAINLOG_RECORDING_CONTINUOUS) {
|
||
int duration_minutes = 30;
|
||
int duration_seconds;
|
||
bool has_value = false;
|
||
double value = 0.0;
|
||
|
||
draw_shell(
|
||
exercise.name,
|
||
"Échap annuler · activité continue"
|
||
);
|
||
|
||
if (!prompt_int_value(
|
||
4,
|
||
"Durée (minutes)",
|
||
1,
|
||
1440,
|
||
duration_minutes,
|
||
&duration_minutes
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
duration_seconds =
|
||
duration_minutes * 60;
|
||
|
||
output->continuous_duration_seconds =
|
||
duration_seconds;
|
||
|
||
output->load_mode =
|
||
TRAINLOG_LOAD_NONE;
|
||
|
||
output->rest_seconds = 0;
|
||
output->target_sets = 0;
|
||
output->target_reps = 0;
|
||
output->target_duration_seconds = 0;
|
||
output->target_has_weight = false;
|
||
output->sets = NULL;
|
||
output->set_count = 0U;
|
||
|
||
if ((exercise.data_fields &
|
||
TRAINLOG_EXERCISE_DATA_SPEED_KMH) != 0U) {
|
||
if (!prompt_optional_double(
|
||
5,
|
||
"Vitesse km/h : ",
|
||
&has_value,
|
||
&value
|
||
) ||
|
||
!has_value) {
|
||
return false;
|
||
}
|
||
|
||
output->continuous_has_speed =
|
||
true;
|
||
|
||
output->continuous_speed_kmh =
|
||
value;
|
||
}
|
||
|
||
if ((exercise.data_fields &
|
||
TRAINLOG_EXERCISE_DATA_DISTANCE_KM) != 0U) {
|
||
has_value = false;
|
||
value = 0.0;
|
||
|
||
if (!prompt_optional_double(
|
||
6,
|
||
"Distance km : ",
|
||
&has_value,
|
||
&value
|
||
) ||
|
||
!has_value) {
|
||
return false;
|
||
}
|
||
|
||
output->continuous_has_distance =
|
||
true;
|
||
|
||
output->continuous_distance_km =
|
||
value;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
target_metric =
|
||
exercise.tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS
|
||
? 10
|
||
: 45;
|
||
|
||
draw_shell(
|
||
exercise.name,
|
||
"Échap annuler · Durées : 90, 90s, 1:30, 1m30, 2m"
|
||
);
|
||
|
||
if (!prompt_int_value(
|
||
4,
|
||
"Charge 1=aucune 2=externe 3=assistance",
|
||
1,
|
||
3,
|
||
1,
|
||
&load_mode
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (load_mode != 1) {
|
||
if (!prompt_optional_double(
|
||
5,
|
||
"Charge cible kg : ",
|
||
&target_has_weight,
|
||
&target_weight
|
||
) ||
|
||
!target_has_weight) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (!prompt_int_value(
|
||
6,
|
||
"Séries prévues",
|
||
1,
|
||
(int)set_capacity,
|
||
3,
|
||
&target_sets
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (exercise.tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS) {
|
||
if (!prompt_int_value(
|
||
7,
|
||
"Répétitions cibles",
|
||
1,
|
||
10000,
|
||
target_metric,
|
||
&target_metric
|
||
)) {
|
||
return false;
|
||
}
|
||
} else {
|
||
if (!prompt_duration_value(
|
||
7,
|
||
"Durée cible",
|
||
1,
|
||
86400,
|
||
target_metric,
|
||
&target_metric
|
||
)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (!prompt_duration_value(
|
||
8,
|
||
"Repos prévu",
|
||
0,
|
||
86400,
|
||
60,
|
||
&rest_seconds
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (
|
||
exercise.tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS
|
||
) {
|
||
for (;;) {
|
||
if (!prompt_text(
|
||
9,
|
||
"Séries réalisées (5x10 | 4,5,6,... | 4..10..4) : ",
|
||
rep_sequence,
|
||
sizeof(rep_sequence),
|
||
false
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (
|
||
trainlog_reps_parse_sequence(
|
||
rep_sequence,
|
||
rep_values,
|
||
set_capacity,
|
||
&rep_count
|
||
) ==
|
||
TRAINLOG_STATUS_OK
|
||
) {
|
||
break;
|
||
}
|
||
|
||
status_line(
|
||
"Séries invalides. Exemples : 5x10 · 4,5,6,7 · 4..10..4",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
|
||
refresh();
|
||
}
|
||
} else {
|
||
if (!prompt_int_value(
|
||
9,
|
||
"Séries réellement faites",
|
||
0,
|
||
(int)set_capacity,
|
||
target_sets,
|
||
&actual_sets
|
||
)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
output->load_mode =
|
||
load_mode == 1
|
||
? TRAINLOG_LOAD_NONE
|
||
: (
|
||
load_mode == 2
|
||
? TRAINLOG_LOAD_EXTERNAL
|
||
: TRAINLOG_LOAD_ASSISTANCE
|
||
);
|
||
|
||
output->rest_seconds = rest_seconds;
|
||
output->target_sets = target_sets;
|
||
|
||
output->target_reps =
|
||
exercise.tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS
|
||
? target_metric
|
||
: 0;
|
||
|
||
output->target_duration_seconds =
|
||
exercise.tracking_mode ==
|
||
TRAINLOG_TRACKING_DURATION
|
||
? target_metric
|
||
: 0;
|
||
|
||
output->target_has_weight =
|
||
target_has_weight;
|
||
|
||
output->target_weight_kg =
|
||
target_weight;
|
||
|
||
output->sets = set_storage;
|
||
output->set_count =
|
||
exercise.tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS
|
||
? rep_count
|
||
: (size_t)actual_sets;
|
||
|
||
for (set_index = 0U;
|
||
set_index < output->set_count;
|
||
++set_index) {
|
||
int actual_metric =
|
||
target_metric;
|
||
|
||
(void)memset(
|
||
&set_storage[set_index],
|
||
0,
|
||
sizeof(set_storage[set_index])
|
||
);
|
||
|
||
if (
|
||
exercise.tracking_mode ==
|
||
TRAINLOG_TRACKING_DURATION ||
|
||
target_has_weight
|
||
) {
|
||
draw_shell(
|
||
exercise.name,
|
||
"Échap annuler · Durées : 90, 90s, 1:30, 1m30, 2m"
|
||
);
|
||
|
||
mvprintw(
|
||
3,
|
||
4,
|
||
"Série %zu / %zu",
|
||
set_index + 1U,
|
||
output->set_count
|
||
);
|
||
}
|
||
|
||
if (
|
||
exercise.tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS
|
||
) {
|
||
actual_metric =
|
||
rep_values[set_index];
|
||
|
||
set_storage[set_index].reps =
|
||
actual_metric;
|
||
} else {
|
||
if (!prompt_duration_value(
|
||
5,
|
||
"Durée réalisée",
|
||
1,
|
||
86400,
|
||
target_metric,
|
||
&actual_metric
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
set_storage[
|
||
set_index
|
||
].duration_seconds =
|
||
actual_metric;
|
||
}
|
||
|
||
if (target_has_weight) {
|
||
char buffer[64];
|
||
char prompt[128];
|
||
double actual_weight =
|
||
target_weight;
|
||
|
||
(void)snprintf(
|
||
prompt,
|
||
sizeof(prompt),
|
||
"Charge kg [%.1f] : ",
|
||
target_weight
|
||
);
|
||
|
||
if (!prompt_text(
|
||
6,
|
||
prompt,
|
||
buffer,
|
||
sizeof(buffer),
|
||
true
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (buffer[0] != '\0' &&
|
||
!parse_double_positive(
|
||
buffer,
|
||
&actual_weight
|
||
)) {
|
||
status_line(
|
||
"Charge invalide.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
|
||
wait_key();
|
||
return false;
|
||
}
|
||
|
||
set_storage[
|
||
set_index
|
||
].has_weight =
|
||
true;
|
||
|
||
set_storage[
|
||
set_index
|
||
].weight_kg =
|
||
actual_weight;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/* TRAINLOG_SESSION_TYPE_TUI */
|
||
|
||
static const char *session_type_label(
|
||
TrainlogSessionType type
|
||
)
|
||
{
|
||
switch (type) {
|
||
case TRAINLOG_SESSION_MAX_TEST:
|
||
return "Test de max";
|
||
|
||
case TRAINLOG_SESSION_TRAINING:
|
||
default:
|
||
return "Entraînement";
|
||
}
|
||
}
|
||
|
||
static const char *session_type_history_label(
|
||
TrainlogSessionType type
|
||
)
|
||
{
|
||
return type == TRAINLOG_SESSION_MAX_TEST
|
||
? "[MAX]"
|
||
: "[ENTRAINEMENT]";
|
||
}
|
||
|
||
static bool choose_session_type(
|
||
TrainlogSessionType *output
|
||
)
|
||
{
|
||
int selected = 0;
|
||
int nav_selected = 1;
|
||
int focus = 1;
|
||
|
||
if (output == NULL) {
|
||
return false;
|
||
}
|
||
|
||
for (;;) {
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
int key;
|
||
|
||
if (large_layout) {
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
section_ascii_header(
|
||
":: N O U V E L L E S E A N C E ::"
|
||
);
|
||
|
||
primary_top_navbar(
|
||
1,
|
||
nav_selected,
|
||
focus == 0
|
||
);
|
||
|
||
focused_panel(
|
||
11,
|
||
4,
|
||
20,
|
||
COLS - 5,
|
||
"TYPE DE SEANCE",
|
||
focus == 1
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"Tab zone ←→ menu ↑↓ type Entrée valider 0/Home accueil F2-F4 direct Échap annuler"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
if (focus == 1 &&
|
||
selected == 0) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
13,
|
||
7,
|
||
" Entraînement "
|
||
);
|
||
|
||
if (focus == 1 &&
|
||
selected == 0) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
14,
|
||
9,
|
||
"Séance normale : progression, volume, travail courant."
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
if (focus == 1 &&
|
||
selected == 1) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
17,
|
||
7,
|
||
" Test de max "
|
||
);
|
||
|
||
if (focus == 1 &&
|
||
selected == 1) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
18,
|
||
9,
|
||
"Séance explicitement dédiée aux mesures de max."
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
} else {
|
||
WINDOW *panel;
|
||
int panel_width =
|
||
COLS - 8;
|
||
|
||
draw_shell(
|
||
"TRAINLOG — Nouvelle séance",
|
||
"↑↓ choisir Entrée valider Échap annuler"
|
||
);
|
||
|
||
if (panel_width < 40) {
|
||
panel_width = 40;
|
||
}
|
||
|
||
panel = derwin(
|
||
stdscr,
|
||
9,
|
||
panel_width,
|
||
3,
|
||
4
|
||
);
|
||
|
||
if (panel == NULL) {
|
||
return false;
|
||
}
|
||
|
||
box(panel, 0, 0);
|
||
|
||
wattron(
|
||
panel,
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvwprintw(
|
||
panel,
|
||
0,
|
||
2,
|
||
" TYPE DE SEANCE "
|
||
);
|
||
|
||
wattroff(
|
||
panel,
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
if (selected == 0) {
|
||
wattron(
|
||
panel,
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvwprintw(
|
||
panel,
|
||
2,
|
||
3,
|
||
" Entraînement "
|
||
);
|
||
|
||
if (selected == 0) {
|
||
wattroff(
|
||
panel,
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvwprintw(
|
||
panel,
|
||
3,
|
||
5,
|
||
"Séance normale : progression, volume, travail courant."
|
||
);
|
||
|
||
if (selected == 1) {
|
||
wattron(
|
||
panel,
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvwprintw(
|
||
panel,
|
||
5,
|
||
3,
|
||
" Test de max "
|
||
);
|
||
|
||
if (selected == 1) {
|
||
wattroff(
|
||
panel,
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvwprintw(
|
||
panel,
|
||
6,
|
||
5,
|
||
"Séance explicitement dédiée aux mesures de max."
|
||
);
|
||
|
||
syncok(panel, TRUE);
|
||
wsyncup(panel);
|
||
delwin(panel);
|
||
}
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (large_layout &&
|
||
(key == '\t' ||
|
||
key == KEY_BTAB)) {
|
||
focus =
|
||
focus == 0
|
||
? 1
|
||
: 0;
|
||
continue;
|
||
}
|
||
|
||
if (key == 27) {
|
||
return false;
|
||
}
|
||
|
||
if (large_layout &&
|
||
(key == '0' ||
|
||
key == KEY_HOME)) {
|
||
return false;
|
||
}
|
||
|
||
if (large_layout &&
|
||
(key == '2' ||
|
||
key == KEY_F(2) ||
|
||
key == '3' ||
|
||
key == KEY_F(3) ||
|
||
key == '4' ||
|
||
key == KEY_F(4) ||
|
||
key == '5' ||
|
||
key == KEY_F(5))) {
|
||
if (primary_top_nav_forward(key)) {
|
||
return false;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (large_layout &&
|
||
focus == 0) {
|
||
if (key == KEY_LEFT) {
|
||
nav_selected =
|
||
nav_selected > 0
|
||
? nav_selected - 1
|
||
: 5;
|
||
} else if (key == KEY_RIGHT) {
|
||
nav_selected =
|
||
nav_selected < 5
|
||
? nav_selected + 1
|
||
: 0;
|
||
} else if (
|
||
key == '\n' ||
|
||
key == KEY_ENTER
|
||
) {
|
||
if (nav_selected == 1) {
|
||
focus = 1;
|
||
} else if (
|
||
primary_top_nav_activate(
|
||
nav_selected
|
||
)
|
||
) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_UP ||
|
||
key == KEY_LEFT ||
|
||
key == KEY_DOWN ||
|
||
key == KEY_RIGHT) {
|
||
selected =
|
||
selected == 0
|
||
? 1
|
||
: 0;
|
||
continue;
|
||
}
|
||
|
||
if (key == '\n' ||
|
||
key == KEY_ENTER) {
|
||
*output =
|
||
selected == 0
|
||
? TRAINLOG_SESSION_TRAINING
|
||
: TRAINLOG_SESSION_MAX_TEST;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_SESSION_EDIT_TUI */
|
||
|
||
typedef struct TrainlogSessionDraftExercise {
|
||
TrainlogSessionExerciseInput input;
|
||
TrainlogSetInput sets[MAX_SETS_PER_EXERCISE];
|
||
char name[TRAINLOG_NAME_MAX + 1U];
|
||
TrainlogTrackingMode tracking_mode;
|
||
char notes[TRAINLOG_NOTE_MAX + 1U];
|
||
} TrainlogSessionDraftExercise;
|
||
|
||
static bool draft_lookup_exercise(
|
||
TrainlogDatabase *database,
|
||
const char *exercise_id,
|
||
char *output_name,
|
||
size_t output_name_size,
|
||
TrainlogTrackingMode *output_tracking
|
||
)
|
||
{
|
||
TrainlogExercise exercises[MAX_EXERCISES];
|
||
size_t count = 0U;
|
||
size_t index;
|
||
|
||
if (database == NULL ||
|
||
exercise_id == NULL ||
|
||
output_name == NULL ||
|
||
output_name_size == 0U ||
|
||
output_tracking == NULL) {
|
||
return false;
|
||
}
|
||
|
||
if (trainlog_database_list_exercises(
|
||
database,
|
||
exercises,
|
||
MAX_EXERCISES,
|
||
&count
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return false;
|
||
}
|
||
|
||
for (index = 0U;
|
||
index < count;
|
||
++index) {
|
||
if (strcmp(
|
||
exercises[index].exercise_id,
|
||
exercise_id
|
||
) == 0) {
|
||
(void)snprintf(
|
||
output_name,
|
||
output_name_size,
|
||
"%s",
|
||
exercises[index].name
|
||
);
|
||
|
||
*output_tracking =
|
||
exercises[index].tracking_mode;
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
static bool draft_build_exercise(
|
||
TrainlogDatabase *database,
|
||
TrainlogSessionDraftExercise *draft,
|
||
const char *preserved_notes
|
||
)
|
||
{
|
||
TrainlogSessionExerciseInput input;
|
||
TrainlogSetInput sets[MAX_SETS_PER_EXERCISE];
|
||
|
||
if (database == NULL ||
|
||
draft == NULL) {
|
||
return false;
|
||
}
|
||
|
||
(void)memset(
|
||
&input,
|
||
0,
|
||
sizeof(input)
|
||
);
|
||
|
||
(void)memset(
|
||
sets,
|
||
0,
|
||
sizeof(sets)
|
||
);
|
||
|
||
if (!build_session_exercise(
|
||
database,
|
||
&input,
|
||
sets,
|
||
MAX_SETS_PER_EXERCISE
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
(void)memset(
|
||
draft,
|
||
0,
|
||
sizeof(*draft)
|
||
);
|
||
|
||
draft->input = input;
|
||
|
||
if (input.set_count >
|
||
MAX_SETS_PER_EXERCISE) {
|
||
return false;
|
||
}
|
||
|
||
(void)memcpy(
|
||
draft->sets,
|
||
sets,
|
||
input.set_count *
|
||
sizeof(draft->sets[0])
|
||
);
|
||
|
||
draft->input.sets =
|
||
draft->sets;
|
||
|
||
if (preserved_notes != NULL &&
|
||
preserved_notes[0] != '\0') {
|
||
(void)snprintf(
|
||
draft->notes,
|
||
sizeof(draft->notes),
|
||
"%s",
|
||
preserved_notes
|
||
);
|
||
|
||
draft->input.notes =
|
||
draft->notes;
|
||
} else {
|
||
draft->notes[0] = '\0';
|
||
draft->input.notes = NULL;
|
||
}
|
||
|
||
if (!draft_lookup_exercise(
|
||
database,
|
||
draft->input.exercise_id,
|
||
draft->name,
|
||
sizeof(draft->name),
|
||
&draft->tracking_mode
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
static void draft_bind_input(
|
||
TrainlogSessionDraftExercise *draft
|
||
)
|
||
{
|
||
if (draft == NULL) {
|
||
return;
|
||
}
|
||
|
||
draft->input.sets =
|
||
draft->sets;
|
||
|
||
draft->input.notes =
|
||
draft->notes[0] != '\0'
|
||
? draft->notes
|
||
: NULL;
|
||
}
|
||
|
||
static void draft_delete_exercise(
|
||
TrainlogSessionDraftExercise *drafts,
|
||
size_t *count,
|
||
size_t selected
|
||
)
|
||
{
|
||
size_t index;
|
||
|
||
if (drafts == NULL ||
|
||
count == NULL ||
|
||
selected >= *count) {
|
||
return;
|
||
}
|
||
|
||
for (index = selected;
|
||
index + 1U < *count;
|
||
++index) {
|
||
drafts[index] =
|
||
drafts[index + 1U];
|
||
|
||
draft_bind_input(
|
||
&drafts[index]
|
||
);
|
||
}
|
||
|
||
--(*count);
|
||
|
||
if (*count < MAX_SESSION_EXERCISES) {
|
||
(void)memset(
|
||
&drafts[*count],
|
||
0,
|
||
sizeof(drafts[*count])
|
||
);
|
||
}
|
||
}
|
||
|
||
static void draft_set_summary(
|
||
const TrainlogSessionDraftExercise *draft,
|
||
char *output,
|
||
size_t output_size
|
||
)
|
||
{
|
||
if (draft == NULL ||
|
||
output == NULL ||
|
||
output_size == 0U) {
|
||
return;
|
||
}
|
||
|
||
if (draft->input.load_mode ==
|
||
TRAINLOG_LOAD_EXTERNAL) {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%zu séries · %.1f kg",
|
||
draft->input.set_count,
|
||
draft->input.target_weight_kg
|
||
);
|
||
} else if (
|
||
draft->input.load_mode ==
|
||
TRAINLOG_LOAD_ASSISTANCE
|
||
) {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%zu séries · %.1f kg aide",
|
||
draft->input.set_count,
|
||
draft->input.target_weight_kg
|
||
);
|
||
} else {
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%zu séries",
|
||
draft->input.set_count
|
||
);
|
||
}
|
||
}
|
||
|
||
static bool confirm_draft_delete(
|
||
const char *name
|
||
)
|
||
{
|
||
int answer = 0;
|
||
|
||
draw_shell(
|
||
"Supprimer de la séance ?",
|
||
"1 confirmer 0 annuler"
|
||
);
|
||
|
||
mvprintw(
|
||
4,
|
||
4,
|
||
"%s",
|
||
name != NULL
|
||
? name
|
||
: "Exercice"
|
||
);
|
||
|
||
if (!prompt_int_value(
|
||
6,
|
||
"Supprimer ? 1=oui 0=non",
|
||
0,
|
||
1,
|
||
0,
|
||
&answer
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
return answer == 1;
|
||
}
|
||
|
||
static bool edit_session_draft(
|
||
TrainlogDatabase *database,
|
||
TrainlogSessionDraftExercise drafts[MAX_SESSION_EXERCISES],
|
||
size_t *count,
|
||
TrainlogSessionType session_type
|
||
)
|
||
{
|
||
size_t selected = 0U;
|
||
|
||
if (database == NULL ||
|
||
drafts == NULL ||
|
||
count == NULL) {
|
||
return false;
|
||
}
|
||
|
||
for (;;) {
|
||
size_t index;
|
||
size_t top = 0U;
|
||
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
int frame_top =
|
||
large_layout ? 8 : 3;
|
||
|
||
int frame_bottom =
|
||
LINES - 4;
|
||
|
||
int first_row =
|
||
frame_top + 3;
|
||
|
||
int visible_rows =
|
||
frame_bottom -
|
||
first_row;
|
||
|
||
int key;
|
||
|
||
if (visible_rows < 1) {
|
||
return false;
|
||
}
|
||
|
||
if (*count > 0U &&
|
||
selected >= *count) {
|
||
selected =
|
||
*count - 1U;
|
||
}
|
||
|
||
if (*count > 0U &&
|
||
selected >=
|
||
(size_t)visible_rows) {
|
||
top =
|
||
selected -
|
||
(size_t)visible_rows +
|
||
1U;
|
||
}
|
||
|
||
if (large_layout) {
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
section_ascii_header(
|
||
":: S E A N C E E N C O U R S ::"
|
||
);
|
||
|
||
focused_panel(
|
||
frame_top,
|
||
2,
|
||
frame_bottom,
|
||
COLS - 3,
|
||
"RESUME AVANT ENREGISTREMENT",
|
||
true
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"↑↓ choisir e/Entrée modifier a ajouter d supprimer f enregistrer q/Échap abandonner"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"TRAINLOG — Séance en cours",
|
||
"↑↓ choisir e/Entrée modifier a ajouter d supprimer f enregistrer q/Échap abandonner"
|
||
);
|
||
}
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
frame_top + 1,
|
||
large_layout ? 5 : 4,
|
||
"Type : %s",
|
||
session_type_label(
|
||
session_type
|
||
)
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
if (*count == 0U) {
|
||
mvprintw(
|
||
first_row + 1,
|
||
large_layout ? 5 : 4,
|
||
"Aucun exercice saisi. a = ajouter."
|
||
);
|
||
}
|
||
|
||
for (index = 0U;
|
||
index < (size_t)visible_rows &&
|
||
top + index < *count;
|
||
++index) {
|
||
size_t absolute =
|
||
top + index;
|
||
|
||
char summary[96];
|
||
|
||
int row =
|
||
first_row +
|
||
(int)index;
|
||
|
||
draft_set_summary(
|
||
&drafts[absolute],
|
||
summary,
|
||
sizeof(summary)
|
||
);
|
||
|
||
if (absolute == selected) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
row,
|
||
large_layout ? 5 : 4,
|
||
" %2zu %-36.36s %-28.28s ",
|
||
absolute + 1U,
|
||
drafts[absolute].name,
|
||
summary
|
||
);
|
||
|
||
if (absolute == selected) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (key == 'q' ||
|
||
key == 'Q' ||
|
||
key == 27) {
|
||
return false;
|
||
}
|
||
|
||
if (key == 'f' ||
|
||
key == 'F') {
|
||
if (*count == 0U) {
|
||
status_line(
|
||
"Ajoutez au moins un exercice avant d'enregistrer.",
|
||
TRAINLOG_COLOR_WARNING
|
||
);
|
||
|
||
refresh();
|
||
(void)getch();
|
||
continue;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
if (key == KEY_UP &&
|
||
*count > 0U) {
|
||
selected =
|
||
selected > 0U
|
||
? selected - 1U
|
||
: *count - 1U;
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_DOWN &&
|
||
*count > 0U) {
|
||
selected =
|
||
selected + 1U < *count
|
||
? selected + 1U
|
||
: 0U;
|
||
continue;
|
||
}
|
||
|
||
if (key == 'a' ||
|
||
key == 'A') {
|
||
if (*count >=
|
||
MAX_SESSION_EXERCISES) {
|
||
status_line(
|
||
"Nombre maximal d'exercices atteint.",
|
||
TRAINLOG_COLOR_WARNING
|
||
);
|
||
|
||
refresh();
|
||
(void)getch();
|
||
continue;
|
||
}
|
||
|
||
if (draft_build_exercise(
|
||
database,
|
||
&drafts[*count],
|
||
NULL
|
||
)) {
|
||
selected = *count;
|
||
++(*count);
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if ((key == 'e' ||
|
||
key == 'E' ||
|
||
key == '\n' ||
|
||
key == KEY_ENTER) &&
|
||
*count > 0U) {
|
||
TrainlogSessionDraftExercise replacement;
|
||
|
||
if (draft_build_exercise(
|
||
database,
|
||
&replacement,
|
||
drafts[selected].notes
|
||
)) {
|
||
drafts[selected] =
|
||
replacement;
|
||
|
||
draft_bind_input(
|
||
&drafts[selected]
|
||
);
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if ((key == 'd' ||
|
||
key == 'D') &&
|
||
*count > 0U) {
|
||
if (confirm_draft_delete(
|
||
drafts[selected].name
|
||
)) {
|
||
draft_delete_exercise(
|
||
drafts,
|
||
count,
|
||
selected
|
||
);
|
||
|
||
if (*count > 0U &&
|
||
selected >= *count) {
|
||
selected =
|
||
*count - 1U;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
static bool load_persisted_draft(
|
||
TrainlogDatabase *database,
|
||
const char *session_id,
|
||
TrainlogSessionSummary *session,
|
||
TrainlogSessionDraftExercise drafts[MAX_SESSION_EXERCISES],
|
||
size_t *draft_count
|
||
)
|
||
{
|
||
TrainlogEditableExerciseRecord
|
||
records[MAX_SESSION_EXERCISES];
|
||
|
||
TrainlogSetInput
|
||
sets[MAX_SESSION_EXERCISES *
|
||
MAX_SETS_PER_EXERCISE];
|
||
|
||
size_t record_count = 0U;
|
||
size_t set_count = 0U;
|
||
size_t index;
|
||
|
||
if (database == NULL ||
|
||
session_id == NULL ||
|
||
session == NULL ||
|
||
drafts == NULL ||
|
||
draft_count == NULL) {
|
||
return false;
|
||
}
|
||
|
||
if (trainlog_database_load_session_editable(
|
||
database,
|
||
session_id,
|
||
session,
|
||
records,
|
||
MAX_SESSION_EXERCISES,
|
||
&record_count,
|
||
sets,
|
||
MAX_SESSION_EXERCISES *
|
||
MAX_SETS_PER_EXERCISE,
|
||
&set_count
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return false;
|
||
}
|
||
|
||
(void)set_count;
|
||
|
||
(void)memset(
|
||
drafts,
|
||
0,
|
||
sizeof(*drafts) *
|
||
MAX_SESSION_EXERCISES
|
||
);
|
||
|
||
for (index = 0U;
|
||
index < record_count;
|
||
++index) {
|
||
TrainlogSessionDraftExercise *draft =
|
||
&drafts[index];
|
||
|
||
const TrainlogEditableExerciseRecord *record =
|
||
&records[index];
|
||
|
||
if (record->set_count >
|
||
MAX_SETS_PER_EXERCISE) {
|
||
return false;
|
||
}
|
||
|
||
(void)snprintf(
|
||
draft->input.exercise_id,
|
||
sizeof(draft->input.exercise_id),
|
||
"%s",
|
||
record->exercise_id
|
||
);
|
||
|
||
(void)snprintf(
|
||
draft->name,
|
||
sizeof(draft->name),
|
||
"%s",
|
||
record->name
|
||
);
|
||
|
||
draft->tracking_mode =
|
||
record->tracking_mode;
|
||
|
||
draft->input.load_mode =
|
||
record->load_mode;
|
||
|
||
draft->input.rest_seconds =
|
||
record->rest_seconds;
|
||
|
||
draft->input.target_sets =
|
||
record->target_sets;
|
||
|
||
draft->input.target_reps =
|
||
record->target_reps;
|
||
|
||
draft->input.target_duration_seconds =
|
||
record->target_duration_seconds;
|
||
|
||
draft->input.target_has_weight =
|
||
record->has_target_weight != 0;
|
||
|
||
draft->input.target_weight_kg =
|
||
record->target_weight_kg;
|
||
|
||
(void)snprintf(
|
||
draft->notes,
|
||
sizeof(draft->notes),
|
||
"%s",
|
||
record->notes
|
||
);
|
||
|
||
if (record->set_count > 0U) {
|
||
(void)memcpy(
|
||
draft->sets,
|
||
&sets[record->set_offset],
|
||
record->set_count *
|
||
sizeof(draft->sets[0])
|
||
);
|
||
}
|
||
|
||
draft->input.set_count =
|
||
record->set_count;
|
||
|
||
draft_bind_input(draft);
|
||
}
|
||
|
||
*draft_count =
|
||
record_count;
|
||
|
||
return true;
|
||
}
|
||
|
||
static bool persist_draft_replacement(
|
||
TrainlogDatabase *database,
|
||
const char *session_id,
|
||
TrainlogSessionDraftExercise drafts[MAX_SESSION_EXERCISES],
|
||
size_t count
|
||
)
|
||
{
|
||
TrainlogSessionExerciseInput
|
||
inputs[MAX_SESSION_EXERCISES];
|
||
|
||
size_t index;
|
||
|
||
for (index = 0U;
|
||
index < count;
|
||
++index) {
|
||
draft_bind_input(
|
||
&drafts[index]
|
||
);
|
||
|
||
inputs[index] =
|
||
drafts[index].input;
|
||
}
|
||
|
||
return trainlog_database_replace_session_exercises(
|
||
database,
|
||
session_id,
|
||
inputs,
|
||
count
|
||
) == TRAINLOG_STATUS_OK;
|
||
}
|
||
|
||
static void edit_persisted_session(
|
||
TrainlogDatabase *database,
|
||
const char *session_id
|
||
)
|
||
{
|
||
TrainlogSessionSummary session;
|
||
TrainlogSessionDraftExercise
|
||
drafts[MAX_SESSION_EXERCISES];
|
||
|
||
size_t count = 0U;
|
||
|
||
if (!load_persisted_draft(
|
||
database,
|
||
session_id,
|
||
&session,
|
||
drafts,
|
||
&count
|
||
)) {
|
||
status_line(
|
||
"Impossible de charger la séance à modifier.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
|
||
wait_key();
|
||
return;
|
||
}
|
||
|
||
if (!edit_session_draft(
|
||
database,
|
||
drafts,
|
||
&count,
|
||
session.session_type
|
||
)) {
|
||
status_line(
|
||
"Modification abandonnée : aucune donnée changée.",
|
||
TRAINLOG_COLOR_MUTED
|
||
);
|
||
|
||
wait_key();
|
||
return;
|
||
}
|
||
|
||
if (persist_draft_replacement(
|
||
database,
|
||
session_id,
|
||
drafts,
|
||
count
|
||
)) {
|
||
status_line(
|
||
"✓ Séance corrigée.",
|
||
TRAINLOG_COLOR_SUCCESS
|
||
);
|
||
} else {
|
||
status_line(
|
||
"Échec de la correction : ancienne séance conservée.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
}
|
||
|
||
wait_key();
|
||
}
|
||
|
||
static void screen_new_session(
|
||
TrainlogDatabase *database
|
||
)
|
||
{
|
||
TrainlogSessionDraftExercise
|
||
drafts[MAX_SESSION_EXERCISES];
|
||
|
||
TrainlogSessionExerciseInput
|
||
exercise_inputs[MAX_SESSION_EXERCISES];
|
||
|
||
TrainlogSessionInput session;
|
||
char session_id[TRAINLOG_GENERATED_ID_CAPACITY];
|
||
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||
|
||
TrainlogSessionType session_type =
|
||
TRAINLOG_SESSION_TRAINING;
|
||
|
||
size_t catalog_count = 0U;
|
||
size_t exercise_count = 0U;
|
||
size_t index;
|
||
TrainlogStatus status;
|
||
|
||
if (trainlog_database_exercise_count(
|
||
database,
|
||
&catalog_count
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return;
|
||
}
|
||
|
||
if (catalog_count == 0U) {
|
||
draw_shell(
|
||
"Nouvelle séance",
|
||
"Une touche pour revenir"
|
||
);
|
||
|
||
status_line(
|
||
"Ajoutez d'abord au moins un exercice.",
|
||
TRAINLOG_COLOR_WARNING
|
||
);
|
||
|
||
wait_key();
|
||
return;
|
||
}
|
||
|
||
if (!choose_session_type(
|
||
&session_type
|
||
)) {
|
||
return;
|
||
}
|
||
|
||
if (trainlog_id_generate(
|
||
"se",
|
||
session_id,
|
||
sizeof(session_id)
|
||
) != TRAINLOG_STATUS_OK ||
|
||
trainlog_time_now_rfc3339(
|
||
started_at,
|
||
sizeof(started_at)
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return;
|
||
}
|
||
|
||
(void)memset(
|
||
drafts,
|
||
0,
|
||
sizeof(drafts)
|
||
);
|
||
|
||
/*
|
||
* Start with the first exercise entry immediately. If the user cancels
|
||
* that form, nothing has been committed and the review screen still lets
|
||
* them add another exercise or abandon the whole session.
|
||
*/
|
||
if (draft_build_exercise(
|
||
database,
|
||
&drafts[0],
|
||
NULL
|
||
)) {
|
||
exercise_count = 1U;
|
||
}
|
||
|
||
if (!edit_session_draft(
|
||
database,
|
||
drafts,
|
||
&exercise_count,
|
||
session_type
|
||
)) {
|
||
draw_shell(
|
||
"Séance abandonnée",
|
||
"Une touche pour revenir"
|
||
);
|
||
|
||
status_line(
|
||
"Aucune donnée de séance n'a été enregistrée.",
|
||
TRAINLOG_COLOR_MUTED
|
||
);
|
||
|
||
wait_key();
|
||
return;
|
||
}
|
||
|
||
if (trainlog_time_now_rfc3339(
|
||
ended_at,
|
||
sizeof(ended_at)
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return;
|
||
}
|
||
|
||
for (index = 0U;
|
||
index < exercise_count;
|
||
++index) {
|
||
draft_bind_input(
|
||
&drafts[index]
|
||
);
|
||
|
||
exercise_inputs[index] =
|
||
drafts[index].input;
|
||
}
|
||
|
||
(void)memset(
|
||
&session,
|
||
0,
|
||
sizeof(session)
|
||
);
|
||
|
||
session.session_type =
|
||
session_type;
|
||
|
||
(void)snprintf(
|
||
session.session_id,
|
||
sizeof(session.session_id),
|
||
"%s",
|
||
session_id
|
||
);
|
||
|
||
(void)snprintf(
|
||
session.started_at,
|
||
sizeof(session.started_at),
|
||
"%s",
|
||
started_at
|
||
);
|
||
|
||
(void)snprintf(
|
||
session.ended_at,
|
||
sizeof(session.ended_at),
|
||
"%s",
|
||
ended_at
|
||
);
|
||
|
||
session.exercises =
|
||
exercise_inputs;
|
||
|
||
session.exercise_count =
|
||
exercise_count;
|
||
|
||
status =
|
||
trainlog_database_insert_session(
|
||
database,
|
||
&session
|
||
);
|
||
|
||
draw_shell(
|
||
"Fin de séance",
|
||
"Une touche pour revenir"
|
||
);
|
||
|
||
if (status == TRAINLOG_STATUS_OK) {
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_SUCCESS
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
4,
|
||
2,
|
||
"✓ Séance enregistrée."
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_SUCCESS
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
6,
|
||
2,
|
||
"Début : %s",
|
||
started_at
|
||
);
|
||
|
||
mvprintw(
|
||
7,
|
||
2,
|
||
"Fin : %s",
|
||
ended_at
|
||
);
|
||
|
||
mvprintw(
|
||
8,
|
||
2,
|
||
"Type : %s",
|
||
session_type_label(
|
||
session_type
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
9,
|
||
2,
|
||
"Exercices : %zu",
|
||
exercise_count
|
||
);
|
||
} else {
|
||
status_line(
|
||
"Échec lors de l'enregistrement de la séance.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
}
|
||
|
||
wait_key();
|
||
}
|
||
|
||
/* TRAINLOG_SESSION_DETAILS_SCREEN */
|
||
|
||
static const char *session_detail_load_label(TrainlogLoadMode mode)
|
||
{
|
||
switch (mode) {
|
||
case TRAINLOG_LOAD_EXTERNAL:
|
||
return "externe";
|
||
case TRAINLOG_LOAD_ASSISTANCE:
|
||
return "assistance";
|
||
case TRAINLOG_LOAD_NONE:
|
||
default:
|
||
return "aucune";
|
||
}
|
||
}
|
||
|
||
static void screen_session_detail(
|
||
TrainlogDatabase *database,
|
||
const char *session_id
|
||
)
|
||
{
|
||
TrainlogSessionSummary session;
|
||
TrainlogPersistedExerciseDetail
|
||
exercises[MAX_SESSION_EXERCISES];
|
||
|
||
size_t count = 0U;
|
||
size_t selected = 0U;
|
||
TrainlogStatus status;
|
||
|
||
status =
|
||
trainlog_database_get_session_details(
|
||
database,
|
||
session_id,
|
||
&session,
|
||
exercises,
|
||
MAX_SESSION_EXERCISES,
|
||
&count
|
||
);
|
||
|
||
if (status != TRAINLOG_STATUS_OK) {
|
||
draw_shell(
|
||
"Détail séance",
|
||
"Une touche pour revenir"
|
||
);
|
||
|
||
status_line(
|
||
"Impossible de charger la séance.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
|
||
wait_key();
|
||
return;
|
||
}
|
||
|
||
for (;;) {
|
||
bool decorated =
|
||
COLS >= 100 &&
|
||
LINES >= 32;
|
||
|
||
int key;
|
||
|
||
if (decorated) {
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
section_ascii_header(
|
||
":: D E T A I L S E A N C E ::"
|
||
);
|
||
|
||
dashboard_panel(
|
||
8,
|
||
2,
|
||
13,
|
||
COLS - 3,
|
||
"SEANCE"
|
||
);
|
||
|
||
focused_panel(
|
||
14,
|
||
2,
|
||
LINES - 4,
|
||
COLS - 3,
|
||
"EXERCICE",
|
||
true
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"←→/↑↓ exercice précédent/suivant e modifier la séance b/Échap retour"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
10,
|
||
5,
|
||
"Début : %s",
|
||
session.started_at
|
||
);
|
||
|
||
mvprintw(
|
||
11,
|
||
5,
|
||
"Fin : %s",
|
||
session.ended_at[0] != '\0'
|
||
? session.ended_at
|
||
: "séance ouverte"
|
||
);
|
||
|
||
mvprintw(
|
||
12,
|
||
5,
|
||
"Type : %s",
|
||
session_type_label(
|
||
session.session_type
|
||
)
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"TRAINLOG — Détail séance",
|
||
"←→/↑↓ naviguer e modifier b/Échap retour"
|
||
);
|
||
|
||
mvprintw(
|
||
3,
|
||
4,
|
||
"Début : %s",
|
||
session.started_at
|
||
);
|
||
|
||
mvprintw(
|
||
4,
|
||
4,
|
||
"Fin : %s",
|
||
session.ended_at[0] != '\0'
|
||
? session.ended_at
|
||
: "séance ouverte"
|
||
);
|
||
|
||
mvprintw(
|
||
5,
|
||
4,
|
||
"Type : %s",
|
||
session_type_label(
|
||
session.session_type
|
||
)
|
||
);
|
||
}
|
||
|
||
if (count == 0U) {
|
||
mvprintw(
|
||
decorated ? 17 : 7,
|
||
decorated ? 5 : 4,
|
||
"Aucun exercice dans cette séance."
|
||
);
|
||
} else {
|
||
TrainlogPersistedExerciseDetail *exercise =
|
||
&exercises[selected];
|
||
|
||
if (exercise->recording_mode ==
|
||
TRAINLOG_RECORDING_CONTINUOUS) {
|
||
char duration_text[64];
|
||
|
||
int title_row =
|
||
decorated ? 16 : 6;
|
||
|
||
int mode_row =
|
||
decorated ? 18 : 8;
|
||
|
||
int first_data_row =
|
||
decorated ? 20 : 10;
|
||
|
||
int row =
|
||
first_data_row;
|
||
|
||
if (trainlog_duration_format(
|
||
exercise->continuous_duration_seconds,
|
||
duration_text,
|
||
sizeof(duration_text)
|
||
) != TRAINLOG_STATUS_OK) {
|
||
(void)snprintf(
|
||
duration_text,
|
||
sizeof(duration_text),
|
||
"%d s",
|
||
exercise->continuous_duration_seconds
|
||
);
|
||
}
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
title_row,
|
||
decorated ? 5 : 4,
|
||
"Exercice %zu/%zu — %s",
|
||
selected + 1U,
|
||
count,
|
||
exercise->name
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
mode_row,
|
||
decorated ? 5 : 4,
|
||
"Mode : continu"
|
||
);
|
||
|
||
mvprintw(
|
||
row++,
|
||
decorated ? 5 : 4,
|
||
"Durée : %s",
|
||
duration_text
|
||
);
|
||
|
||
if (exercise->has_continuous_speed != 0) {
|
||
mvprintw(
|
||
row++,
|
||
decorated ? 5 : 4,
|
||
"Vitesse : %.1f km/h",
|
||
exercise->continuous_speed_kmh
|
||
);
|
||
}
|
||
|
||
if (exercise->has_continuous_distance != 0) {
|
||
mvprintw(
|
||
row++,
|
||
decorated ? 5 : 4,
|
||
"Distance : %.2f km",
|
||
exercise->continuous_distance_km
|
||
);
|
||
}
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_SUCCESS
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
row + 1,
|
||
decorated ? 5 : 4,
|
||
"Réalisé : activité continue"
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_SUCCESS
|
||
)
|
||
);
|
||
} else {
|
||
char rest_text[64];
|
||
char target_duration_text[64];
|
||
|
||
int title_row =
|
||
decorated ? 16 : 6;
|
||
|
||
int mode_row =
|
||
decorated ? 18 : 8;
|
||
|
||
int target_row =
|
||
decorated ? 20 : 10;
|
||
|
||
int weight_row =
|
||
decorated ? 21 : 11;
|
||
|
||
int actual_row =
|
||
decorated ? 23 : 13;
|
||
|
||
int summary_row =
|
||
decorated ? 25 : 15;
|
||
|
||
int warning_row =
|
||
decorated ? 27 : 17;
|
||
|
||
if (trainlog_duration_format(
|
||
exercise->rest_seconds,
|
||
rest_text,
|
||
sizeof(rest_text)
|
||
) != TRAINLOG_STATUS_OK) {
|
||
(void)snprintf(
|
||
rest_text,
|
||
sizeof(rest_text),
|
||
"%ds",
|
||
exercise->rest_seconds
|
||
);
|
||
}
|
||
|
||
target_duration_text[0] = '\0';
|
||
|
||
if (exercise->tracking_mode ==
|
||
TRAINLOG_TRACKING_DURATION) {
|
||
(void)trainlog_duration_format(
|
||
exercise->target_duration_seconds,
|
||
target_duration_text,
|
||
sizeof(target_duration_text)
|
||
);
|
||
}
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
title_row,
|
||
decorated ? 5 : 4,
|
||
"Exercice %zu/%zu — %s",
|
||
selected + 1U,
|
||
count,
|
||
exercise->name
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
mode_row,
|
||
decorated ? 5 : 4,
|
||
"Mode : %-12s Charge : %-10s Repos : %s",
|
||
exercise->tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS
|
||
? "répétitions"
|
||
: "durée",
|
||
session_detail_load_label(
|
||
exercise->load_mode
|
||
),
|
||
rest_text
|
||
);
|
||
|
||
if (
|
||
exercise->target_sets <= 0
|
||
) {
|
||
mvprintw(
|
||
target_row,
|
||
decorated ? 5 : 4,
|
||
"Cible : non renseignée"
|
||
);
|
||
} else if (
|
||
exercise->tracking_mode ==
|
||
TRAINLOG_TRACKING_REPS
|
||
) {
|
||
mvprintw(
|
||
target_row,
|
||
decorated ? 5 : 4,
|
||
"Cible : %d série(s) × %d reps",
|
||
exercise->target_sets,
|
||
exercise->target_reps
|
||
);
|
||
} else {
|
||
mvprintw(
|
||
target_row,
|
||
decorated ? 5 : 4,
|
||
"Cible : %d série(s) × %s",
|
||
exercise->target_sets,
|
||
target_duration_text
|
||
);
|
||
}
|
||
|
||
if (exercise->has_target_weight != 0) {
|
||
mvprintw(
|
||
weight_row,
|
||
decorated ? 5 : 4,
|
||
"Charge cible : %.1f kg",
|
||
exercise->target_weight_kg
|
||
);
|
||
} else {
|
||
mvprintw(
|
||
weight_row,
|
||
decorated ? 5 : 4,
|
||
"Charge cible : —"
|
||
);
|
||
}
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_SUCCESS
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
actual_row,
|
||
decorated ? 5 : 4,
|
||
"Réalisé : %zu série(s)",
|
||
exercise->actual_set_count
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_SUCCESS
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
summary_row,
|
||
decorated ? 5 : 4,
|
||
"%.*s",
|
||
COLS - 10,
|
||
exercise->actual_summary
|
||
);
|
||
|
||
if (exercise->load_mode ==
|
||
TRAINLOG_LOAD_ASSISTANCE) {
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_WARNING
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
warning_row,
|
||
decorated ? 5 : 4,
|
||
"Assistance : plus de kg = davantage d'aide."
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_WARNING
|
||
)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (key == 'b' ||
|
||
key == 'B' ||
|
||
key == 27) {
|
||
return;
|
||
}
|
||
|
||
if (key == 'e' ||
|
||
key == 'E') {
|
||
edit_persisted_session(
|
||
database,
|
||
session_id
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
(key == KEY_RIGHT ||
|
||
key == KEY_DOWN)) {
|
||
selected =
|
||
selected + 1U < count
|
||
? selected + 1U
|
||
: 0U;
|
||
} else if (
|
||
count > 0U &&
|
||
(key == KEY_LEFT ||
|
||
key == KEY_UP)
|
||
) {
|
||
selected =
|
||
selected > 0U
|
||
? selected - 1U
|
||
: count - 1U;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_HISTORY_ASCII_HEADER */
|
||
|
||
static void history_ascii_header(void)
|
||
{
|
||
static const char *const logo[] = {
|
||
"TTTTT RRRR AAA IIIII N N L OOO GGG ",
|
||
" T R R A A I NN N L O O G ",
|
||
" T RRRR AAAAA I N N N L O O G GG",
|
||
" T R R A A I N NN L O O G G",
|
||
" T R R A A IIIII N N LLLLL OOO GGG "
|
||
};
|
||
|
||
const size_t line_count =
|
||
sizeof(logo) /
|
||
sizeof(logo[0]);
|
||
|
||
size_t index;
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
for (index = 0U;
|
||
index < line_count;
|
||
++index) {
|
||
int width =
|
||
(int)strlen(logo[index]);
|
||
|
||
int column =
|
||
(COLS - width) / 2;
|
||
|
||
if (column < 2) {
|
||
column = 2;
|
||
}
|
||
|
||
mvprintw(
|
||
1 + (int)index,
|
||
column,
|
||
"%.*s",
|
||
COLS - column - 2,
|
||
logo[index]
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
{
|
||
const char *label =
|
||
":: H I S T O R I Q U E ::";
|
||
|
||
int width =
|
||
(int)strlen(label);
|
||
|
||
int column =
|
||
(COLS - width) / 2;
|
||
|
||
if (column < 2) {
|
||
column = 2;
|
||
}
|
||
|
||
mvprintw(
|
||
6,
|
||
column,
|
||
"%s",
|
||
label
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
}
|
||
|
||
static void history_scrollbar(
|
||
int top,
|
||
int bottom,
|
||
int column,
|
||
size_t selected,
|
||
size_t count,
|
||
size_t visible
|
||
)
|
||
{
|
||
int track_height;
|
||
int row;
|
||
int thumb;
|
||
|
||
if (bottom <= top ||
|
||
count <= visible ||
|
||
count <= 1U) {
|
||
return;
|
||
}
|
||
|
||
track_height =
|
||
bottom - top + 1;
|
||
|
||
if (track_height < 2) {
|
||
return;
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
for (row = top;
|
||
row <= bottom;
|
||
++row) {
|
||
mvaddch(
|
||
row,
|
||
column,
|
||
ACS_VLINE
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
thumb =
|
||
top +
|
||
(int)(
|
||
(selected *
|
||
(size_t)(track_height - 1)) /
|
||
(count - 1U)
|
||
);
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvaddch(
|
||
thumb,
|
||
column,
|
||
ACS_CKBOARD
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
static void screen_history(
|
||
TrainlogDatabase *database
|
||
)
|
||
{
|
||
TrainlogSessionSummary
|
||
sessions[MAX_SESSIONS];
|
||
|
||
size_t selected = 0U;
|
||
int nav_selected = 2;
|
||
int focus = 1;
|
||
|
||
for (;;) {
|
||
size_t count = 0U;
|
||
size_t top = 0U;
|
||
size_t index;
|
||
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
int list_top =
|
||
large_layout
|
||
? 11
|
||
: 3;
|
||
|
||
int list_bottom =
|
||
LINES - 4;
|
||
|
||
int first_row =
|
||
list_top + 1;
|
||
|
||
int visible_rows =
|
||
list_bottom -
|
||
first_row;
|
||
|
||
int key;
|
||
|
||
if (visible_rows < 1) {
|
||
return;
|
||
}
|
||
|
||
if (trainlog_database_list_sessions(
|
||
database,
|
||
sessions,
|
||
MAX_SESSIONS,
|
||
&count
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return;
|
||
}
|
||
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
if (large_layout) {
|
||
history_ascii_header();
|
||
|
||
primary_top_navbar(
|
||
2,
|
||
nav_selected,
|
||
focus == 0
|
||
);
|
||
|
||
focused_panel(
|
||
list_top,
|
||
2,
|
||
list_bottom,
|
||
COLS - 3,
|
||
"SEANCES ENREGISTREES",
|
||
focus == 1
|
||
);
|
||
} else {
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
1,
|
||
2,
|
||
" TRAINLOG — Historique "
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
large_layout
|
||
? "Tab zone ↑↓/PgUp/PgDn liste ←→ menu Entrée ouvrir e modifier 0/Home accueil F1-F4 direct b/Échap retour"
|
||
: "↑↓ naviguer Entrée détail e modifier b/Échap retour"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
if (count == 0U) {
|
||
mvprintw(
|
||
first_row + 1,
|
||
large_layout ? 6 : 4,
|
||
"Aucune séance."
|
||
);
|
||
}
|
||
|
||
if (count > 0U) {
|
||
if (selected >= count) {
|
||
selected =
|
||
count - 1U;
|
||
}
|
||
|
||
if (selected >=
|
||
(size_t)visible_rows) {
|
||
top =
|
||
selected -
|
||
(size_t)visible_rows +
|
||
1U;
|
||
}
|
||
|
||
for (index = 0U;
|
||
index < (size_t)visible_rows &&
|
||
top + index < count;
|
||
++index) {
|
||
size_t absolute =
|
||
top + index;
|
||
|
||
int item_row =
|
||
first_row +
|
||
(int)index;
|
||
|
||
int item_col =
|
||
large_layout ? 5 : 4;
|
||
|
||
if (focus == 1 &&
|
||
absolute == selected) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
{
|
||
char display_date[17];
|
||
|
||
session_history_datetime(
|
||
sessions[absolute].started_at,
|
||
display_date
|
||
);
|
||
|
||
mvprintw(
|
||
item_row,
|
||
item_col,
|
||
" %-16s %-14s %2zu exercice(s) ",
|
||
display_date,
|
||
session_type_history_label(
|
||
sessions[absolute].session_type
|
||
),
|
||
sessions[absolute].exercise_count
|
||
);
|
||
}
|
||
|
||
if (focus == 1 &&
|
||
absolute == selected) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
if (large_layout) {
|
||
history_scrollbar(
|
||
first_row,
|
||
list_bottom - 1,
|
||
COLS - 5,
|
||
selected,
|
||
count,
|
||
(size_t)visible_rows
|
||
);
|
||
}
|
||
}
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (large_layout &&
|
||
(key == '\t' ||
|
||
key == KEY_BTAB)) {
|
||
focus =
|
||
focus == 0
|
||
? 1
|
||
: 0;
|
||
continue;
|
||
}
|
||
|
||
if (large_layout &&
|
||
primary_top_nav_forward(key)) {
|
||
return;
|
||
}
|
||
|
||
if (key == 'b' ||
|
||
key == 'B' ||
|
||
key == 27) {
|
||
return;
|
||
}
|
||
|
||
if (large_layout &&
|
||
focus == 0) {
|
||
if (key == KEY_LEFT) {
|
||
nav_selected =
|
||
nav_selected > 0
|
||
? nav_selected - 1
|
||
: 5;
|
||
} else if (key == KEY_RIGHT) {
|
||
nav_selected =
|
||
nav_selected < 5
|
||
? nav_selected + 1
|
||
: 0;
|
||
} else if (
|
||
key == '\n' ||
|
||
key == KEY_ENTER
|
||
) {
|
||
if (primary_top_nav_activate(
|
||
nav_selected
|
||
)) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (count == 0U) {
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_UP) {
|
||
selected =
|
||
selected > 0U
|
||
? selected - 1U
|
||
: 0U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_DOWN) {
|
||
selected =
|
||
selected + 1U < count
|
||
? selected + 1U
|
||
: count - 1U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_PPAGE) {
|
||
size_t jump =
|
||
(size_t)visible_rows;
|
||
|
||
selected =
|
||
selected > jump
|
||
? selected - jump
|
||
: 0U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_NPAGE) {
|
||
size_t jump =
|
||
(size_t)visible_rows;
|
||
|
||
selected =
|
||
selected + jump < count
|
||
? selected + jump
|
||
: count - 1U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == '\n' ||
|
||
key == KEY_ENTER) {
|
||
screen_session_detail(
|
||
database,
|
||
sessions[selected].session_id
|
||
);
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == 'e' ||
|
||
key == 'E') {
|
||
edit_persisted_session(
|
||
database,
|
||
sessions[selected].session_id
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_BODY_RECORDS_TUI */
|
||
|
||
#define MAX_BODY_OBSERVATIONS 512U
|
||
|
||
static void body_panel(
|
||
int top,
|
||
int left,
|
||
int bottom,
|
||
int right,
|
||
const char *label
|
||
)
|
||
{
|
||
WINDOW *panel;
|
||
int height;
|
||
int width;
|
||
|
||
if (top < 0 ||
|
||
left < 0 ||
|
||
bottom <= top ||
|
||
right <= left ||
|
||
bottom >= LINES ||
|
||
right >= COLS) {
|
||
return;
|
||
}
|
||
|
||
height = bottom - top + 1;
|
||
width = right - left + 1;
|
||
|
||
panel = derwin(
|
||
stdscr,
|
||
height,
|
||
width,
|
||
top,
|
||
left
|
||
);
|
||
|
||
if (panel == NULL) {
|
||
return;
|
||
}
|
||
|
||
box(panel, 0, 0);
|
||
|
||
if (label != NULL &&
|
||
label[0] != '\0' &&
|
||
width > 8) {
|
||
wattron(
|
||
panel,
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvwprintw(
|
||
panel,
|
||
0,
|
||
2,
|
||
" %.*s ",
|
||
width - 6,
|
||
label
|
||
);
|
||
|
||
wattroff(
|
||
panel,
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
syncok(panel, TRUE);
|
||
wsyncup(panel);
|
||
delwin(panel);
|
||
}
|
||
|
||
static void body_short_date(
|
||
const char *timestamp,
|
||
char output[9]
|
||
)
|
||
{
|
||
if (timestamp == NULL ||
|
||
strlen(timestamp) < 10U) {
|
||
(void)snprintf(
|
||
output,
|
||
9U,
|
||
"%s",
|
||
"--/--/--"
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
(void)snprintf(
|
||
output,
|
||
9U,
|
||
"%c%c/%c%c/%c%c",
|
||
timestamp[8],
|
||
timestamp[9],
|
||
timestamp[5],
|
||
timestamp[6],
|
||
timestamp[2],
|
||
timestamp[3]
|
||
);
|
||
}
|
||
|
||
static size_t body_record_metric_count(
|
||
const TrainlogBodyObservationRecord *record
|
||
)
|
||
{
|
||
size_t count = 0U;
|
||
|
||
if (record == NULL) {
|
||
return 0U;
|
||
}
|
||
|
||
#define COUNT_BODY_VALUE(flag_) \
|
||
do { \
|
||
if ((flag_)) { \
|
||
++count; \
|
||
} \
|
||
} while (0)
|
||
|
||
COUNT_BODY_VALUE(record->has_body_weight);
|
||
COUNT_BODY_VALUE(record->has_neck);
|
||
COUNT_BODY_VALUE(record->has_shoulders);
|
||
COUNT_BODY_VALUE(record->has_chest);
|
||
COUNT_BODY_VALUE(record->has_waist);
|
||
COUNT_BODY_VALUE(record->has_hips);
|
||
COUNT_BODY_VALUE(record->has_left_arm);
|
||
COUNT_BODY_VALUE(record->has_right_arm);
|
||
COUNT_BODY_VALUE(record->has_left_forearm);
|
||
COUNT_BODY_VALUE(record->has_right_forearm);
|
||
COUNT_BODY_VALUE(record->has_left_thigh);
|
||
COUNT_BODY_VALUE(record->has_right_thigh);
|
||
COUNT_BODY_VALUE(record->has_left_calf);
|
||
COUNT_BODY_VALUE(record->has_right_calf);
|
||
|
||
#undef COUNT_BODY_VALUE
|
||
|
||
return count;
|
||
}
|
||
|
||
static void body_draw_scrollbar(
|
||
int top,
|
||
int bottom,
|
||
int column,
|
||
size_t selected,
|
||
size_t count,
|
||
size_t visible
|
||
)
|
||
{
|
||
int track_height;
|
||
int row;
|
||
int thumb;
|
||
|
||
if (bottom <= top ||
|
||
count <= visible ||
|
||
count <= 1U) {
|
||
return;
|
||
}
|
||
|
||
track_height =
|
||
bottom - top + 1;
|
||
|
||
if (track_height < 2) {
|
||
return;
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
for (row = top;
|
||
row <= bottom;
|
||
++row) {
|
||
mvaddch(
|
||
row,
|
||
column,
|
||
ACS_VLINE
|
||
);
|
||
}
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
thumb =
|
||
top +
|
||
(int)(
|
||
(selected *
|
||
(size_t)(track_height - 1)) /
|
||
(count - 1U)
|
||
);
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvaddch(
|
||
thumb,
|
||
column,
|
||
ACS_CKBOARD
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
static void body_summary_text(
|
||
const TrainlogBodyObservationRecord *record,
|
||
char *output,
|
||
size_t output_size
|
||
)
|
||
{
|
||
char weight[24];
|
||
char waist[24];
|
||
char chest[24];
|
||
size_t metric_count;
|
||
|
||
if (record == NULL ||
|
||
output == NULL ||
|
||
output_size == 0U) {
|
||
return;
|
||
}
|
||
|
||
if (record->has_body_weight) {
|
||
(void)snprintf(
|
||
weight,
|
||
sizeof(weight),
|
||
"%.1f kg",
|
||
record->body_weight_kg
|
||
);
|
||
} else {
|
||
(void)snprintf(
|
||
weight,
|
||
sizeof(weight),
|
||
"%s",
|
||
"— kg"
|
||
);
|
||
}
|
||
|
||
if (record->has_waist) {
|
||
(void)snprintf(
|
||
waist,
|
||
sizeof(waist),
|
||
"taille %.1f",
|
||
record->waist_cm
|
||
);
|
||
} else {
|
||
(void)snprintf(
|
||
waist,
|
||
sizeof(waist),
|
||
"%s",
|
||
"taille —"
|
||
);
|
||
}
|
||
|
||
if (record->has_chest) {
|
||
(void)snprintf(
|
||
chest,
|
||
sizeof(chest),
|
||
"poitrine %.1f",
|
||
record->chest_cm
|
||
);
|
||
} else {
|
||
(void)snprintf(
|
||
chest,
|
||
sizeof(chest),
|
||
"%s",
|
||
"poitrine —"
|
||
);
|
||
}
|
||
|
||
metric_count =
|
||
body_record_metric_count(record);
|
||
|
||
(void)snprintf(
|
||
output,
|
||
output_size,
|
||
"%-9s %-9s %-14s %2zu valeur(s)",
|
||
weight,
|
||
waist,
|
||
chest,
|
||
metric_count
|
||
);
|
||
}
|
||
|
||
static bool prompt_optional_double_existing(
|
||
int row,
|
||
const char *label,
|
||
bool *present,
|
||
double *value
|
||
)
|
||
{
|
||
char prompt[160];
|
||
char buffer[64];
|
||
|
||
if (label == NULL ||
|
||
present == NULL ||
|
||
value == NULL) {
|
||
return false;
|
||
}
|
||
|
||
if (*present) {
|
||
(void)snprintf(
|
||
prompt,
|
||
sizeof(prompt),
|
||
"%s [%.1f] (Entrée=garder, -=effacer) : ",
|
||
label,
|
||
*value
|
||
);
|
||
} else {
|
||
(void)snprintf(
|
||
prompt,
|
||
sizeof(prompt),
|
||
"%s [vide] : ",
|
||
label
|
||
);
|
||
}
|
||
|
||
if (!prompt_text(
|
||
row,
|
||
prompt,
|
||
buffer,
|
||
sizeof(buffer),
|
||
true
|
||
)) {
|
||
return false;
|
||
}
|
||
|
||
if (buffer[0] == '\0') {
|
||
return true;
|
||
}
|
||
|
||
if (strcmp(buffer, "-") == 0) {
|
||
*present = false;
|
||
*value = 0.0;
|
||
return true;
|
||
}
|
||
|
||
if (!parse_double_positive(
|
||
buffer,
|
||
value
|
||
)) {
|
||
status_line(
|
||
"Nombre positif invalide.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
|
||
wait_key();
|
||
return false;
|
||
}
|
||
|
||
*present = true;
|
||
return true;
|
||
}
|
||
|
||
static void body_input_from_record(
|
||
const TrainlogBodyObservationRecord *record,
|
||
TrainlogBodyObservationInput *input
|
||
)
|
||
{
|
||
if (record == NULL ||
|
||
input == NULL) {
|
||
return;
|
||
}
|
||
|
||
(void)memset(
|
||
input,
|
||
0,
|
||
sizeof(*input)
|
||
);
|
||
|
||
(void)snprintf(
|
||
input->observation_id,
|
||
sizeof(input->observation_id),
|
||
"%s",
|
||
record->observation_id
|
||
);
|
||
|
||
(void)snprintf(
|
||
input->observed_at,
|
||
sizeof(input->observed_at),
|
||
"%s",
|
||
record->observed_at
|
||
);
|
||
|
||
input->has_body_weight =
|
||
record->has_body_weight;
|
||
|
||
input->body_weight_kg =
|
||
record->body_weight_kg;
|
||
|
||
input->has_neck =
|
||
record->has_neck;
|
||
|
||
input->neck_cm =
|
||
record->neck_cm;
|
||
|
||
input->has_shoulders =
|
||
record->has_shoulders;
|
||
|
||
input->shoulders_cm =
|
||
record->shoulders_cm;
|
||
|
||
input->has_chest =
|
||
record->has_chest;
|
||
|
||
input->chest_cm =
|
||
record->chest_cm;
|
||
|
||
input->has_waist =
|
||
record->has_waist;
|
||
|
||
input->waist_cm =
|
||
record->waist_cm;
|
||
|
||
input->has_hips =
|
||
record->has_hips;
|
||
|
||
input->hips_cm =
|
||
record->hips_cm;
|
||
|
||
input->has_left_arm =
|
||
record->has_left_arm;
|
||
|
||
input->left_arm_cm =
|
||
record->left_arm_cm;
|
||
|
||
input->has_right_arm =
|
||
record->has_right_arm;
|
||
|
||
input->right_arm_cm =
|
||
record->right_arm_cm;
|
||
|
||
input->has_left_forearm =
|
||
record->has_left_forearm;
|
||
|
||
input->left_forearm_cm =
|
||
record->left_forearm_cm;
|
||
|
||
input->has_right_forearm =
|
||
record->has_right_forearm;
|
||
|
||
input->right_forearm_cm =
|
||
record->right_forearm_cm;
|
||
|
||
input->has_left_thigh =
|
||
record->has_left_thigh;
|
||
|
||
input->left_thigh_cm =
|
||
record->left_thigh_cm;
|
||
|
||
input->has_right_thigh =
|
||
record->has_right_thigh;
|
||
|
||
input->right_thigh_cm =
|
||
record->right_thigh_cm;
|
||
|
||
input->has_left_calf =
|
||
record->has_left_calf;
|
||
|
||
input->left_calf_cm =
|
||
record->left_calf_cm;
|
||
|
||
input->has_right_calf =
|
||
record->has_right_calf;
|
||
|
||
input->right_calf_cm =
|
||
record->right_calf_cm;
|
||
|
||
input->notes =
|
||
record->notes[0] != '\0'
|
||
? record->notes
|
||
: NULL;
|
||
}
|
||
|
||
static bool body_edit_record(
|
||
TrainlogDatabase *database,
|
||
const char *observation_id
|
||
)
|
||
{
|
||
TrainlogBodyObservationRecord record;
|
||
TrainlogBodyObservationInput input;
|
||
int row;
|
||
|
||
if (trainlog_database_get_body_observation(
|
||
database,
|
||
observation_id,
|
||
&record
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return false;
|
||
}
|
||
|
||
body_input_from_record(
|
||
&record,
|
||
&input
|
||
);
|
||
|
||
draw_shell(
|
||
"Modifier relevé — 1/2",
|
||
"Entrée garde · - efface une mesure · Échap annule tout"
|
||
);
|
||
|
||
row = 4;
|
||
|
||
#define EDIT_BODY_VALUE(label_, flag_, value_) \
|
||
do { \
|
||
if (!prompt_optional_double_existing( \
|
||
row++, \
|
||
(label_), \
|
||
&(flag_), \
|
||
&(value_) \
|
||
)) { \
|
||
return false; \
|
||
} \
|
||
} while (0)
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Poids kg",
|
||
input.has_body_weight,
|
||
input.body_weight_kg
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Cou cm",
|
||
input.has_neck,
|
||
input.neck_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Épaules cm",
|
||
input.has_shoulders,
|
||
input.shoulders_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Poitrine cm",
|
||
input.has_chest,
|
||
input.chest_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Tour de taille cm",
|
||
input.has_waist,
|
||
input.waist_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Hanches cm",
|
||
input.has_hips,
|
||
input.hips_cm
|
||
);
|
||
|
||
draw_shell(
|
||
"Modifier relevé — 2/2",
|
||
"Entrée garde · - efface une mesure · Échap annule tout"
|
||
);
|
||
|
||
row = 4;
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Bras gauche cm",
|
||
input.has_left_arm,
|
||
input.left_arm_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Bras droit cm",
|
||
input.has_right_arm,
|
||
input.right_arm_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Avant-bras gauche cm",
|
||
input.has_left_forearm,
|
||
input.left_forearm_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Avant-bras droit cm",
|
||
input.has_right_forearm,
|
||
input.right_forearm_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Cuisse gauche cm",
|
||
input.has_left_thigh,
|
||
input.left_thigh_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Cuisse droite cm",
|
||
input.has_right_thigh,
|
||
input.right_thigh_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Mollet gauche cm",
|
||
input.has_left_calf,
|
||
input.left_calf_cm
|
||
);
|
||
|
||
EDIT_BODY_VALUE(
|
||
"Mollet droit cm",
|
||
input.has_right_calf,
|
||
input.right_calf_cm
|
||
);
|
||
|
||
#undef EDIT_BODY_VALUE
|
||
|
||
if (trainlog_database_update_body_observation(
|
||
database,
|
||
&input
|
||
) == TRAINLOG_STATUS_OK) {
|
||
status_line(
|
||
"✓ Relevé corrigé.",
|
||
TRAINLOG_COLOR_SUCCESS
|
||
);
|
||
|
||
wait_key();
|
||
return true;
|
||
}
|
||
|
||
status_line(
|
||
"Correction impossible. Au moins une mesure doit rester présente.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
|
||
wait_key();
|
||
return false;
|
||
}
|
||
|
||
static void body_detail_value(
|
||
int row,
|
||
int column,
|
||
const char *label,
|
||
bool present,
|
||
double value,
|
||
const char *unit
|
||
)
|
||
{
|
||
if (present) {
|
||
mvprintw(
|
||
row,
|
||
column,
|
||
"%-24s %7.1f %s",
|
||
label,
|
||
value,
|
||
unit
|
||
);
|
||
} else {
|
||
mvprintw(
|
||
row,
|
||
column,
|
||
"%-24s %7s",
|
||
label,
|
||
"—"
|
||
);
|
||
}
|
||
}
|
||
|
||
static void screen_body_observation_detail(
|
||
TrainlogDatabase *database,
|
||
const char *observation_id
|
||
)
|
||
{
|
||
int page = 0;
|
||
|
||
for (;;) {
|
||
TrainlogBodyObservationRecord record;
|
||
char date[9];
|
||
bool decorated =
|
||
COLS >= 100 &&
|
||
LINES >= 34;
|
||
int frame_top =
|
||
decorated ? 8 : 3;
|
||
int key;
|
||
|
||
if (trainlog_database_get_body_observation(
|
||
database,
|
||
observation_id,
|
||
&record
|
||
) != TRAINLOG_STATUS_OK) {
|
||
status_line(
|
||
"Impossible de charger le relevé.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
|
||
wait_key();
|
||
return;
|
||
}
|
||
|
||
body_short_date(
|
||
record.observed_at,
|
||
date
|
||
);
|
||
|
||
if (decorated) {
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
section_ascii_header(
|
||
":: D E T A I L C O R P S ::"
|
||
);
|
||
|
||
focused_panel(
|
||
frame_top,
|
||
2,
|
||
LINES - 4,
|
||
COLS - 3,
|
||
page == 0
|
||
? "RELEVE — GENERAL"
|
||
: "RELEVE — MEMBRES",
|
||
true
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"←→ page e Modifier b/Échap retour"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"TRAINLOG — Détail relevé",
|
||
"←→ page e Modifier b/Échap retour"
|
||
);
|
||
|
||
body_panel(
|
||
3,
|
||
2,
|
||
LINES - 4,
|
||
COLS - 3,
|
||
page == 0
|
||
? "RELEVE — GENERAL"
|
||
: "RELEVE — MEMBRES"
|
||
);
|
||
}
|
||
|
||
attron(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
frame_top + 2,
|
||
5,
|
||
"%s page %d/2",
|
||
date,
|
||
page + 1
|
||
);
|
||
|
||
attroff(
|
||
A_BOLD |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
|
||
if (page == 0) {
|
||
body_detail_value(
|
||
frame_top + 5,
|
||
6,
|
||
"Poids",
|
||
record.has_body_weight,
|
||
record.body_weight_kg,
|
||
"kg"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 7,
|
||
6,
|
||
"Cou",
|
||
record.has_neck,
|
||
record.neck_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 9,
|
||
6,
|
||
"Épaules",
|
||
record.has_shoulders,
|
||
record.shoulders_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 11,
|
||
6,
|
||
"Poitrine",
|
||
record.has_chest,
|
||
record.chest_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 13,
|
||
6,
|
||
"Tour de taille",
|
||
record.has_waist,
|
||
record.waist_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 15,
|
||
6,
|
||
"Hanches",
|
||
record.has_hips,
|
||
record.hips_cm,
|
||
"cm"
|
||
);
|
||
} else {
|
||
body_detail_value(
|
||
frame_top + 5,
|
||
6,
|
||
"Bras gauche",
|
||
record.has_left_arm,
|
||
record.left_arm_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 7,
|
||
6,
|
||
"Bras droit",
|
||
record.has_right_arm,
|
||
record.right_arm_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 9,
|
||
6,
|
||
"Avant-bras gauche",
|
||
record.has_left_forearm,
|
||
record.left_forearm_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 11,
|
||
6,
|
||
"Avant-bras droit",
|
||
record.has_right_forearm,
|
||
record.right_forearm_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 13,
|
||
6,
|
||
"Cuisse gauche",
|
||
record.has_left_thigh,
|
||
record.left_thigh_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 15,
|
||
6,
|
||
"Cuisse droite",
|
||
record.has_right_thigh,
|
||
record.right_thigh_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 17,
|
||
6,
|
||
"Mollet gauche",
|
||
record.has_left_calf,
|
||
record.left_calf_cm,
|
||
"cm"
|
||
);
|
||
|
||
body_detail_value(
|
||
frame_top + 19,
|
||
6,
|
||
"Mollet droit",
|
||
record.has_right_calf,
|
||
record.right_calf_cm,
|
||
"cm"
|
||
);
|
||
}
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (key == 'b' ||
|
||
key == 'B' ||
|
||
key == 27) {
|
||
return;
|
||
}
|
||
|
||
if (key == KEY_LEFT ||
|
||
key == KEY_RIGHT) {
|
||
page =
|
||
page == 0
|
||
? 1
|
||
: 0;
|
||
continue;
|
||
}
|
||
|
||
if (key == 'e' ||
|
||
key == 'E') {
|
||
(void)body_edit_record(
|
||
database,
|
||
observation_id
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
static void screen_body(
|
||
TrainlogDatabase *database
|
||
)
|
||
{
|
||
TrainlogBodyObservationRecord
|
||
records[MAX_BODY_OBSERVATIONS];
|
||
|
||
size_t selected = 0U;
|
||
int nav_selected = 4;
|
||
int focus = 1;
|
||
|
||
for (;;) {
|
||
TrainlogBodyMetricPoint
|
||
weight_points[MAX_BODY_METRIC_POINTS];
|
||
|
||
size_t count = 0U;
|
||
size_t weight_count = 0U;
|
||
size_t top = 0U;
|
||
size_t index;
|
||
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
int graph_top =
|
||
large_layout ? 11 : 3;
|
||
|
||
int graph_bottom =
|
||
large_layout ? 19 : 11;
|
||
|
||
int list_top =
|
||
graph_bottom + 1;
|
||
|
||
int list_bottom =
|
||
LINES - 4;
|
||
|
||
int first_row =
|
||
list_top + 1;
|
||
|
||
int visible_rows =
|
||
list_bottom -
|
||
first_row;
|
||
|
||
int key;
|
||
|
||
if (visible_rows < 1) {
|
||
return;
|
||
}
|
||
|
||
if (trainlog_database_list_body_observations(
|
||
database,
|
||
records,
|
||
MAX_BODY_OBSERVATIONS,
|
||
&count
|
||
) != TRAINLOG_STATUS_OK) {
|
||
return;
|
||
}
|
||
|
||
(void)trainlog_database_list_body_metric_points(
|
||
database,
|
||
TRAINLOG_BODY_METRIC_WEIGHT,
|
||
weight_points,
|
||
MAX_BODY_METRIC_POINTS,
|
||
&weight_count
|
||
);
|
||
|
||
if (count > 0U &&
|
||
selected >= count) {
|
||
selected =
|
||
count - 1U;
|
||
}
|
||
|
||
if (count > 0U &&
|
||
selected >=
|
||
(size_t)visible_rows) {
|
||
top =
|
||
selected -
|
||
(size_t)visible_rows +
|
||
1U;
|
||
}
|
||
|
||
if (large_layout) {
|
||
erase();
|
||
box(stdscr, 0, 0);
|
||
|
||
section_ascii_header(
|
||
":: C O R P S ::"
|
||
);
|
||
|
||
primary_top_navbar(
|
||
4,
|
||
nav_selected,
|
||
focus == 0
|
||
);
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"Tab zone ↑↓/PgUp/PgDn relevés ←→ menu Entrée détail e Modifier a ajouter g vue globale 0/Home accueil F1-F4 direct"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"TRAINLOG — Corps",
|
||
"↑↓ choisir Entrée détail e Modifier a ajouter g vue globale b/Échap retour"
|
||
);
|
||
}
|
||
|
||
body_panel(
|
||
graph_top,
|
||
2,
|
||
graph_bottom,
|
||
COLS - 3,
|
||
"EVOLUTION DU POIDS"
|
||
);
|
||
|
||
if (weight_count == 0U) {
|
||
mvprintw(
|
||
graph_top + 3,
|
||
6,
|
||
"Aucune donnée de poids."
|
||
);
|
||
} else {
|
||
draw_body_metric_graph(
|
||
graph_top + 1,
|
||
graph_bottom -
|
||
graph_top -
|
||
2,
|
||
weight_points,
|
||
weight_count,
|
||
"kg"
|
||
);
|
||
}
|
||
|
||
if (large_layout) {
|
||
focused_panel(
|
||
list_top,
|
||
2,
|
||
list_bottom,
|
||
COLS - 3,
|
||
"RELEVES ENREGISTRES",
|
||
focus == 1
|
||
);
|
||
} else {
|
||
body_panel(
|
||
list_top,
|
||
2,
|
||
list_bottom,
|
||
COLS - 3,
|
||
"RELEVES ENREGISTRES"
|
||
);
|
||
}
|
||
|
||
if (count == 0U) {
|
||
mvprintw(
|
||
first_row + 1,
|
||
6,
|
||
"Aucun relevé. Appuyez sur a pour en ajouter un."
|
||
);
|
||
}
|
||
|
||
for (index = 0U;
|
||
index < (size_t)visible_rows &&
|
||
top + index < count;
|
||
++index) {
|
||
size_t absolute =
|
||
top + index;
|
||
|
||
char date[9];
|
||
char summary[128];
|
||
|
||
int row =
|
||
first_row +
|
||
(int)index;
|
||
|
||
body_short_date(
|
||
records[absolute].observed_at,
|
||
date
|
||
);
|
||
|
||
body_summary_text(
|
||
&records[absolute],
|
||
summary,
|
||
sizeof(summary)
|
||
);
|
||
|
||
if (focus == 1 &&
|
||
absolute == selected) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
row,
|
||
5,
|
||
" %-8s %.*s ",
|
||
date,
|
||
COLS - 20,
|
||
summary
|
||
);
|
||
|
||
if (focus == 1 &&
|
||
absolute == selected) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
body_draw_scrollbar(
|
||
first_row,
|
||
list_bottom - 1,
|
||
COLS - 5,
|
||
selected,
|
||
count,
|
||
(size_t)visible_rows
|
||
);
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (large_layout &&
|
||
(key == '\t' ||
|
||
key == KEY_BTAB)) {
|
||
focus =
|
||
focus == 0
|
||
? 1
|
||
: 0;
|
||
continue;
|
||
}
|
||
|
||
if (large_layout &&
|
||
primary_top_nav_forward(key)) {
|
||
return;
|
||
}
|
||
|
||
if (key == 'b' ||
|
||
key == 'B' ||
|
||
key == 27) {
|
||
return;
|
||
}
|
||
|
||
if (large_layout &&
|
||
focus == 0) {
|
||
if (key == KEY_LEFT) {
|
||
nav_selected =
|
||
nav_selected > 0
|
||
? nav_selected - 1
|
||
: 5;
|
||
} else if (key == KEY_RIGHT) {
|
||
nav_selected =
|
||
nav_selected < 5
|
||
? nav_selected + 1
|
||
: 0;
|
||
} else if (
|
||
key == '\n' ||
|
||
key == KEY_ENTER
|
||
) {
|
||
if (primary_top_nav_activate(
|
||
nav_selected
|
||
)) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_UP &&
|
||
count > 0U) {
|
||
selected =
|
||
selected > 0U
|
||
? selected - 1U
|
||
: 0U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_DOWN &&
|
||
count > 0U) {
|
||
selected =
|
||
selected + 1U < count
|
||
? selected + 1U
|
||
: count - 1U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_PPAGE &&
|
||
count > 0U) {
|
||
size_t jump =
|
||
(size_t)visible_rows;
|
||
|
||
selected =
|
||
selected > jump
|
||
? selected - jump
|
||
: 0U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == KEY_NPAGE &&
|
||
count > 0U) {
|
||
size_t jump =
|
||
(size_t)visible_rows;
|
||
|
||
selected =
|
||
selected + jump < count
|
||
? selected + jump
|
||
: count - 1U;
|
||
|
||
continue;
|
||
}
|
||
|
||
if ((key == '\n' ||
|
||
key == KEY_ENTER) &&
|
||
count > 0U) {
|
||
screen_body_observation_detail(
|
||
database,
|
||
records[selected].observation_id
|
||
);
|
||
|
||
continue;
|
||
}
|
||
|
||
if ((key == 'e' ||
|
||
key == 'E') &&
|
||
count > 0U) {
|
||
(void)body_edit_record(
|
||
database,
|
||
records[selected].observation_id
|
||
);
|
||
|
||
continue;
|
||
}
|
||
|
||
if (key == 'a' ||
|
||
key == 'A') {
|
||
add_body_observation(database);
|
||
selected = 0U;
|
||
continue;
|
||
}
|
||
|
||
if (key == 'g' ||
|
||
key == 'G') {
|
||
draw_shell(
|
||
"TRAINLOG — Corps — Vue globale",
|
||
"Une touche pour revenir"
|
||
);
|
||
|
||
draw_global_body_overlay(
|
||
database
|
||
);
|
||
|
||
refresh();
|
||
(void)getch();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* TRAINLOG_SYNC_TUI */
|
||
/* TRAINLOG_SHARED_SYNC_ENGINE_V1 */
|
||
|
||
#define SYNC_HISTORY_CAPACITY 64U
|
||
#define SYNC_HISTORY_TEXT_MAX TRAINLOG_SYNC_SUMMARY_MAX
|
||
#define SYNC_DETAIL_LINE_CAPACITY 160U
|
||
#define SYNC_DETAIL_TEXT_CAPACITY 16384U
|
||
|
||
typedef struct TrainlogSyncHistoryEntry {
|
||
char sync_id[
|
||
TRAINLOG_ID_MAX + 1U
|
||
];
|
||
|
||
char timestamp[17];
|
||
bool success;
|
||
|
||
char summary[
|
||
SYNC_HISTORY_TEXT_MAX + 1U
|
||
];
|
||
} TrainlogSyncHistoryEntry;
|
||
|
||
static void session_history_datetime(
|
||
const char *timestamp,
|
||
char output[17]
|
||
)
|
||
{
|
||
if (
|
||
timestamp == NULL ||
|
||
strlen(timestamp) < 16U
|
||
) {
|
||
(void)snprintf(
|
||
output,
|
||
17U,
|
||
"%s",
|
||
"--/--/---- --:--"
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
(void)snprintf(
|
||
output,
|
||
17U,
|
||
"%c%c/%c%c/%c%c%c%c %c%c:%c%c",
|
||
timestamp[8],
|
||
timestamp[9],
|
||
timestamp[5],
|
||
timestamp[6],
|
||
timestamp[0],
|
||
timestamp[1],
|
||
timestamp[2],
|
||
timestamp[3],
|
||
timestamp[11],
|
||
timestamp[12],
|
||
timestamp[14],
|
||
timestamp[15]
|
||
);
|
||
}
|
||
|
||
static bool sync_history_path(
|
||
char *output,
|
||
size_t output_size
|
||
)
|
||
{
|
||
const char *data_home =
|
||
getenv(
|
||
"XDG_DATA_HOME"
|
||
);
|
||
|
||
const char *home =
|
||
getenv(
|
||
"HOME"
|
||
);
|
||
|
||
int written;
|
||
|
||
if (
|
||
output == NULL ||
|
||
output_size == 0U
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
if (
|
||
data_home != NULL &&
|
||
data_home[0] != '\0'
|
||
) {
|
||
written =
|
||
snprintf(
|
||
output,
|
||
output_size,
|
||
"%s/trainlog/sync_history.log",
|
||
data_home
|
||
);
|
||
} else if (
|
||
home != NULL &&
|
||
home[0] != '\0'
|
||
) {
|
||
written =
|
||
snprintf(
|
||
output,
|
||
output_size,
|
||
"%s/.local/share/trainlog/sync_history.log",
|
||
home
|
||
);
|
||
} else {
|
||
return false;
|
||
}
|
||
|
||
return
|
||
written >= 0 &&
|
||
(size_t)written <
|
||
output_size;
|
||
}
|
||
|
||
static bool sync_runs_path(
|
||
const char *sync_id,
|
||
char *output,
|
||
size_t output_size
|
||
)
|
||
{
|
||
const char *data_home =
|
||
getenv(
|
||
"XDG_DATA_HOME"
|
||
);
|
||
|
||
const char *home =
|
||
getenv(
|
||
"HOME"
|
||
);
|
||
|
||
int written;
|
||
|
||
if (
|
||
sync_id == NULL ||
|
||
sync_id[0] == '\0' ||
|
||
output == NULL ||
|
||
output_size == 0U
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
if (
|
||
data_home != NULL &&
|
||
data_home[0] != '\0'
|
||
) {
|
||
written =
|
||
snprintf(
|
||
output,
|
||
output_size,
|
||
"%s/trainlog/sync_runs/%s.txt",
|
||
data_home,
|
||
sync_id
|
||
);
|
||
} else if (
|
||
home != NULL &&
|
||
home[0] != '\0'
|
||
) {
|
||
written =
|
||
snprintf(
|
||
output,
|
||
output_size,
|
||
"%s/.local/share/trainlog/sync_runs/%s.txt",
|
||
home,
|
||
sync_id
|
||
);
|
||
} else {
|
||
return false;
|
||
}
|
||
|
||
return
|
||
written >= 0 &&
|
||
(size_t)written <
|
||
output_size;
|
||
}
|
||
|
||
static void sync_history_load(
|
||
TrainlogSyncHistoryEntry *output,
|
||
size_t capacity,
|
||
size_t *output_count
|
||
)
|
||
{
|
||
char path[
|
||
PATH_MAX + 1U
|
||
];
|
||
|
||
TrainlogSyncHistoryEntry
|
||
ring[SYNC_HISTORY_CAPACITY];
|
||
|
||
size_t count = 0U;
|
||
size_t next = 0U;
|
||
size_t copied;
|
||
FILE *file;
|
||
char line[768];
|
||
|
||
if (
|
||
output_count == NULL ||
|
||
(
|
||
capacity > 0U &&
|
||
output == NULL
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
|
||
*output_count = 0U;
|
||
|
||
if (
|
||
!sync_history_path(
|
||
path,
|
||
sizeof(path)
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
|
||
file =
|
||
fopen(
|
||
path,
|
||
"rb"
|
||
);
|
||
|
||
if (file == NULL) {
|
||
return;
|
||
}
|
||
|
||
(void)memset(
|
||
ring,
|
||
0,
|
||
sizeof(ring)
|
||
);
|
||
|
||
while (
|
||
fgets(
|
||
line,
|
||
sizeof(line),
|
||
file
|
||
) != NULL
|
||
) {
|
||
char *first;
|
||
char *second;
|
||
char *third;
|
||
char *newline;
|
||
|
||
const char *sync_id;
|
||
const char *timestamp;
|
||
const char *status_text;
|
||
const char *summary;
|
||
|
||
TrainlogSyncHistoryEntry *entry;
|
||
|
||
newline =
|
||
strchr(
|
||
line,
|
||
'\n'
|
||
);
|
||
|
||
if (newline != NULL) {
|
||
*newline = '\0';
|
||
}
|
||
|
||
first =
|
||
strchr(
|
||
line,
|
||
'\t'
|
||
);
|
||
|
||
if (first == NULL) {
|
||
continue;
|
||
}
|
||
|
||
*first = '\0';
|
||
|
||
second =
|
||
strchr(
|
||
first + 1,
|
||
'\t'
|
||
);
|
||
|
||
if (second == NULL) {
|
||
continue;
|
||
}
|
||
|
||
*second = '\0';
|
||
|
||
third =
|
||
strchr(
|
||
second + 1,
|
||
'\t'
|
||
);
|
||
|
||
if (third != NULL) {
|
||
*third = '\0';
|
||
|
||
sync_id = line;
|
||
timestamp = first + 1;
|
||
status_text = second + 1;
|
||
summary = third + 1;
|
||
} else {
|
||
sync_id = "";
|
||
timestamp = line;
|
||
status_text = first + 1;
|
||
summary = second + 1;
|
||
}
|
||
|
||
entry =
|
||
&ring[next];
|
||
|
||
(void)snprintf(
|
||
entry->sync_id,
|
||
sizeof(entry->sync_id),
|
||
"%s",
|
||
sync_id
|
||
);
|
||
|
||
(void)snprintf(
|
||
entry->timestamp,
|
||
sizeof(entry->timestamp),
|
||
"%s",
|
||
timestamp
|
||
);
|
||
|
||
entry->success =
|
||
strcmp(
|
||
status_text,
|
||
"1"
|
||
) == 0;
|
||
|
||
(void)snprintf(
|
||
entry->summary,
|
||
sizeof(entry->summary),
|
||
"%s",
|
||
summary
|
||
);
|
||
|
||
next =
|
||
(
|
||
next + 1U
|
||
) %
|
||
SYNC_HISTORY_CAPACITY;
|
||
|
||
if (
|
||
count <
|
||
SYNC_HISTORY_CAPACITY
|
||
) {
|
||
++count;
|
||
}
|
||
}
|
||
|
||
(void)fclose(
|
||
file
|
||
);
|
||
|
||
copied =
|
||
count < capacity
|
||
? count
|
||
: capacity;
|
||
|
||
for (
|
||
size_t index = 0U;
|
||
index < copied;
|
||
++index
|
||
) {
|
||
size_t source =
|
||
(
|
||
next +
|
||
SYNC_HISTORY_CAPACITY -
|
||
1U -
|
||
index
|
||
) %
|
||
SYNC_HISTORY_CAPACITY;
|
||
|
||
output[index] =
|
||
ring[source];
|
||
}
|
||
|
||
*output_count =
|
||
copied;
|
||
}
|
||
|
||
static void screen_sync_run_detail(
|
||
const char *sync_id
|
||
)
|
||
{
|
||
char path[
|
||
PATH_MAX + 1U
|
||
];
|
||
|
||
char text[
|
||
SYNC_DETAIL_TEXT_CAPACITY
|
||
];
|
||
|
||
char *lines[
|
||
SYNC_DETAIL_LINE_CAPACITY
|
||
];
|
||
|
||
size_t line_count = 0U;
|
||
size_t offset = 0U;
|
||
FILE *file;
|
||
size_t used;
|
||
|
||
if (
|
||
!sync_runs_path(
|
||
sync_id,
|
||
path,
|
||
sizeof(path)
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
|
||
file =
|
||
fopen(
|
||
path,
|
||
"rb"
|
||
);
|
||
|
||
if (file == NULL) {
|
||
return;
|
||
}
|
||
|
||
used =
|
||
fread(
|
||
text,
|
||
1U,
|
||
sizeof(text) - 1U,
|
||
file
|
||
);
|
||
|
||
text[used] = '\0';
|
||
|
||
(void)fclose(
|
||
file
|
||
);
|
||
|
||
if (used > 0U) {
|
||
char *cursor = text;
|
||
|
||
lines[line_count] =
|
||
cursor;
|
||
|
||
++line_count;
|
||
|
||
while (
|
||
*cursor != '\0' &&
|
||
line_count <
|
||
SYNC_DETAIL_LINE_CAPACITY
|
||
) {
|
||
if (*cursor == '\n') {
|
||
*cursor = '\0';
|
||
|
||
if (
|
||
cursor[1] != '\0'
|
||
) {
|
||
lines[line_count] =
|
||
cursor + 1;
|
||
|
||
++line_count;
|
||
}
|
||
}
|
||
|
||
++cursor;
|
||
}
|
||
}
|
||
|
||
for (;;) {
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
int panel_top =
|
||
large_layout
|
||
? 8
|
||
: 3;
|
||
|
||
int panel_bottom =
|
||
LINES - 4;
|
||
|
||
int first_row =
|
||
panel_top + 2;
|
||
|
||
int visible =
|
||
panel_bottom -
|
||
first_row;
|
||
|
||
int key;
|
||
size_t index;
|
||
|
||
if (visible < 1) {
|
||
return;
|
||
}
|
||
|
||
erase();
|
||
box(
|
||
stdscr,
|
||
0,
|
||
0
|
||
);
|
||
|
||
if (large_layout) {
|
||
section_ascii_header(
|
||
":: S Y N C S H O W ::"
|
||
);
|
||
|
||
focused_panel(
|
||
panel_top,
|
||
2,
|
||
panel_bottom,
|
||
COLS - 3,
|
||
"DETAIL SYNCHRONISATION",
|
||
true
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"TRAINLOG — Sync show",
|
||
"↑↓ défiler PgUp/PgDn page b/Échap retour"
|
||
);
|
||
}
|
||
|
||
for (
|
||
index = 0U;
|
||
index < (size_t)visible &&
|
||
offset + index <
|
||
line_count;
|
||
++index
|
||
) {
|
||
mvprintw(
|
||
first_row +
|
||
(int)index,
|
||
large_layout
|
||
? 5
|
||
: 4,
|
||
"%.*s",
|
||
COLS -
|
||
(
|
||
large_layout
|
||
? 10
|
||
: 8
|
||
),
|
||
lines[
|
||
offset +
|
||
index
|
||
]
|
||
);
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"↑↓ défiler PgUp/PgDn page b/Échap retour"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (
|
||
key == 'b' ||
|
||
key == 'B' ||
|
||
key == 27 ||
|
||
key == '\n' ||
|
||
key == KEY_ENTER
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (
|
||
key == KEY_UP &&
|
||
offset > 0U
|
||
) {
|
||
--offset;
|
||
} else if (
|
||
key == KEY_DOWN &&
|
||
offset +
|
||
(size_t)visible <
|
||
line_count
|
||
) {
|
||
++offset;
|
||
} else if (
|
||
key == KEY_PPAGE
|
||
) {
|
||
size_t jump =
|
||
(size_t)visible;
|
||
|
||
offset =
|
||
offset > jump
|
||
? offset - jump
|
||
: 0U;
|
||
} else if (
|
||
key == KEY_NPAGE
|
||
) {
|
||
size_t jump =
|
||
(size_t)visible;
|
||
|
||
if (
|
||
offset + jump <
|
||
line_count
|
||
) {
|
||
offset += jump;
|
||
}
|
||
|
||
if (
|
||
line_count >
|
||
(size_t)visible &&
|
||
offset +
|
||
(size_t)visible >
|
||
line_count
|
||
) {
|
||
offset =
|
||
line_count -
|
||
(size_t)visible;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
static double sync_bytes_to_gib(
|
||
uint64_t bytes
|
||
)
|
||
{
|
||
return
|
||
(double)bytes /
|
||
(
|
||
1024.0 *
|
||
1024.0 *
|
||
1024.0
|
||
);
|
||
}
|
||
|
||
static void screen_sync(
|
||
TrainlogDatabase *database
|
||
)
|
||
{
|
||
size_t selected = 0U;
|
||
int nav_selected = 5;
|
||
int focus = 1;
|
||
|
||
TrainlogSyncDeviceInfo device;
|
||
TrainlogStatus probe_status =
|
||
TRAINLOG_STATUS_NOT_FOUND;
|
||
|
||
bool refresh_device = true;
|
||
|
||
(void)database;
|
||
|
||
(void)memset(
|
||
&device,
|
||
0,
|
||
sizeof(device)
|
||
);
|
||
|
||
for (;;) {
|
||
TrainlogSyncHistoryEntry
|
||
history[SYNC_HISTORY_CAPACITY];
|
||
|
||
size_t history_count = 0U;
|
||
|
||
bool large_layout =
|
||
COLS >= 100 &&
|
||
LINES >= 30;
|
||
|
||
int key;
|
||
|
||
if (refresh_device) {
|
||
probe_status =
|
||
trainlog_sync_probe(
|
||
&device
|
||
);
|
||
|
||
refresh_device = false;
|
||
}
|
||
|
||
sync_history_load(
|
||
history,
|
||
SYNC_HISTORY_CAPACITY,
|
||
&history_count
|
||
);
|
||
|
||
if (
|
||
history_count > 0U &&
|
||
selected >= history_count
|
||
) {
|
||
selected =
|
||
history_count - 1U;
|
||
}
|
||
|
||
erase();
|
||
box(
|
||
stdscr,
|
||
0,
|
||
0
|
||
);
|
||
|
||
if (large_layout) {
|
||
size_t index;
|
||
size_t top = 0U;
|
||
|
||
int history_top = 18;
|
||
|
||
int history_bottom =
|
||
LINES - 4;
|
||
|
||
int visible_rows =
|
||
history_bottom -
|
||
history_top -
|
||
2;
|
||
|
||
section_ascii_header(
|
||
":: S Y N C ::"
|
||
);
|
||
|
||
primary_top_navbar(
|
||
5,
|
||
nav_selected,
|
||
focus == 0
|
||
);
|
||
|
||
focused_panel(
|
||
11,
|
||
2,
|
||
16,
|
||
COLS - 3,
|
||
"APPAREIL CONNECTE",
|
||
false
|
||
);
|
||
|
||
if (
|
||
probe_status ==
|
||
TRAINLOG_STATUS_OK &&
|
||
device.connected &&
|
||
device.storage_ready
|
||
) {
|
||
mvprintw(
|
||
12,
|
||
5,
|
||
"✓ MTP direct connecté"
|
||
);
|
||
|
||
mvprintw(
|
||
13,
|
||
5,
|
||
"%s %s",
|
||
device.device.vendor,
|
||
device.device.model
|
||
);
|
||
|
||
mvprintw(
|
||
14,
|
||
5,
|
||
"Stockage interne : %.2f GiB libres / %.2f GiB",
|
||
sync_bytes_to_gib(
|
||
device.storage
|
||
.free_space_bytes
|
||
),
|
||
sync_bytes_to_gib(
|
||
device.storage
|
||
.max_capacity_bytes
|
||
)
|
||
);
|
||
} else if (
|
||
probe_status ==
|
||
TRAINLOG_STATUS_CONFLICT
|
||
) {
|
||
mvprintw(
|
||
13,
|
||
5,
|
||
"Service de synchronisation occupé."
|
||
);
|
||
} else {
|
||
mvprintw(
|
||
13,
|
||
5,
|
||
"Aucun appareil MTP Trainlog détecté."
|
||
);
|
||
}
|
||
|
||
focused_panel(
|
||
history_top,
|
||
2,
|
||
history_bottom,
|
||
COLS - 3,
|
||
"HISTORIQUE DES SYNCHRONISATIONS",
|
||
focus == 1
|
||
);
|
||
|
||
if (
|
||
history_count == 0U
|
||
) {
|
||
mvprintw(
|
||
history_top + 2,
|
||
5,
|
||
"Aucune synchronisation enregistrée."
|
||
);
|
||
} else {
|
||
if (visible_rows < 1) {
|
||
visible_rows = 1;
|
||
}
|
||
|
||
if (
|
||
selected >=
|
||
(size_t)visible_rows
|
||
) {
|
||
top =
|
||
selected -
|
||
(size_t)visible_rows +
|
||
1U;
|
||
}
|
||
|
||
for (
|
||
index = 0U;
|
||
index <
|
||
(size_t)visible_rows &&
|
||
top + index <
|
||
history_count;
|
||
++index
|
||
) {
|
||
size_t absolute =
|
||
top + index;
|
||
|
||
int row =
|
||
history_top +
|
||
2 +
|
||
(int)index;
|
||
|
||
if (
|
||
focus == 1 &&
|
||
absolute == selected
|
||
) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
row,
|
||
5,
|
||
" %-16s %c %-*.*s ",
|
||
history[absolute]
|
||
.timestamp,
|
||
history[absolute]
|
||
.success
|
||
? '+'
|
||
: '!',
|
||
COLS - 28,
|
||
COLS - 28,
|
||
history[absolute]
|
||
.summary
|
||
);
|
||
|
||
if (
|
||
focus == 1 &&
|
||
absolute == selected
|
||
) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
attron(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
|
||
mvprintw(
|
||
LINES - 2,
|
||
2,
|
||
"%.*s",
|
||
COLS - 4,
|
||
"Tab zone ←→ menu ↑↓ historique Entrée détail s synchroniser r actualiser b/Échap retour"
|
||
);
|
||
|
||
attroff(
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_MUTED
|
||
)
|
||
);
|
||
} else {
|
||
draw_shell(
|
||
"TRAINLOG — Sync",
|
||
"↑↓ historique Entrée détail s synchroniser r actualiser b/Échap retour"
|
||
);
|
||
|
||
if (
|
||
probe_status ==
|
||
TRAINLOG_STATUS_OK &&
|
||
device.connected
|
||
) {
|
||
mvprintw(
|
||
4,
|
||
4,
|
||
"✓ %s %s",
|
||
device.device.vendor,
|
||
device.device.model
|
||
);
|
||
} else {
|
||
mvprintw(
|
||
4,
|
||
4,
|
||
"Aucun appareil MTP."
|
||
);
|
||
}
|
||
|
||
if (
|
||
history_count == 0U
|
||
) {
|
||
mvprintw(
|
||
7,
|
||
4,
|
||
"Aucune synchronisation."
|
||
);
|
||
} else {
|
||
size_t index;
|
||
|
||
size_t limit =
|
||
history_count < 8U
|
||
? history_count
|
||
: 8U;
|
||
|
||
for (
|
||
index = 0U;
|
||
index < limit;
|
||
++index
|
||
) {
|
||
if (
|
||
index == selected
|
||
) {
|
||
attron(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
|
||
mvprintw(
|
||
7 + (int)index,
|
||
4,
|
||
"%-16s %c %.*s",
|
||
history[index]
|
||
.timestamp,
|
||
history[index]
|
||
.success
|
||
? '+'
|
||
: '!',
|
||
COLS - 25,
|
||
history[index]
|
||
.summary
|
||
);
|
||
|
||
if (
|
||
index == selected
|
||
) {
|
||
attroff(
|
||
A_REVERSE |
|
||
trainlog_theme_attribute(
|
||
TRAINLOG_COLOR_ACCENT
|
||
)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
refresh();
|
||
key = getch();
|
||
|
||
if (
|
||
key == 's' ||
|
||
key == 'S'
|
||
) {
|
||
TrainlogSyncReport report;
|
||
TrainlogStatus status;
|
||
|
||
status_line(
|
||
"Synchronisation bidirectionnelle en cours...",
|
||
TRAINLOG_COLOR_WARNING
|
||
);
|
||
|
||
refresh();
|
||
|
||
status =
|
||
trainlog_sync_run(
|
||
TRAINLOG_SYNC_TRIGGER_TUI,
|
||
false,
|
||
&report
|
||
);
|
||
|
||
if (
|
||
status ==
|
||
TRAINLOG_STATUS_OK &&
|
||
report.success
|
||
) {
|
||
status_line(
|
||
report.summary,
|
||
TRAINLOG_COLOR_SUCCESS
|
||
);
|
||
} else {
|
||
status_line(
|
||
report.error[0] != '\0'
|
||
? report.error
|
||
: "Synchronisation échouée.",
|
||
TRAINLOG_COLOR_ERROR
|
||
);
|
||
}
|
||
|
||
refresh();
|
||
(void)getch();
|
||
|
||
refresh_device = true;
|
||
continue;
|
||
}
|
||
|
||
if (
|
||
key == 'r' ||
|
||
key == 'R'
|
||
) {
|
||
refresh_device = true;
|
||
continue;
|
||
}
|
||
|
||
if (
|
||
key == 'b' ||
|
||
key == 'B' ||
|
||
key == 27
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (
|
||
large_layout &&
|
||
(
|
||
key == '\t' ||
|
||
key == KEY_BTAB
|
||
)
|
||
) {
|
||
focus =
|
||
focus == 0
|
||
? 1
|
||
: 0;
|
||
|
||
continue;
|
||
}
|
||
|
||
if (
|
||
large_layout &&
|
||
focus == 0
|
||
) {
|
||
if (
|
||
key == KEY_LEFT
|
||
) {
|
||
nav_selected =
|
||
nav_selected > 0
|
||
? nav_selected - 1
|
||
: 5;
|
||
} else if (
|
||
key == KEY_RIGHT
|
||
) {
|
||
nav_selected =
|
||
nav_selected < 5
|
||
? nav_selected + 1
|
||
: 0;
|
||
} else if (
|
||
key == '\n' ||
|
||
key == KEY_ENTER
|
||
) {
|
||
if (
|
||
nav_selected == 5
|
||
) {
|
||
focus = 1;
|
||
} else if (
|
||
primary_top_nav_activate(
|
||
nav_selected
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (
|
||
history_count > 0U &&
|
||
(
|
||
key == '\n' ||
|
||
key == KEY_ENTER
|
||
)
|
||
) {
|
||
if (
|
||
history[selected]
|
||
.sync_id[0] != '\0'
|
||
) {
|
||
screen_sync_run_detail(
|
||
history[selected]
|
||
.sync_id
|
||
);
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (
|
||
key == KEY_UP &&
|
||
history_count > 0U
|
||
) {
|
||
selected =
|
||
selected > 0U
|
||
? selected - 1U
|
||
: history_count - 1U;
|
||
} else if (
|
||
key == KEY_DOWN &&
|
||
history_count > 0U
|
||
) {
|
||
selected =
|
||
selected + 1U <
|
||
history_count
|
||
? selected + 1U
|
||
: 0U;
|
||
} else if (
|
||
key == '0' ||
|
||
key == KEY_HOME
|
||
) {
|
||
return;
|
||
} else if (
|
||
key == '1' ||
|
||
key == KEY_F(1) ||
|
||
key == '2' ||
|
||
key == KEY_F(2) ||
|
||
key == '3' ||
|
||
key == KEY_F(3) ||
|
||
key == '4' ||
|
||
key == KEY_F(4)
|
||
) {
|
||
if (
|
||
primary_top_nav_forward(
|
||
key
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
int trainlog_tui_run(TrainlogDatabase *database)
|
||
{
|
||
if (database == NULL) {
|
||
return 1;
|
||
}
|
||
|
||
(void)setlocale(LC_ALL, "");
|
||
|
||
if (initscr() == NULL) {
|
||
return 1;
|
||
}
|
||
|
||
cbreak();
|
||
noecho();
|
||
keypad(stdscr, true);
|
||
curs_set(0);
|
||
trainlog_theme_initialize();
|
||
|
||
for (;;) {
|
||
DashboardAction action;
|
||
|
||
if (LINES < 20 || COLS < 72) {
|
||
int key;
|
||
|
||
erase();
|
||
mvprintw(
|
||
1,
|
||
2,
|
||
"Terminal trop petit — minimum 72x20."
|
||
);
|
||
mvprintw(3, 2, "q pour quitter");
|
||
refresh();
|
||
|
||
key = getch();
|
||
if (key == 'q' || key == 'Q') {
|
||
break;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
action = screen_dashboard(database);
|
||
|
||
switch (action) {
|
||
case DASHBOARD_NEW_SESSION:
|
||
screen_new_session(database);
|
||
break;
|
||
case DASHBOARD_HISTORY:
|
||
screen_history(database);
|
||
break;
|
||
case DASHBOARD_EXERCISES:
|
||
screen_exercises(database);
|
||
break;
|
||
case DASHBOARD_BODY:
|
||
screen_body(database);
|
||
break;
|
||
case DASHBOARD_SYNC:
|
||
screen_sync(database);
|
||
break;
|
||
case DASHBOARD_QUIT:
|
||
endwin();
|
||
return 0;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
endwin();
|
||
return 0;
|
||
}
|