239 lines
9.1 KiB
Rust
239 lines
9.1 KiB
Rust
//! 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))
|
|
}
|