Add Sync TUI page and Android sync contract
This commit is contained in:
parent
c76d4c2106
commit
a3eeb91869
6 changed files with 1022 additions and 16 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -139,3 +139,17 @@ Next:
|
||||||
- validated a real Android internal-storage write/list/read roundtrip;
|
- validated a real Android internal-storage write/list/read roundtrip;
|
||||||
- kept Android access mount-free: no GVFS/FUSE mount lifecycle is required.
|
- kept Android access mount-free: no GVFS/FUSE mount lifecycle is required.
|
||||||
<!-- TRAINLOG_MTP_TRANSPORT_CHANGELOG _END -->
|
<!-- TRAINLOG_MTP_TRANSPORT_CHANGELOG _END -->
|
||||||
|
|
||||||
|
<!-- TRAINLOG_SYNC_FINAL_CHANGELOG -->
|
||||||
|
### Sync TUI page
|
||||||
|
|
||||||
|
- added `5 Sync / F5` to primary navigation;
|
||||||
|
- added dedicated Sync ASCII banner and framed page;
|
||||||
|
- added live direct-MTP device and storage status;
|
||||||
|
- suppressed libmtp terminal output during ncurses rendering;
|
||||||
|
- added focused-frame navigation with Tab/Shift+Tab;
|
||||||
|
- added clean list navigation and scrollbar behavior;
|
||||||
|
- exposed Android -> PC session/exercise/body synchronization directions;
|
||||||
|
- exposed PC -> Android canonical exercise-catalog direction;
|
||||||
|
- kept frozen Trainlog session JSON v1 unchanged.
|
||||||
|
<!-- TRAINLOG_SYNC_FINAL_CHANGELOG _END -->
|
||||||
|
|
|
||||||
|
|
@ -234,3 +234,29 @@ slice before the Android recorder depends on it.
|
||||||
The Android application itself will only need to produce a valid frozen
|
The Android application itself will only need to produce a valid frozen
|
||||||
Trainlog JSON v1 document and place it in the agreed exchange area.
|
Trainlog JSON v1 document and place it in the agreed exchange area.
|
||||||
<!-- TRAINLOG_ANDROID_MTP_TRANSPORT _END -->
|
<!-- TRAINLOG_ANDROID_MTP_TRANSPORT _END -->
|
||||||
|
|
||||||
|
<!-- TRAINLOG_ANDROID_NEXT_SLICE -->
|
||||||
|
## 16. Current Android implementation cursor
|
||||||
|
|
||||||
|
The Linux transport and Sync TUI foundations are complete enough to begin the
|
||||||
|
Android client.
|
||||||
|
|
||||||
|
Initial Android development uses fictitious data.
|
||||||
|
|
||||||
|
First Android slice:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. application scaffold
|
||||||
|
2. local exercise catalog
|
||||||
|
3. create/select exercise
|
||||||
|
4. session form
|
||||||
|
5. actual set entry
|
||||||
|
6. optional body measurements
|
||||||
|
7. fictitious completed session
|
||||||
|
```
|
||||||
|
|
||||||
|
Transport/export is added only after the local recorder workflow is comfortable.
|
||||||
|
|
||||||
|
The Android client must preserve stable exercise IDs so a catalog snapshot from
|
||||||
|
the PC can normalize exercise selection on both sides.
|
||||||
|
<!-- TRAINLOG_ANDROID_NEXT_SLICE _END -->
|
||||||
|
|
|
||||||
83
docs/reviews/sync_tui.md
Normal file
83
docs/reviews/sync_tui.md
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
# Sync TUI checkpoint
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
```text
|
||||||
|
MTP_TRANSPORT_FOUNDATION=PASS
|
||||||
|
TUI_SYNC_PAGE=IMPLEMENTED
|
||||||
|
TUI_SYNC_DEVICE_STATUS=IMPLEMENTED
|
||||||
|
TUI_SYNC_FOCUS_NAVIGATION=IMPLEMENTED
|
||||||
|
TUI_SYNC_REMOTE_JSON_CANDIDATES=IMPLEMENTED
|
||||||
|
TUI_SYNC_LOCAL_CATALOG_COUNT=IMPLEMENTED
|
||||||
|
TRAINLOG_FORMAT_V1=FROZEN
|
||||||
|
ANDROID_APP_SCAFFOLD=NEXT
|
||||||
|
```
|
||||||
|
|
||||||
|
The TUI now exposes a dedicated `5 Sync / F5` primary page.
|
||||||
|
|
||||||
|
The page uses the same visual identity as the rest of Trainlog:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TRAINLOG ASCII banner
|
||||||
|
top navigation
|
||||||
|
framed device status
|
||||||
|
framed synchronization list
|
||||||
|
footer shortcuts
|
||||||
|
```
|
||||||
|
|
||||||
|
Only the focused frame is highlighted with the warning/yellow border role.
|
||||||
|
|
||||||
|
`Tab` / `Shift+Tab` moves focus between top navigation and synchronization
|
||||||
|
content. Direct `0-5` / `F1-F5` navigation remains available.
|
||||||
|
|
||||||
|
## Device status
|
||||||
|
|
||||||
|
The page scans the direct USB/MTP backend and displays:
|
||||||
|
|
||||||
|
- connected MTP device;
|
||||||
|
- USB bus/device;
|
||||||
|
- VID:PID;
|
||||||
|
- serial when available;
|
||||||
|
- MTP storage readiness;
|
||||||
|
- free and total space.
|
||||||
|
|
||||||
|
Backend stdout/stderr is suppressed during the live MTP scan so libmtp cannot
|
||||||
|
corrupt ncurses rendering.
|
||||||
|
|
||||||
|
## Synchronization directions
|
||||||
|
|
||||||
|
Incoming Android -> PC categories:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sessions
|
||||||
|
exercise metadata carried by valid sessions
|
||||||
|
body measurements carried by valid sessions
|
||||||
|
```
|
||||||
|
|
||||||
|
New exercise metadata in a valid incoming session is intended to reconcile
|
||||||
|
automatically with the canonical local exercise catalog.
|
||||||
|
|
||||||
|
Body measurements embedded in the session are imported atomically with that
|
||||||
|
session.
|
||||||
|
|
||||||
|
Outgoing PC -> Android:
|
||||||
|
|
||||||
|
```text
|
||||||
|
canonical exercise catalog
|
||||||
|
```
|
||||||
|
|
||||||
|
Exercises created directly on the PC must be exportable to Android so both
|
||||||
|
sides use the same stable exercise IDs, display names, and tracking modes.
|
||||||
|
|
||||||
|
Catalog synchronization will use its own versioned snapshot contract and will
|
||||||
|
not modify or overload the frozen Trainlog session JSON v1 format.
|
||||||
|
|
||||||
|
## Current boundary
|
||||||
|
|
||||||
|
The Sync page currently reports remote JSON candidates and the local exercise
|
||||||
|
count.
|
||||||
|
|
||||||
|
It does not yet classify/import session JSON or emit the catalog snapshot.
|
||||||
|
|
||||||
|
The next development slice is the Android application scaffold using fictitious
|
||||||
|
development records until the full transfer path is validated.
|
||||||
|
|
@ -230,3 +230,58 @@ Next implementation slice:
|
||||||
4. begin the minimal Android recorder
|
4. begin the minimal Android recorder
|
||||||
```
|
```
|
||||||
<!-- TRAINLOG_MTP_TRANSPORT_ROADMAP _END -->
|
<!-- TRAINLOG_MTP_TRANSPORT_ROADMAP _END -->
|
||||||
|
|
||||||
|
<!-- TRAINLOG_SYNC_PAGE_ROADMAP -->
|
||||||
|
## Sync TUI integration
|
||||||
|
|
||||||
|
```text
|
||||||
|
MTP_TRANSPORT_FOUNDATION=PASS
|
||||||
|
TUI_SYNC_PAGE=IMPLEMENTED
|
||||||
|
TUI_SYNC_DEVICE_STATUS=IMPLEMENTED
|
||||||
|
TUI_SYNC_REMOTE_JSON_CANDIDATES=IMPLEMENTED
|
||||||
|
TUI_SYNC_LOCAL_CATALOG_COUNT=IMPLEMENTED
|
||||||
|
JSON_V1_MTP_CLASSIFICATION_IMPORT=NEXT
|
||||||
|
CATALOG_SNAPSHOT_SYNC=AFTER
|
||||||
|
ANDROID_MINIMAL_RECORDER=AFTER
|
||||||
|
TRAINLOG_FORMAT_V1=FROZEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Sync direction:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Android -> PC
|
||||||
|
sessions
|
||||||
|
new exercises embedded in sessions -> automatic reconciliation
|
||||||
|
body data embedded in sessions -> automatic import
|
||||||
|
|
||||||
|
PC -> Android
|
||||||
|
canonical exercise catalog snapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Standalone body-observation exchange, if required by the Android recorder, gets
|
||||||
|
its own explicit versioned contract rather than changing session JSON v1.
|
||||||
|
<!-- TRAINLOG_SYNC_PAGE_ROADMAP _END -->
|
||||||
|
|
||||||
|
<!-- TRAINLOG_SYNC_FINAL_ROADMAP -->
|
||||||
|
## Sync UI checkpoint complete
|
||||||
|
|
||||||
|
```text
|
||||||
|
MTP_TRANSPORT_FOUNDATION=PASS
|
||||||
|
TUI_SYNC_PAGE=IMPLEMENTED
|
||||||
|
TUI_SYNC_DEVICE_STATUS=IMPLEMENTED
|
||||||
|
TUI_SYNC_FOCUS_NAVIGATION=IMPLEMENTED
|
||||||
|
TUI_SYNC_REMOTE_JSON_CANDIDATES=IMPLEMENTED
|
||||||
|
TUI_SYNC_LOCAL_CATALOG_COUNT=IMPLEMENTED
|
||||||
|
ANDROID_APP_SCAFFOLD=NEXT
|
||||||
|
ANDROID_FAKE_DATA_FLOW=AFTER
|
||||||
|
JSON_V1_ANDROID_EXPORT=AFTER
|
||||||
|
CATALOG_SNAPSHOT_SYNC=AFTER
|
||||||
|
TRAINLOG_FORMAT_V1=FROZEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Development now moves to the Android client.
|
||||||
|
|
||||||
|
Fictitious sessions, exercises, and body observations are used during Android
|
||||||
|
development and synchronization testing. The development database will be
|
||||||
|
purged before normal production use begins.
|
||||||
|
<!-- TRAINLOG_SYNC_FINAL_ROADMAP _END -->
|
||||||
|
|
|
||||||
97
docs/tui.md
97
docs/tui.md
|
|
@ -577,3 +577,100 @@ preserved.
|
||||||
The dashboard rolling 12-month axis clamps the final `MM/YY` label inside the
|
The dashboard rolling 12-month axis clamps the final `MM/YY` label inside the
|
||||||
dashboard frame so the current-month label does not overwrite the right border.
|
dashboard frame so the current-month label does not overwrite the right border.
|
||||||
<!-- TRAINLOG_TUI_EDITABILITY_NAV_CHECKPOINT _END -->
|
<!-- TRAINLOG_TUI_EDITABILITY_NAV_CHECKPOINT _END -->
|
||||||
|
|
||||||
|
<!-- TRAINLOG_SYNC_PAGE -->
|
||||||
|
## Sync page
|
||||||
|
|
||||||
|
The primary TUI navigation includes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
5 Sync / F5
|
||||||
|
```
|
||||||
|
|
||||||
|
The Sync screen uses the same ASCII banner, top navigation, ncurses frames and
|
||||||
|
footer conventions as the other primary pages.
|
||||||
|
|
||||||
|
The screen is a live overview of the direct USB/MTP transport:
|
||||||
|
|
||||||
|
- connected physical MTP device;
|
||||||
|
- USB bus/device and VID:PID;
|
||||||
|
- device serial when available;
|
||||||
|
- selected MTP storage;
|
||||||
|
- free/capacity values;
|
||||||
|
- readiness of the root `Trainlog` exchange area.
|
||||||
|
|
||||||
|
Incoming categories are presented explicitly:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Séances
|
||||||
|
Exercices
|
||||||
|
Mensurations
|
||||||
|
```
|
||||||
|
|
||||||
|
New exercise metadata arriving inside a valid session is intended to reconcile
|
||||||
|
automatically against the canonical local catalog.
|
||||||
|
|
||||||
|
Body measurements carried by a valid session are imported with that session.
|
||||||
|
|
||||||
|
The opposite direction is also explicit: exercises created directly on the PC
|
||||||
|
must be exportable to the Android application so both sides use the same stable
|
||||||
|
exercise IDs, names and tracking modes. This catalog synchronization uses a
|
||||||
|
separate versioned catalog snapshot; it does not overload or modify the frozen
|
||||||
|
Trainlog session JSON v1 contract.
|
||||||
|
|
||||||
|
The first Sync-page slice displays remote JSON candidates and the local exercise
|
||||||
|
count. Actual JSON classification/import and catalog-snapshot export are the
|
||||||
|
next synchronization slices.
|
||||||
|
<!-- TRAINLOG_SYNC_PAGE _END -->
|
||||||
|
|
||||||
|
<!-- TRAINLOG_SYNC_FINAL_CHECKPOINT -->
|
||||||
|
## Sync page checkpoint
|
||||||
|
|
||||||
|
Primary navigation now includes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 Accueil
|
||||||
|
1 Séance
|
||||||
|
2 Historique
|
||||||
|
3 Exercices
|
||||||
|
4 Corps
|
||||||
|
5 Sync
|
||||||
|
```
|
||||||
|
|
||||||
|
`5 Sync / F5` opens a dedicated page rather than drawing over the dashboard.
|
||||||
|
|
||||||
|
The page contains:
|
||||||
|
|
||||||
|
- `APPAREIL CONNECTE`;
|
||||||
|
- `SYNCHRONISATION`.
|
||||||
|
|
||||||
|
Focus rules:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tab / Shift+Tab switch focused frame
|
||||||
|
Left/Right move inside top navigation
|
||||||
|
Enter activate selected navigation item
|
||||||
|
Up/Down move through synchronization rows
|
||||||
|
PgUp/PgDn scroll synchronization content
|
||||||
|
r rescan USB/MTP state
|
||||||
|
b / Escape return
|
||||||
|
```
|
||||||
|
|
||||||
|
Only the focused frame uses the yellow border/title role.
|
||||||
|
|
||||||
|
The live page reports the connected Android MTP device and exposes these sync
|
||||||
|
directions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Android -> PC
|
||||||
|
sessions
|
||||||
|
exercises carried by sessions
|
||||||
|
body measurements carried by sessions
|
||||||
|
|
||||||
|
PC -> Android
|
||||||
|
canonical exercise catalog
|
||||||
|
```
|
||||||
|
|
||||||
|
The session exchange remains frozen Trainlog JSON v1.
|
||||||
|
Catalog synchronization is a separate versioned contract.
|
||||||
|
<!-- TRAINLOG_SYNC_FINAL_CHECKPOINT _END -->
|
||||||
|
|
|
||||||
763
tui/src/tui.c
763
tui/src/tui.c
|
|
@ -6,6 +6,7 @@
|
||||||
#include "trainlog/tui.h"
|
#include "trainlog/tui.h"
|
||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <locale.h>
|
#include <locale.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
@ -14,6 +15,7 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <wchar.h>
|
#include <wchar.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <curses.h>
|
#include <curses.h>
|
||||||
|
|
||||||
|
|
@ -21,8 +23,10 @@
|
||||||
#include "trainlog/catalog.h"
|
#include "trainlog/catalog.h"
|
||||||
#include "trainlog/duration.h"
|
#include "trainlog/duration.h"
|
||||||
#include "trainlog/id.h"
|
#include "trainlog/id.h"
|
||||||
|
#include "trainlog/mtp.h"
|
||||||
#include "trainlog/theme.h"
|
#include "trainlog/theme.h"
|
||||||
#include "trainlog/timeutil.h"
|
#include "trainlog/timeutil.h"
|
||||||
|
#include "trainlog/usb.h"
|
||||||
|
|
||||||
#define MAX_EXERCISES 128U
|
#define MAX_EXERCISES 128U
|
||||||
#define MAX_SESSION_EXERCISES 32U
|
#define MAX_SESSION_EXERCISES 32U
|
||||||
|
|
@ -40,6 +44,7 @@ typedef enum DashboardAction {
|
||||||
DASHBOARD_HISTORY,
|
DASHBOARD_HISTORY,
|
||||||
DASHBOARD_EXERCISES,
|
DASHBOARD_EXERCISES,
|
||||||
DASHBOARD_BODY,
|
DASHBOARD_BODY,
|
||||||
|
DASHBOARD_SYNC,
|
||||||
DASHBOARD_QUIT
|
DASHBOARD_QUIT
|
||||||
} DashboardAction;
|
} DashboardAction;
|
||||||
|
|
||||||
|
|
@ -52,6 +57,10 @@ static void screen_exercises(
|
||||||
TrainlogDatabase *database
|
TrainlogDatabase *database
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static void screen_sync(
|
||||||
|
TrainlogDatabase *database
|
||||||
|
);
|
||||||
|
|
||||||
static void dashboard_panel(
|
static void dashboard_panel(
|
||||||
int top,
|
int top,
|
||||||
int left,
|
int left,
|
||||||
|
|
@ -1667,10 +1676,10 @@ primary_top_nav_forward(key)) {
|
||||||
nav_selected =
|
nav_selected =
|
||||||
nav_selected > 0
|
nav_selected > 0
|
||||||
? nav_selected - 1
|
? nav_selected - 1
|
||||||
: 4;
|
: 5;
|
||||||
} else if (key == KEY_RIGHT) {
|
} else if (key == KEY_RIGHT) {
|
||||||
nav_selected =
|
nav_selected =
|
||||||
nav_selected < 4
|
nav_selected < 5
|
||||||
? nav_selected + 1
|
? nav_selected + 1
|
||||||
: 0;
|
: 0;
|
||||||
} else if (
|
} else if (
|
||||||
|
|
@ -3351,7 +3360,8 @@ static void primary_top_navbar(
|
||||||
"1 Séance",
|
"1 Séance",
|
||||||
"2 Historique",
|
"2 Historique",
|
||||||
"3 Exercices",
|
"3 Exercices",
|
||||||
"4 Corps"
|
"4 Corps",
|
||||||
|
"5 Sync"
|
||||||
};
|
};
|
||||||
|
|
||||||
int column = 5;
|
int column = 5;
|
||||||
|
|
@ -3367,7 +3377,7 @@ static void primary_top_navbar(
|
||||||
);
|
);
|
||||||
|
|
||||||
for (index = 0;
|
for (index = 0;
|
||||||
index < 5;
|
index < 6;
|
||||||
++index) {
|
++index) {
|
||||||
int width =
|
int width =
|
||||||
(int)strlen(labels[index]) + 4;
|
(int)strlen(labels[index]) + 4;
|
||||||
|
|
@ -3420,7 +3430,7 @@ static bool primary_top_nav_activate(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (selected_page < 0 ||
|
if (selected_page < 0 ||
|
||||||
selected_page > 4) {
|
selected_page > 5) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3469,6 +3479,11 @@ static bool primary_top_nav_forward(
|
||||||
forwarded = '4';
|
forwarded = '4';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case KEY_F(5):
|
||||||
|
case '5':
|
||||||
|
forwarded = '5';
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -4170,7 +4185,8 @@ static DashboardAction screen_dashboard(
|
||||||
"1 Séance",
|
"1 Séance",
|
||||||
"2 Historique",
|
"2 Historique",
|
||||||
"3 Exercices",
|
"3 Exercices",
|
||||||
"4 Corps"
|
"4 Corps",
|
||||||
|
"5 Sync"
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char *const footer =
|
static const char *const footer =
|
||||||
|
|
@ -4215,7 +4231,7 @@ static DashboardAction screen_dashboard(
|
||||||
large_layout ? 5 : 3;
|
large_layout ? 5 : 3;
|
||||||
|
|
||||||
for (index = 0;
|
for (index = 0;
|
||||||
index < 5;
|
index < 6;
|
||||||
++index) {
|
++index) {
|
||||||
int width =
|
int width =
|
||||||
(int)strlen(labels[index]) + 4;
|
(int)strlen(labels[index]) + 4;
|
||||||
|
|
@ -4279,13 +4295,13 @@ static DashboardAction screen_dashboard(
|
||||||
selected =
|
selected =
|
||||||
selected > 0
|
selected > 0
|
||||||
? selected - 1
|
? selected - 1
|
||||||
: 4;
|
: 5;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KEY_DOWN:
|
case KEY_DOWN:
|
||||||
case KEY_RIGHT:
|
case KEY_RIGHT:
|
||||||
selected =
|
selected =
|
||||||
selected < 4
|
selected < 5
|
||||||
? selected + 1
|
? selected + 1
|
||||||
: 0;
|
: 0;
|
||||||
break;
|
break;
|
||||||
|
|
@ -4303,6 +4319,8 @@ static DashboardAction screen_dashboard(
|
||||||
return DASHBOARD_EXERCISES;
|
return DASHBOARD_EXERCISES;
|
||||||
case 4:
|
case 4:
|
||||||
return DASHBOARD_BODY;
|
return DASHBOARD_BODY;
|
||||||
|
case 5:
|
||||||
|
return DASHBOARD_SYNC;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -4329,6 +4347,10 @@ static DashboardAction screen_dashboard(
|
||||||
case '4':
|
case '4':
|
||||||
return DASHBOARD_BODY;
|
return DASHBOARD_BODY;
|
||||||
|
|
||||||
|
case KEY_F(5):
|
||||||
|
case '5':
|
||||||
|
return DASHBOARD_SYNC;
|
||||||
|
|
||||||
case 'q':
|
case 'q':
|
||||||
case 'Q':
|
case 'Q':
|
||||||
return DASHBOARD_QUIT;
|
return DASHBOARD_QUIT;
|
||||||
|
|
@ -4903,7 +4925,9 @@ static bool choose_session_type(
|
||||||
key == '3' ||
|
key == '3' ||
|
||||||
key == KEY_F(3) ||
|
key == KEY_F(3) ||
|
||||||
key == '4' ||
|
key == '4' ||
|
||||||
key == KEY_F(4))) {
|
key == KEY_F(4) ||
|
||||||
|
key == '5' ||
|
||||||
|
key == KEY_F(5))) {
|
||||||
if (primary_top_nav_forward(key)) {
|
if (primary_top_nav_forward(key)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -4917,10 +4941,10 @@ static bool choose_session_type(
|
||||||
nav_selected =
|
nav_selected =
|
||||||
nav_selected > 0
|
nav_selected > 0
|
||||||
? nav_selected - 1
|
? nav_selected - 1
|
||||||
: 4;
|
: 5;
|
||||||
} else if (key == KEY_RIGHT) {
|
} else if (key == KEY_RIGHT) {
|
||||||
nav_selected =
|
nav_selected =
|
||||||
nav_selected < 4
|
nav_selected < 5
|
||||||
? nav_selected + 1
|
? nav_selected + 1
|
||||||
: 0;
|
: 0;
|
||||||
} else if (
|
} else if (
|
||||||
|
|
@ -6753,10 +6777,10 @@ static void screen_history(
|
||||||
nav_selected =
|
nav_selected =
|
||||||
nav_selected > 0
|
nav_selected > 0
|
||||||
? nav_selected - 1
|
? nav_selected - 1
|
||||||
: 4;
|
: 5;
|
||||||
} else if (key == KEY_RIGHT) {
|
} else if (key == KEY_RIGHT) {
|
||||||
nav_selected =
|
nav_selected =
|
||||||
nav_selected < 4
|
nav_selected < 5
|
||||||
? nav_selected + 1
|
? nav_selected + 1
|
||||||
: 0;
|
: 0;
|
||||||
} else if (
|
} else if (
|
||||||
|
|
@ -8050,10 +8074,10 @@ static void screen_body(
|
||||||
nav_selected =
|
nav_selected =
|
||||||
nav_selected > 0
|
nav_selected > 0
|
||||||
? nav_selected - 1
|
? nav_selected - 1
|
||||||
: 4;
|
: 5;
|
||||||
} else if (key == KEY_RIGHT) {
|
} else if (key == KEY_RIGHT) {
|
||||||
nav_selected =
|
nav_selected =
|
||||||
nav_selected < 4
|
nav_selected < 5
|
||||||
? nav_selected + 1
|
? nav_selected + 1
|
||||||
: 0;
|
: 0;
|
||||||
} else if (
|
} else if (
|
||||||
|
|
@ -8162,6 +8186,710 @@ static void screen_body(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TRAINLOG_SYNC_TUI */
|
||||||
|
|
||||||
|
#define SYNC_DEVICE_CAPACITY 8U
|
||||||
|
#define SYNC_STORAGE_CAPACITY 8U
|
||||||
|
#define SYNC_ENTRY_CAPACITY 128U
|
||||||
|
|
||||||
|
typedef struct TrainlogSyncOverview {
|
||||||
|
bool connected;
|
||||||
|
bool storage_ready;
|
||||||
|
bool exchange_ready;
|
||||||
|
size_t device_count;
|
||||||
|
size_t remote_entry_count;
|
||||||
|
size_t remote_json_count;
|
||||||
|
size_t local_exercise_count;
|
||||||
|
TrainlogUsbDevice device;
|
||||||
|
TrainlogMtpStorage storage;
|
||||||
|
uint32_t exchange_folder_id;
|
||||||
|
} TrainlogSyncOverview;
|
||||||
|
|
||||||
|
static bool sync_name_has_json_suffix(
|
||||||
|
const char *name
|
||||||
|
)
|
||||||
|
{
|
||||||
|
size_t length;
|
||||||
|
|
||||||
|
if (name == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
length = strlen(name);
|
||||||
|
|
||||||
|
return
|
||||||
|
length >= 5U &&
|
||||||
|
strcmp(
|
||||||
|
name + length - 5U,
|
||||||
|
".json"
|
||||||
|
) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double sync_bytes_to_gib(
|
||||||
|
uint64_t bytes
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
(double)bytes /
|
||||||
|
(1024.0 * 1024.0 * 1024.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sync_load_overview(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
TrainlogSyncOverview *overview
|
||||||
|
)
|
||||||
|
{
|
||||||
|
TrainlogUsbDevice devices[SYNC_DEVICE_CAPACITY];
|
||||||
|
TrainlogMtpStorage storages[SYNC_STORAGE_CAPACITY];
|
||||||
|
TrainlogMtpEntry entries[SYNC_ENTRY_CAPACITY];
|
||||||
|
|
||||||
|
size_t device_count = 0U;
|
||||||
|
size_t storage_count = 0U;
|
||||||
|
size_t entry_count = 0U;
|
||||||
|
size_t index;
|
||||||
|
bool folder_created = false;
|
||||||
|
|
||||||
|
if (overview == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
overview,
|
||||||
|
0,
|
||||||
|
sizeof(*overview)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)trainlog_database_exercise_count(
|
||||||
|
database,
|
||||||
|
&overview->local_exercise_count
|
||||||
|
);
|
||||||
|
|
||||||
|
if (trainlog_usb_list_mtp_devices(
|
||||||
|
devices,
|
||||||
|
SYNC_DEVICE_CAPACITY,
|
||||||
|
&device_count
|
||||||
|
) != TRAINLOG_STATUS_OK ||
|
||||||
|
device_count == 0U) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
overview->connected = true;
|
||||||
|
overview->device_count = device_count;
|
||||||
|
overview->device = devices[0];
|
||||||
|
|
||||||
|
if (trainlog_mtp_list_storages(
|
||||||
|
overview->device.bus_number,
|
||||||
|
overview->device.device_number,
|
||||||
|
storages,
|
||||||
|
SYNC_STORAGE_CAPACITY,
|
||||||
|
&storage_count
|
||||||
|
) != TRAINLOG_STATUS_OK ||
|
||||||
|
storage_count == 0U) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
overview->storage_ready = true;
|
||||||
|
overview->storage = storages[0];
|
||||||
|
|
||||||
|
if (trainlog_mtp_ensure_root_folder(
|
||||||
|
overview->device.bus_number,
|
||||||
|
overview->device.device_number,
|
||||||
|
overview->storage.storage_id,
|
||||||
|
"Trainlog",
|
||||||
|
&overview->exchange_folder_id,
|
||||||
|
&folder_created
|
||||||
|
) != TRAINLOG_STATUS_OK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)folder_created;
|
||||||
|
overview->exchange_ready = true;
|
||||||
|
|
||||||
|
if (trainlog_mtp_list_folder(
|
||||||
|
overview->device.bus_number,
|
||||||
|
overview->device.device_number,
|
||||||
|
overview->storage.storage_id,
|
||||||
|
overview->exchange_folder_id,
|
||||||
|
entries,
|
||||||
|
SYNC_ENTRY_CAPACITY,
|
||||||
|
&entry_count
|
||||||
|
) != TRAINLOG_STATUS_OK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
overview->remote_entry_count =
|
||||||
|
entry_count;
|
||||||
|
|
||||||
|
for (index = 0U;
|
||||||
|
index < entry_count;
|
||||||
|
++index) {
|
||||||
|
if (!entries[index].folder &&
|
||||||
|
sync_name_has_json_suffix(
|
||||||
|
entries[index].name
|
||||||
|
)) {
|
||||||
|
++overview->remote_json_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void screen_sync(
|
||||||
|
TrainlogDatabase *database
|
||||||
|
)
|
||||||
|
{
|
||||||
|
size_t selected = 0U;
|
||||||
|
int nav_selected = 5;
|
||||||
|
int focus = 1;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
TrainlogSyncOverview overview;
|
||||||
|
bool large_layout =
|
||||||
|
COLS >= 100 &&
|
||||||
|
LINES >= 30;
|
||||||
|
|
||||||
|
const size_t item_count = 4U;
|
||||||
|
size_t top = 0U;
|
||||||
|
size_t index;
|
||||||
|
int list_top;
|
||||||
|
int list_bottom;
|
||||||
|
int first_row;
|
||||||
|
int visible_rows;
|
||||||
|
int key;
|
||||||
|
|
||||||
|
int saved_stdout = -1;
|
||||||
|
int saved_stderr = -1;
|
||||||
|
int null_fd = -1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* libmtp may print raw-device identification directly to stdout or
|
||||||
|
* stderr. Redirect both temporarily so backend diagnostics cannot
|
||||||
|
* corrupt ncurses' physical-screen state.
|
||||||
|
*/
|
||||||
|
(void)fflush(stdout);
|
||||||
|
(void)fflush(stderr);
|
||||||
|
|
||||||
|
saved_stdout =
|
||||||
|
dup(STDOUT_FILENO);
|
||||||
|
|
||||||
|
saved_stderr =
|
||||||
|
dup(STDERR_FILENO);
|
||||||
|
|
||||||
|
null_fd =
|
||||||
|
open(
|
||||||
|
"/dev/null",
|
||||||
|
O_WRONLY | O_CLOEXEC
|
||||||
|
);
|
||||||
|
|
||||||
|
if (saved_stdout >= 0 &&
|
||||||
|
saved_stderr >= 0 &&
|
||||||
|
null_fd >= 0) {
|
||||||
|
(void)dup2(
|
||||||
|
null_fd,
|
||||||
|
STDOUT_FILENO
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)dup2(
|
||||||
|
null_fd,
|
||||||
|
STDERR_FILENO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_load_overview(
|
||||||
|
database,
|
||||||
|
&overview
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)fflush(stdout);
|
||||||
|
(void)fflush(stderr);
|
||||||
|
|
||||||
|
if (saved_stdout >= 0) {
|
||||||
|
(void)dup2(
|
||||||
|
saved_stdout,
|
||||||
|
STDOUT_FILENO
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)close(
|
||||||
|
saved_stdout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saved_stderr >= 0) {
|
||||||
|
(void)dup2(
|
||||||
|
saved_stderr,
|
||||||
|
STDERR_FILENO
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)close(
|
||||||
|
saved_stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null_fd >= 0) {
|
||||||
|
(void)close(
|
||||||
|
null_fd
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Force a complete physical redraw. This also protects the page if a
|
||||||
|
* backend writes directly to the terminal despite stdio redirection.
|
||||||
|
*/
|
||||||
|
clear();
|
||||||
|
clearok(
|
||||||
|
stdscr,
|
||||||
|
TRUE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!large_layout) {
|
||||||
|
draw_shell(
|
||||||
|
"TRAINLOG — Sync",
|
||||||
|
"↑↓ parcourir r actualiser b/Échap retour"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!overview.connected) {
|
||||||
|
mvprintw(
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
"Aucun appareil Android MTP connecté."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mvprintw(
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
"Appareil : %.*s",
|
||||||
|
COLS - 16,
|
||||||
|
overview.device.model
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
6,
|
||||||
|
4,
|
||||||
|
"Séances : %zu JSON candidat(s)",
|
||||||
|
overview.remote_json_count
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
7,
|
||||||
|
4,
|
||||||
|
"Catalogue PC : %zu exercice(s)",
|
||||||
|
overview.local_exercise_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
key = getch();
|
||||||
|
|
||||||
|
if (key == 'b' ||
|
||||||
|
key == 'B' ||
|
||||||
|
key == 27) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list_top = 17;
|
||||||
|
list_bottom = LINES - 4;
|
||||||
|
first_row = list_top + 2;
|
||||||
|
visible_rows =
|
||||||
|
list_bottom -
|
||||||
|
first_row;
|
||||||
|
|
||||||
|
if (visible_rows < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected >= item_count) {
|
||||||
|
selected =
|
||||||
|
item_count - 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected >=
|
||||||
|
(size_t)visible_rows) {
|
||||||
|
top =
|
||||||
|
selected -
|
||||||
|
(size_t)visible_rows +
|
||||||
|
1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
box(
|
||||||
|
stdscr,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
section_ascii_header(
|
||||||
|
":: S Y N C ::"
|
||||||
|
);
|
||||||
|
|
||||||
|
primary_top_navbar(
|
||||||
|
5,
|
||||||
|
nav_selected,
|
||||||
|
focus == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
dashboard_panel(
|
||||||
|
11,
|
||||||
|
2,
|
||||||
|
16,
|
||||||
|
COLS - 3,
|
||||||
|
"APPAREIL CONNECTE"
|
||||||
|
);
|
||||||
|
|
||||||
|
focused_panel(
|
||||||
|
list_top,
|
||||||
|
2,
|
||||||
|
list_bottom,
|
||||||
|
COLS - 3,
|
||||||
|
"SYNCHRONISATION",
|
||||||
|
focus == 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!overview.connected) {
|
||||||
|
attron(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_WARNING
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
13,
|
||||||
|
5,
|
||||||
|
"Aucun appareil Android en mode partage de fichiers."
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_WARNING
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
14,
|
||||||
|
5,
|
||||||
|
"%.*s",
|
||||||
|
COLS - 10,
|
||||||
|
"Branchez et déverrouillez le téléphone puis choisissez Transfert de fichiers."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
attron(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_SUCCESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
12,
|
||||||
|
5,
|
||||||
|
"✓ MTP direct connecté"
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_SUCCESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
13,
|
||||||
|
5,
|
||||||
|
"%.*s",
|
||||||
|
COLS - 10,
|
||||||
|
overview.device.model[0] != '\0'
|
||||||
|
? overview.device.model
|
||||||
|
: overview.device.vendor
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
14,
|
||||||
|
5,
|
||||||
|
"USB %03u:%03u %04x:%04x série %.*s",
|
||||||
|
overview.device.bus_number,
|
||||||
|
overview.device.device_number,
|
||||||
|
overview.device.vendor_id,
|
||||||
|
overview.device.product_id,
|
||||||
|
COLS - 48,
|
||||||
|
overview.device.serial[0] != '\0'
|
||||||
|
? overview.device.serial
|
||||||
|
: "—"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overview.storage_ready) {
|
||||||
|
mvprintw(
|
||||||
|
15,
|
||||||
|
5,
|
||||||
|
"%.*s · %.2f GiB libres / %.2f GiB",
|
||||||
|
24,
|
||||||
|
overview.storage.description[0] != '\0'
|
||||||
|
? overview.storage.description
|
||||||
|
: "Stockage MTP",
|
||||||
|
sync_bytes_to_gib(
|
||||||
|
overview.storage.free_space_bytes
|
||||||
|
),
|
||||||
|
sync_bytes_to_gib(
|
||||||
|
overview.storage.max_capacity_bytes
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index = 0U;
|
||||||
|
index < (size_t)visible_rows &&
|
||||||
|
top + index < item_count;
|
||||||
|
++index) {
|
||||||
|
size_t absolute =
|
||||||
|
top + index;
|
||||||
|
|
||||||
|
int row =
|
||||||
|
first_row +
|
||||||
|
(int)index;
|
||||||
|
|
||||||
|
const char *direction;
|
||||||
|
const char *category;
|
||||||
|
char status[128];
|
||||||
|
|
||||||
|
switch (absolute) {
|
||||||
|
case 0U:
|
||||||
|
direction = "↓";
|
||||||
|
category =
|
||||||
|
"Séances Android -> PC";
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
status,
|
||||||
|
sizeof(status),
|
||||||
|
"%zu JSON candidat(s) à analyser",
|
||||||
|
overview.remote_json_count
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1U:
|
||||||
|
direction = "↓";
|
||||||
|
category =
|
||||||
|
"Exercices Android -> PC";
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
status,
|
||||||
|
sizeof(status),
|
||||||
|
"%s",
|
||||||
|
"import automatique avec séance valide"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2U:
|
||||||
|
direction = "↓";
|
||||||
|
category =
|
||||||
|
"Mensurations Android -> PC";
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
status,
|
||||||
|
sizeof(status),
|
||||||
|
"%s",
|
||||||
|
"import automatique avec séance valide"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3U:
|
||||||
|
default:
|
||||||
|
direction = "↑";
|
||||||
|
category =
|
||||||
|
"Catalogue PC -> Android";
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
status,
|
||||||
|
sizeof(status),
|
||||||
|
"%zu exercice(s) locaux à publier",
|
||||||
|
overview.local_exercise_count
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focus == 1 &&
|
||||||
|
absolute == selected) {
|
||||||
|
attron(
|
||||||
|
A_REVERSE |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_ACCENT
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
row,
|
||||||
|
5,
|
||||||
|
" %s %-30.30s %-.*s ",
|
||||||
|
direction,
|
||||||
|
category,
|
||||||
|
COLS - 45,
|
||||||
|
status
|
||||||
|
);
|
||||||
|
|
||||||
|
if (focus == 1 &&
|
||||||
|
absolute == selected) {
|
||||||
|
attroff(
|
||||||
|
A_REVERSE |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_ACCENT
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overview.exchange_ready &&
|
||||||
|
LINES > 31) {
|
||||||
|
attron(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_MUTED
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
list_bottom - 2,
|
||||||
|
5,
|
||||||
|
"Zone Trainlog : %zu objet(s) · JSON séance v1 gelé · snapshot catalogue séparé",
|
||||||
|
overview.remote_entry_count
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_MUTED
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
section_scrollbar(
|
||||||
|
first_row,
|
||||||
|
list_bottom - 1,
|
||||||
|
COLS - 5,
|
||||||
|
selected,
|
||||||
|
item_count,
|
||||||
|
(size_t)visible_rows
|
||||||
|
);
|
||||||
|
|
||||||
|
attron(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_MUTED
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
LINES - 2,
|
||||||
|
2,
|
||||||
|
"%.*s",
|
||||||
|
COLS - 4,
|
||||||
|
"Tab zone ←→ menu ↑↓/PgUp/PgDn parcourir Entrée ouvrir r actualiser 0-5/F1-F5 direct b/Échap retour"
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_MUTED
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
touchwin(
|
||||||
|
stdscr
|
||||||
|
);
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
key = getch();
|
||||||
|
|
||||||
|
if (key == '\t' ||
|
||||||
|
key == KEY_BTAB) {
|
||||||
|
focus =
|
||||||
|
focus == 0
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primary_top_nav_forward(
|
||||||
|
key
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == 'b' ||
|
||||||
|
key == 'B' ||
|
||||||
|
key == 27) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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 (key == KEY_UP) {
|
||||||
|
selected =
|
||||||
|
selected > 0U
|
||||||
|
? selected - 1U
|
||||||
|
: 0U;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == KEY_DOWN) {
|
||||||
|
selected =
|
||||||
|
selected + 1U < item_count
|
||||||
|
? selected + 1U
|
||||||
|
: item_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 < item_count
|
||||||
|
? selected + jump
|
||||||
|
: item_count - 1U;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 'r' intentionally reaches the next loop iteration. Every iteration
|
||||||
|
* performs a fresh USB/MTP scan before repainting the page.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int trainlog_tui_run(TrainlogDatabase *database)
|
int trainlog_tui_run(TrainlogDatabase *database)
|
||||||
{
|
{
|
||||||
if (database == NULL) {
|
if (database == NULL) {
|
||||||
|
|
@ -8217,6 +8945,9 @@ int trainlog_tui_run(TrainlogDatabase *database)
|
||||||
case DASHBOARD_BODY:
|
case DASHBOARD_BODY:
|
||||||
screen_body(database);
|
screen_body(database);
|
||||||
break;
|
break;
|
||||||
|
case DASHBOARD_SYNC:
|
||||||
|
screen_sync(database);
|
||||||
|
break;
|
||||||
case DASHBOARD_QUIT:
|
case DASHBOARD_QUIT:
|
||||||
endwin();
|
endwin();
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue