Files
lightcraft/crates/ui-egui/src/model_setup.rs
T
Claude d7f868fc6d LR-EDIT-DETAIL-DENOISE: dismissing the model setup turns AI Denoise back off (#922)
Turning on AI Denoise without a model opens Settings > AI Denoise and
keeps a pending request, so the switch showed checked and Settings
offered "Cancel pending action". Closing the setup with no model
download or installation running now drops that request: the switch
goes back off and nothing claims to be pending. A download started
before closing still turns AI Denoise on when it finishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKWB9TCnvzJHV6xMLAj2J4
2026-10-11 12:00:15 +00:00

155 lines
6.2 KiB
Rust

//! Resume a denoise request after model setup, bound to its original photo and library.
use crate::i18n::tr;
use crate::{LightcraftApp, state::Dialog};
use lightcraft_catalog::{PhotoId, Source};
use lightcraft_engine::denoise::PhotoState;
use serde_json::{Value, json};
struct Request {
library: u64,
photo: PhotoId,
source: Source,
}
impl Request {
fn capture(app: &LightcraftApp, id: PhotoId) -> Result<Self, String> {
let photo = app.session.catalog.photo(id).filter(|p| !p.deleted).ok_or("the requested photo is no longer available")?;
Ok(Self { library: app.session.library_generation(), photo: id, source: photo.source.clone() })
}
fn valid(&self, app: &LightcraftApp) -> bool {
self.library == app.session.library_generation()
&& app.session.catalog.photo(self.photo).is_some_and(|p| !p.deleted && p.source == self.source)
}
}
#[derive(Default)]
pub(crate) struct Pending {
denoise: Option<Request>,
next_check: f64,
}
pub(crate) fn denoise_requested(app: &LightcraftApp, id: PhotoId) -> bool {
app.model_setup.denoise.as_ref().is_some_and(|r| r.photo == id && r.valid(app))
}
pub(crate) fn intercept(app: &mut LightcraftApp, command: &str, params: &Value) -> Option<Result<Value, String>> {
if matches!(command, "app.openLibrary" | "file.restoreLibrary") {
app.model_setup = Pending::default();
return None;
}
if command == "modelSetup.cancel" {
if params.get("kind").and_then(Value::as_str) != Some("denoise") {
return Some(Err("modelSetup.cancel: use kind denoise".into()));
}
app.model_setup.denoise = None;
app.ui.status = tr("Pending photo action cancelled").into();
return Some(Ok(json!({"cancelled": true})));
}
if command == "denoise.models.downloadCancel" {
app.model_setup.denoise = None;
}
if command != "denoise.toggle" {
return None;
}
let id = match params.get("id") {
Some(v) => match v.as_u64() {
Some(id) => PhotoId(id),
None => return Some(Err("denoise.toggle: id must be a photo id".into())),
},
None => match app.session.active() {
Some(id) => id,
None => return Some(Err("select a raw photo".into())),
},
};
let enabled = app.session.develop_of(id).is_some_and(|d| d.enhance.denoise_enabled());
let on = match params.get("enabled") {
Some(v) => match v.as_bool() {
Some(on) => on,
None => return Some(Err("denoise.toggle: enabled must be true or false".into())),
},
None => !enabled && !denoise_requested(app, id),
};
if !on {
app.model_setup.denoise = None;
return Some(app.session.execute("denoise.toggle", &json!({"id": id.0, "enabled": false})).map_err(|e| e.to_string()));
}
let _ = app.session.execute("denoise.status", &json!({"id": id.0}));
if !matches!(app.session.denoise_photo_state(id), PhotoState::NoModel) {
return None;
}
if app.session.execute("denoise.models.list", &json!({})).ok().is_none_or(|v| v["dir"].is_null()) {
return None;
}
Some(Request::capture(app, id).map(|request| {
app.model_setup.denoise = Some(request);
app.model_setup.next_check = 0.0;
app.ui.dialog = Some(Dialog::Settings { tab: "denoise".into() });
app.ui.status = tr("Install a denoise model to enable AI Denoise on this photo").into();
json!({"setupRequired": true, "photo": id.0})
}))
}
pub(crate) fn notice(app: &mut LightcraftApp, ui: &mut egui::Ui, kind: &str) {
if kind != "denoise" || !app.model_setup.denoise.as_ref().is_some_and(|r| r.valid(app)) {
return;
}
ui.add(egui::Label::new(tr("After installation, AI Denoise will turn on for the requesting photo.")).wrap());
let r = ui.small_button(tr("Cancel pending action"));
crate::widgets::register(ui.ctx(), "modelSetup:cancel:denoise", r.rect);
if r.clicked() {
let _ = app.run("modelSetup.cancel", json!({"kind": "denoise"}));
}
ui.add_space(6.0);
}
/// The model setup was dismissed with nothing on the way: neither Settings nor a model's terms
/// are showing, no model download or installation is running, and the photo still has no model.
fn abandoned(app: &mut LightcraftApp) -> bool {
if matches!(app.ui.dialog, Some(Dialog::Settings { .. } | Dialog::DenoiseModel { .. })) {
return false;
}
let Some(photo) = app.model_setup.denoise.as_ref().map(|r| r.photo) else { return false };
if !matches!(app.session.denoise_photo_state(photo), PhotoState::NoModel) {
return false;
}
let Ok(v) = app.session.execute("denoise.models.downloads", &json!({})) else { return false };
!v["downloads"].as_array().into_iter().flatten().any(|d| matches!(d["state"].as_str(), Some("running" | "done" | "installing" | "installed")))
}
pub(crate) fn pump(app: &mut LightcraftApp, ctx: &egui::Context) {
if app.model_setup.denoise.is_none() {
return;
}
if abandoned(app) {
// the setup was closed without installing anything: the switch goes back off (issue #922)
app.model_setup.denoise = None;
app.ui.status = tr("Pending photo action cancelled").into();
return;
}
if app.session.interaction.is_some() {
ctx.request_repaint_after(std::time::Duration::from_millis(250));
return;
}
let now = ctx.input(|i| i.time);
if now < app.model_setup.next_check {
return;
}
app.model_setup.next_check = now + 0.25;
let Some(request) = app.model_setup.denoise.take() else { return };
if !request.valid(app) {
app.ui.status = tr("Pending photo action cancelled because its library or photo changed").into();
return;
}
if matches!(app.session.denoise_photo_state(request.photo), PhotoState::NoModel | PhotoState::NotApplicable) {
app.model_setup.denoise = Some(request);
ctx.request_repaint_after(std::time::Duration::from_millis(250));
return;
}
match app.run("denoise.toggle", json!({"id": request.photo.0, "enabled": true})) {
Ok(_) => app.ui.status = tr("AI Denoise is enabled on the requesting photo").into(),
Err(error) => app.ui.status = error,
}
}