Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/i3rs-app/Packager.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
product-name = "i3rs"
name = "i3rs-app"
version = "0.4.0"
version = "0.5.0"
identifier = "io.github.friss.i3rs"
publisher = "i3rs contributors"
authors = ["i3rs contributors"]
Expand Down
104 changes: 72 additions & 32 deletions crates/i3rs-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>i3rs Web</title>
<link rel="icon" href="i3rs.ico" />
<link data-trunk rel="rust" href="Cargo.toml" data-target-name="i3rs_app" />
<link data-trunk rel="copy-file" href="packaging/icons/i3rs.ico" />
<link data-trunk rel="copy-file" href="../../test_data/VIR_LAP.ld" />
<link data-trunk rel="copy-file" href="../../test_data/VIR_LAP.ldx" />
<style>
Expand All @@ -21,24 +23,41 @@
}

body {
position: relative;
}

.load-overlay {
position: fixed;
inset: 0;
z-index: 10;
display: grid;
grid-template-rows: auto 1fr;
place-items: center;
padding: 1rem;
background: rgba(15, 17, 21, 0.5);
backdrop-filter: blur(3px);
}

.load-overlay[hidden] {
display: none;
}

.app-shell {
padding: 0.75rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0));
.load-modal {
width: min(40rem, 100%);
padding: 1rem;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 0.5rem;
background: #171a20;
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.35);
}

.app-shell h1 {
.load-modal h1 {
margin: 0;
font-size: 1rem;
font-weight: 650;
letter-spacing: 0.02em;
}

.app-shell p {
.load-modal p {
margin: 0.3rem 0 0;
font-size: 0.9rem;
opacity: 0.75;
Expand Down Expand Up @@ -133,30 +152,32 @@
</style>
</head>
<body>
<div class="app-shell">
<h1>i3rs Web</h1>
<p>Open a MoTeC <code>.ld</code> file from the File menu inside the app.</p>
<div class="load-controls">
<label>
LD file
<input id="ld-file" type="file" accept=".ld" />
</label>
<label>
Optional LDX file
<input id="ldx-file" type="file" accept=".ldx" />
</label>
<button id="load-session" type="button">Load Session</button>
<button id="load-vir-lap" type="button">Load VIR_LAP Sample</button>
</div>
<div id="load-status" class="load-status" data-state="idle" aria-live="polite">
<span class="load-status-message">Web app starting...</span>
<canvas id="i3rs-canvas"></canvas>
<div id="load-overlay" class="load-overlay">
<div class="load-modal" role="dialog" aria-labelledby="load-title" aria-modal="true">
<h1 id="load-title">Open telemetry</h1>
<p>Select a MoTeC <code>.ld</code> file and optional <code>.ldx</code> sidecar together.</p>
<div class="load-controls">
<label for="session-files">
Session files
<input
id="session-files"
name="session-files"
type="file"
accept=".ld,.ldx"
multiple
/>
</label>
<button id="load-vir-lap" type="button">Load VIR_LAP Sample</button>
</div>
<div id="load-status" class="load-status" data-state="idle" aria-live="polite">
<span class="load-status-message">Web app starting...</span>
</div>
</div>
</div>
<canvas id="i3rs-canvas"></canvas>
<script>
const ldInput = document.getElementById("ld-file");
const ldxInput = document.getElementById("ldx-file");
const loadButton = document.getElementById("load-session");
const loadOverlay = document.getElementById("load-overlay");
const sessionFilesInput = document.getElementById("session-files");
const sampleButton = document.getElementById("load-vir-lap");
const statusEl = document.getElementById("load-status");
const statusMessageEl = statusEl.querySelector(".load-status-message");
Expand All @@ -165,9 +186,13 @@ <h1>i3rs Web</h1>

const readAsUint8Array = async (file) => new Uint8Array(await file.arrayBuffer());
const readAsText = async (file) => await file.text();
const extensionOf = (file) => file.name.split(".").pop()?.toLowerCase() ?? "";
const waitForPaint = async () =>
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const notifyLoadState = (state) => {
if (state.state !== "success") {
loadOverlay.hidden = false;
}
window.i3rsLoadState = state;
statusEl.dataset.state = state.state;
statusMessageEl.textContent = state.message;
Expand All @@ -180,7 +205,7 @@ <h1>i3rs Web</h1>
}
};
const setBusy = (busy) => {
loadButton.disabled = busy;
sessionFilesInput.disabled = busy;
sampleButton.disabled = busy;
};
const setLoadState = (state, message, extra = {}) => {
Expand Down Expand Up @@ -221,6 +246,8 @@ <h1>i3rs Web</h1>
parseDurationMs,
summary,
});
await waitForPaint();
loadOverlay.hidden = true;
return summary;
};

Expand All @@ -241,12 +268,19 @@ <h1>i3rs Web</h1>
setLoadState("error", "Web app startup timed out.");
};

