290 lines
10 KiB
Rust
290 lines
10 KiB
Rust
use crate::client::api::{ApiClient, UreqTransport};
|
|
use crate::client::event_stream::{StreamItem, follow_session_events};
|
|
use crate::client::transcript_fold::{
|
|
TranscriptItem, fold_event, fold_page, group_tool_runs, raw_seq,
|
|
};
|
|
use crate::ui::{self, TranscriptScreen};
|
|
use event_model::SeqEvent;
|
|
use iris::android::{AndroidAppState, AndroidUiState};
|
|
use iris::prelude::*;
|
|
use std::{
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicU64, Ordering},
|
|
},
|
|
thread,
|
|
};
|
|
|
|
#[derive(AndroidUiState)]
|
|
pub struct TranscriptClient {
|
|
ui_state: AndroidUiState,
|
|
/// The screen's own content -- everything under the fixed
|
|
/// [`frame_report_controls`] bar, which is built once (`new`, below)
|
|
/// and never touched by `show_message`/`rebuild_transcript`'s own
|
|
/// `set` calls the way `desktop-app`'s `transcript_ptr` isn't touched
|
|
/// by rebuilding the session list beside it.
|
|
content: WeakWidget<WidgetPtr>,
|
|
screen: Option<TranscriptScreen>,
|
|
items: Vec<TranscriptItem>,
|
|
session_id: Option<String>,
|
|
generation: Arc<AtomicU64>,
|
|
}
|
|
|
|
fn build_transport() -> Result<UreqTransport, String> {
|
|
crate::android::enrollment::transport()
|
|
}
|
|
|
|
fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
|
|
wtext(message.to_string())
|
|
.color(PaintId::WHITE)
|
|
.overflow(TextOverflow::Wrap)
|
|
.pad(16)
|
|
.add_strong(rsc)
|
|
.any()
|
|
}
|
|
|
|
fn frame_report_controls(rsc: &mut StdRsc<TranscriptClient>) -> WeakWidget {
|
|
type Rsc = StdRsc<TranscriptClient>;
|
|
let report_rect = rect(Srgba8::rgb(50, 50, 60))
|
|
.on(
|
|
CursorSense::click(),
|
|
|ctx: EventIdCtx<'_, Rsc, _, _>, _rsc: &mut Rsc| match ctx
|
|
.state
|
|
.android_state()
|
|
.frame_report
|
|
.report()
|
|
{
|
|
Some(stats) => log::info!("iris frame report: {stats}"),
|
|
None => log::info!(
|
|
"iris frame report: no frames recorded -- scroll first, then press this"
|
|
),
|
|
},
|
|
)
|
|
.label("Frame report");
|
|
let report = (
|
|
report_rect,
|
|
wtext("Frame report").size(18).text_align(Align::CENTER),
|
|
)
|
|
.stack()
|
|
.pad(8)
|
|
.add(rsc);
|
|
|
|
let reset_rect = rect(Srgba8::rgb(70, 40, 40))
|
|
.on(
|
|
CursorSense::click(),
|
|
|ctx: EventIdCtx<'_, Rsc, _, _>, _rsc: &mut Rsc| {
|
|
ctx.state.android_state_mut().frame_report.reset();
|
|
log::info!("iris frame report: reset");
|
|
},
|
|
)
|
|
.label("Reset frame report");
|
|
let reset = (
|
|
reset_rect,
|
|
wtext("Reset").size(18).text_align(Align::CENTER),
|
|
)
|
|
.stack()
|
|
.pad(8)
|
|
.add(rsc);
|
|
|
|
(report, reset).span(Dir::RIGHT).height(56).add(rsc)
|
|
}
|
|
|
|
impl TranscriptClient {
|
|
pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
|
crate::ui::register_fonts(&mut rsc.ui);
|
|
let content = WidgetPtr::new().add(rsc);
|
|
let loading = placeholder(rsc, "Loading sessions...");
|
|
content(rsc).set(loading);
|
|
|
|
let tree = (frame_report_controls(rsc), content.height(rest(1)))
|
|
.span(Dir::DOWN)
|
|
.add_strong(rsc)
|
|
.any();
|
|
ui_state.set_root(rsc, tree);
|
|
|
|
let mut client = Self {
|
|
ui_state,
|
|
content,
|
|
screen: None,
|
|
items: Vec::new(),
|
|
session_id: None,
|
|
generation: Arc::new(AtomicU64::new(0)),
|
|
};
|
|
client.spawn_fetch_sessions(rsc);
|
|
client
|
|
}
|
|
}
|
|
|
|
impl AndroidAppState for TranscriptClient {
|
|
type Resources = StdRsc<Self>;
|
|
|
|
fn back_pressed(&mut self, _rsc: &mut Self::Resources) -> bool {
|
|
// No screen stack of its own -- same "let the activity finish"
|
|
// answer `iris-android-app`'s tabs `Client` already gives.
|
|
false
|
|
}
|
|
}
|
|
|
|
impl TranscriptClient {
|
|
fn show_message(&mut self, rsc: &mut StdRsc<Self>, message: &str) {
|
|
let widget = placeholder(rsc, message);
|
|
(self.content)(rsc).set(widget);
|
|
self.screen = None;
|
|
}
|
|
|
|
fn spawn_fetch_sessions(&mut self, rsc: &mut StdRsc<Self>) {
|
|
let my_generation = self.generation.load(Ordering::SeqCst);
|
|
let generation = self.generation.clone();
|
|
rsc.spawn_task(async move |mut ctx| {
|
|
let outcome = match build_transport() {
|
|
Ok(transport) => ApiClient::new(transport)
|
|
.fetch_sessions()
|
|
.map_err(|e| e.to_string()),
|
|
Err(e) => Err(format!("couldn't set up TLS: {e}")),
|
|
};
|
|
ctx.update(move |state: &mut TranscriptClient, rsc| {
|
|
if generation.load(Ordering::SeqCst) != my_generation {
|
|
return;
|
|
}
|
|
match outcome {
|
|
Ok(sessions) => match sessions.into_iter().next() {
|
|
Some(session) => state.select_session(rsc, session.id),
|
|
None => state.show_message(rsc, "No sessions on the sandbox server."),
|
|
},
|
|
Err(message) => {
|
|
state.show_message(rsc, &format!("Couldn't list sessions: {message}"))
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn select_session(&mut self, rsc: &mut StdRsc<Self>, session_id: String) {
|
|
let my_generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
|
|
self.items.clear();
|
|
self.session_id = Some(session_id.clone());
|
|
self.show_message(rsc, "Loading transcript...");
|
|
|
|
let live_generation = self.generation.clone();
|
|
rsc.spawn_task(async move |mut ctx| {
|
|
let transports =
|
|
build_transport().and_then(|rest| build_transport().map(|stream| (rest, stream)));
|
|
let (rest, stream_transport) = match transports {
|
|
Ok(pair) => pair,
|
|
Err(e) => {
|
|
let message = format!("couldn't set up TLS: {e}");
|
|
ctx.update(move |state: &mut TranscriptClient, rsc| {
|
|
if live_generation.load(Ordering::SeqCst) == my_generation {
|
|
state.show_message(rsc, &message);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
let api = ApiClient::new(rest);
|
|
|
|
let page: Result<Vec<serde_json::Value>, String> = api
|
|
.fetch_transcript_page(&session_id, None, 200, true)
|
|
.map_err(|e| e.to_string());
|
|
let after = page
|
|
.as_ref()
|
|
.ok()
|
|
.and_then(|values| values.last())
|
|
.and_then(raw_seq)
|
|
.unwrap_or(0);
|
|
let result = page.and_then(|values| fold_page(&values));
|
|
|
|
{
|
|
let live_generation = live_generation.clone();
|
|
ctx.update(move |state: &mut TranscriptClient, rsc| {
|
|
if live_generation.load(Ordering::SeqCst) != my_generation {
|
|
return;
|
|
}
|
|
match result {
|
|
Ok(items) => {
|
|
state.items = items;
|
|
state.rebuild_transcript(rsc);
|
|
}
|
|
Err(message) => {
|
|
state.show_message(rsc, &format!("Couldn't load transcript: {message}"))
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if live_generation.load(Ordering::SeqCst) != my_generation {
|
|
return;
|
|
}
|
|
let _ =
|
|
follow_session_events(
|
|
&stream_transport,
|
|
&session_id,
|
|
after,
|
|
move |item| match item {
|
|
StreamItem::Open | StreamItem::Reset => {
|
|
live_generation.load(Ordering::SeqCst) == my_generation
|
|
}
|
|
StreamItem::Event { event, .. } => {
|
|
if live_generation.load(Ordering::SeqCst) != my_generation {
|
|
return false;
|
|
}
|
|
let live_generation = live_generation.clone();
|
|
ctx.update(move |state: &mut TranscriptClient, rsc| {
|
|
if live_generation.load(Ordering::SeqCst) != my_generation {
|
|
return;
|
|
}
|
|
state.apply_event(rsc, &event);
|
|
});
|
|
true
|
|
}
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
fn rebuild_transcript(&mut self, rsc: &mut StdRsc<Self>) {
|
|
let in_progress = self
|
|
.screen
|
|
.as_ref()
|
|
.map(|screen| (screen.composer.field)(rsc).text().to_string())
|
|
.filter(|t| !t.is_empty());
|
|
|
|
let rows = group_tool_runs(&self.items);
|
|
let (screen, tree) = ui::build_tree(rsc, rows);
|
|
|
|
if let Some(text) = in_progress {
|
|
(screen.composer.field)(rsc).set(&text);
|
|
}
|
|
if let Some(session_id) = self.session_id.clone() {
|
|
let field = screen.composer.field;
|
|
rsc.register_event(field, Submit, move |ctx, rsc| {
|
|
let text = field(rsc).take();
|
|
let text = text.trim().to_string();
|
|
if !text.is_empty() {
|
|
ctx.state.send_message(session_id.clone(), text);
|
|
}
|
|
});
|
|
}
|
|
|
|
(self.content)(rsc).set(tree);
|
|
self.screen = Some(screen);
|
|
}
|
|
|
|
fn apply_event(&mut self, rsc: &mut StdRsc<Self>, event: &SeqEvent) {
|
|
let old_items = self.items.clone();
|
|
self.items = fold_event(&self.items, event);
|
|
match self.screen.as_mut() {
|
|
Some(screen) => screen.apply(rsc, &old_items, &self.items),
|
|
None => self.rebuild_transcript(rsc),
|
|
}
|
|
}
|
|
|
|
fn send_message(&mut self, session_id: String, text: String) {
|
|
thread::spawn(move || {
|
|
if let Ok(transport) = build_transport() {
|
|
let api = ApiClient::new(transport);
|
|
let _ = api.send_message(&session_id, &text, &[]);
|
|
}
|
|
});
|
|
}
|
|
}
|