//! Hosted dialog. Main-header integration is a small caller-owned hook. use super::{Event, Operation, client::Secret}; use crate::{ app::{App, Message}, style, views::{action, label}, }; use iced::{ Element, Fill, Font, widget::{self, button, column, container, row, scrollable, text, text_editor, text_input}, }; use std::collections::BTreeSet; pub fn overlay<'a>(app: &'a App, base: Element<'a, Message>) -> Element<'a, Message> { if !app.hosted.visible || app.pending.is_some() || app.file_prompt.is_some() || app.workflow.is_some() || app.delete_pending { return base; } widget::stack![ base, widget::opaque( container( container(dialog(app)) .padding(22) .width((app.window_size.width - 64.).clamp(680., 1000.)) .height((app.window_size.height - 80.).max(400.)) .style(style::card) ) .center(Fill) .style(|_| container::Style { background: Some(iced::Color::from_rgba(0.08, 0.12, 0.18, 0.38).into()), ..Default::default() }) ) ] .into() } fn control<'a>(title: &'a str, event: Event, enabled: bool) -> widget::Button<'a, Message> { button(text(title).size(12)) .padding([9, 12]) .style(style::nav) .on_press_maybe(enabled.then_some(Message::Hosted(event))) } fn dialog(app: &App) -> Element<'_, Message> { let state = &app.hosted; let enabled = !app.busy; if let Some(path) = &state.review_path { return column![ text("Server source - read only").size(22), text(path).size(13), text_editor(&state.review_editor) .font(Font::MONOSPACE) .size(13) .height(Fill) .on_action(|action| Message::Hosted(Event::ReviewAction(action))), control("Back to review", Event::ReviewBack, enabled), ] .spacing(12) .into(); } let mut content = column![ label("SERVICE ADDRESS"), text_input("http://127.0.0.1:8787", &state.base) .padding(10) .on_input_maybe(enabled.then_some(|value| Message::Hosted(Event::Base(value)))), label("BEARER TOKEN - KEPT IN MEMORY ONLY"), text_input("Optional for a loopback service", &state.token.0) .secure(true) .padding(10) .on_input_maybe( enabled.then_some(|value| Message::Hosted(Event::Token(Secret(value)))) ), row![ control("Connect / refresh", Event::Connect, enabled), control( "Forget connection", Event::Forget, enabled && state.session.is_none() ), ] .spacing(8), ] .spacing(8); if let Some(session) = &state.session { content = content.push(container(column![ text("Current hosted model").size(16), text(session.connection.base()).size(12), text(format!("Resource {} - revision {}", session.resource_id, session.revision)).size(12), label("Edits and Undo stay local. Save sends the complete authored source project."), row![ control("Check server", Event::Check, enabled), control("Save as local...", Event::Local, enabled), ].spacing(8), ].spacing(8)).padding(12).style(style::panel)); } if let Some(error) = &state.error { content = content.push( container( column![ text(if error.uncertain { "The write outcome is unknown" } else { "Hosted action needs attention" }) .size(16), text(&error.detail).size(13), label("Your local model and source history are retained."), ] .spacing(7), ) .padding(12) .style(style::panel), ); } if let Some(failed) = &state.uncertain { if failed.operation == Operation::Publish { content = content.push(text("A previous publication may already exist. Inspect the catalog before publishing another copy.").size(13)); } if failed.resource_id.is_some() { content = content.push(control("Check previous write", Event::Check, enabled)); } } if let Some(review) = &state.review { let remote = &review.remote; let local = &app.document.bundle().files; let server = &remote.compiled.bundle.files; let paths: BTreeSet<_> = local.keys().chain(server.keys()).collect(); let mut changes = column![ text(format!("Review server revision {}", remote.revision)).size(17), label(format!("{} - resource {}", review.connection.base(), remote.resource_id)), label("Reload replaces accepted local content as one undoable change. Unapplied drafts are checked first."), ].spacing(8); if app.document.bundle().entry != remote.compiled.bundle.entry { changes = changes.push( text(format!( "Entry: {} -> {}", app.document.bundle().entry, remote.compiled.bundle.entry )) .size(12), ); } for path in paths { if local.get(path) != server.get(path) { let change = match (local.contains_key(path), server.contains_key(path)) { (false, true) => "Added on server", (true, false) => "Removed on server", _ => "Changed", }; changes = changes.push( row![ text(format!("{change}: {path}")).size(12).width(Fill), control( "View server text", Event::ReviewFile(path.clone()), enabled && server.contains_key(path) ), ] .spacing(8) .align_y(iced::Center), ); } } changes = changes.push(control("Reload server version", Event::Reload, enabled)); content = content.push(container(changes).padding(12).style(style::panel)); } content = content.push(text("Hosted model catalog").size(17)).push( row![ text_input("Search model names", &state.query) .padding(9) .on_input_maybe(enabled.then_some(|value| Message::Hosted(Event::Query(value)))) .on_submit(Message::Hosted(Event::Connect)), control("Search", Event::Connect, enabled), ] .spacing(8), ); if let Some(message) = state.catalog_message() { content = content.push(label(message)); } for item in &state.catalog { let selected = state.selected == Some(item.resource_id); content = content.push( button( column![ text(&item.name).size(14), label(format!( "{} - revision {} - {} objects / {} links", item.resource_id, item.revision, item.elements, item.relations )), ] .spacing(4), ) .padding(10) .width(Fill) .style(if selected { style::selected } else { style::nav }) .on_press_maybe(enabled.then_some(Message::Hosted(Event::Select(item.resource_id)))), ); } if !state.catalog_stale && state.next.is_some() { content = content.push(control("More models", Event::More, enabled)); } let publish_label = if state .uncertain .as_ref() .is_some_and(|r| r.operation == Operation::Publish) { "Publish another copy" } else { "Publish as new" }; column![ text("Hosted models").size(24), label("Optional shared storage for desktop, scripts and engineering integrations."), scrollable(content.spacing(12)).height(Fill), text(if app.busy { app.status.as_str() } else { "Publishing creates a separate hosted resource. Existing resources change only through Save to server." }).size(12), row![ control("Close", Event::Close, enabled), control(publish_label, Event::Publish, enabled), control("Open selected", Event::OpenSelected, enabled && state.selected.is_some() && state.connection.is_some()), control("Save to server", Event::Save, enabled && state.session.is_some() && state.review.is_none()), ].spacing(8), ].spacing(12).into() } // Kept here so a caller can integrate the feature without duplicating event wiring. pub fn header_action() -> widget::Button<'static, Message> { action("Hosted models...", Message::Hosted(Event::Show)) }