loadButton.addEventListener("click", async () => {
const ldFile = ldInput.files && ldInput.files[0];
const loadSelectedFiles = async () => {
const selectedFiles = Array.from(sessionFilesInput.files ?? []);
const ldFiles = selectedFiles.filter((file) => extensionOf(file) === "ld");
const ldxFiles = selectedFiles.filter((file) => extensionOf(file) === "ldx");
const ldFile = ldFiles[0];
if (!ldFile) {
setLoadState("error", "Choose an .ld file first.");
return;
}
if (ldFiles.length > 1) {
setLoadState("error", "Choose only one .ld file.");
return;
}

setBusy(true);
try {
Expand All @@ -256,7 +290,7 @@ <h1>i3rs Web</h1>
phase: "read",
});
const ldBytes = await readAsUint8Array(ldFile);
const ldxFile = ldxInput.files && ldxInput.files[0];
const ldxFile = ldxFiles[0];
let ldxText = null;
Comment on lines +293 to 294
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject multiple .ldx files in the combined picker

The combined picker now accepts multiple files, but only .ld count is validated; .ldx uses ldxFiles[0] and silently ignores additional sidecars. If a user selects more than one .ldx, the app may attach the wrong sidecar without warning, leading to incorrect channel metadata for the loaded session.

Useful? React with 👍 / 👎.

if (ldxFile) {
setLoadState("loading", `Reading ${ldxFile.name}...`, {
Expand All @@ -277,6 +311,12 @@ <h1>i3rs Web</h1>
} finally {
setBusy(false);
}
};

sessionFilesInput.addEventListener("change", () => {
if (sessionFilesInput.files && sessionFilesInput.files.length > 0) {
void loadSelectedFiles();
}
Comment on lines +316 to +319
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset file input to allow selecting the same session again

The new auto-load flow only runs on the file input change event, but the input value is never cleared after success or failure. In browsers, choosing the same file list again does not fire change, so after a failed parse/startup attempt users cannot retry the same .ld/.ldx pair from this overlay without first selecting a different file or refreshing.

Useful? React with 👍 / 👎.

});

sampleButton.addEventListener("click", async () => {
Expand Down
5 changes: 5 additions & 0 deletions crates/i3rs-app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ fn apply_theme_to_ctx(ctx: &egui::Context, theme: ThemeChoice) {
}

pub struct App {
egui_ctx: egui::Context,
shared: SharedState,
worksheets: Vec<Worksheet>,
active_worksheet: usize,
Expand Down Expand Up @@ -167,6 +168,7 @@ impl App {
}];
apply_theme_to_ctx(&cc.egui_ctx, preferences.theme);
Self {
egui_ctx: cc.egui_ctx.clone(),
shared,
worksheets,
active_worksheet: 0,
Expand Down Expand Up @@ -370,6 +372,8 @@ impl App {
if let Some(path) = self.shared.ld_path.clone() {
self.register_project_session(&path);
}

self.egui_ctx.request_repaint();
}

fn submit_load_session(
Expand Down Expand Up @@ -2166,6 +2170,7 @@ mod tests {
#[cfg(not(target_arch = "wasm32"))]
let (native_pick_tx, native_pick_rx) = crate::platform::native_pick_channel();
App {
egui_ctx: egui::Context::default(),
shared,
worksheets,
active_worksheet: 0,
Expand Down
8 changes: 6 additions & 2 deletions crates/i3rs-app/src/background_jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ use std::sync::Arc;

use crate::state::{ChannelStats, DownsampleSeriesKey, compute_channel_stats};
use i3rs_core::{
ChannelData, DownsampledPoint, Lap, LdFile, LdxFile, TrackData, detect_laps, downsample_minmax,
evaluate_expression_with_aliases, extract_gps_track, find_ldx_for_ld,
ChannelData, DownsampledPoint, Lap, LdFile, LdxFile, TrackData, detect_laps,
evaluate_expression_with_aliases, find_ldx_for_ld,
};
#[cfg(not(target_arch = "wasm32"))]
use i3rs_core::{downsample_minmax, extract_gps_track};

pub struct LoadedSession {
pub file_name: String,
Expand Down Expand Up @@ -180,6 +182,7 @@ fn perform_decode_physical_channel(
})
}

#[cfg(not(target_arch = "wasm32"))]
fn perform_build_track_data(ld: Arc<LdFile>) -> Option<TrackData> {
let _perf = crate::perf_metrics::scope("track-map draw");
extract_gps_track(&ld)
Expand All @@ -201,6 +204,7 @@ fn perform_evaluate_math_channel(
})
}

#[cfg(not(target_arch = "wasm32"))]
fn perform_build_downsampled_series(
data: Arc<[f64]>,
freq: u16,
Expand Down
9 changes: 8 additions & 1 deletion crates/i3rs-app/src/panels/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2721,7 +2721,14 @@ impl GraphPanel {

let picker_ctx = viewport_ui.ctx().clone();
let ui = &mut *viewport_ui;
ui.label("Channels");
ui.horizontal(|ui| {
ui.label("Channels");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Close").clicked() {
open = false;
}
});
});
ui.small(
"Drag rows onto the drop lines to reorder them or move them into another graph.",
);
Expand Down
Loading