feat(io): add user-approved recovery

This commit is contained in:
Hakan Seven committed 2026-08-03 12:47:48 +03:00
1 parent 25515fa33c
commit 9e90e50662
25 files changed
+2723 -198

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ pub mod shortcuts;
pub mod layers;
pub mod update_notice;
pub mod open_progress;
pub mod recovery;
pub mod options;
pub mod attribute_editor;
pub mod alias_editor;
+224
View File
@@ -0,0 +1,224 @@
use crate::app::Message;
use crate::io::recovery::{RecoveryReport, RecoveryStatus};
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Element, Fill, Theme};
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn status_style(
status: RecoveryStatus,
) -> impl Fn(&Theme) -> iced::widget::text::Style + Copy {
move |theme: &Theme| iced::widget::text::Style {
color: Some(match status {
RecoveryStatus::Recovered => theme.palette().warning.base.color,
RecoveryStatus::Failed => theme.palette().danger.base.color,
}),
}
}
fn metric<'a>(label: String, value: String) -> Element<'a, Message> {
container(
column![
text(label).size(10).style(muted_style),
text(value).size(18),
]
.spacing(3),
)
.padding([10, 12])
.width(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 5.0.into(),
},
..Default::default()
})
.into()
}
pub fn view_window<'a>(
report: &'a RecoveryReport,
allow_save_copy: bool,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let recovered = report.status == RecoveryStatus::Recovered;
let heading = if recovered {
crate::tr!("recovery-opened-with-repairs")
} else {
crate::tr!("recovery-open-failed")
};
let description = if recovered {
crate::tr!("recovery-repaired-description")
} else {
crate::tr!("recovery-failed-description")
};
let status = report.status;
let metrics = row![
metric(
crate::tr!("recovery-entities-checked"),
report.entities_scanned.to_string(),
),
metric(
crate::tr!("recovery-issues-found"),
report.issues_found().to_string(),
),
metric(
crate::tr!("recovery-entities-removed"),
report.removed_total().to_string(),
),
metric(
crate::tr!("recovery-references-checked"),
report.references_checked.to_string(),
),
]
.spacing(8)
.width(Fill);
let mut details = column![].spacing(6);
if report.referenced_entities_removed > 0 {
details = details.push(
text(format!(
"{}: {}",
crate::tr!("recovery-referenced-entities-removed"),
report.referenced_entities_removed
))
.size(11),
);
}
if report.references_missing > 0 || report.references_failed > 0 {
details = details.push(
text(format!(
"{}: {}",
crate::tr!("recovery-references-unavailable"),
report
.references_missing
.saturating_add(report.references_failed)
))
.size(11),
);
}
if let Some(error) = &report.error {
details = details.push(text(error).size(11));
}
for (kind, message) in report.diagnostics.iter().take(100) {
details = details.push(text(format!("[{kind}] {message}")).size(10));
}
if let Some(path) = &report.log_path {
details = details.push(
text(format!("{}: {}", crate::tr!("recovery-log-path"), path.display()))
.size(10)
.style(muted_style),
);
} else if let Some(error) = &report.log_error {
details = details.push(
text(format!(
"{}: {error}",
crate::tr!("recovery-log-write-failed")
))
.size(10)
.style(status_style(RecoveryStatus::Failed)),
);
} else if cfg!(target_arch = "wasm32") {
details = details.push(
text(crate::tr!("recovery-log-download-ready"))
.size(10)
.style(muted_style),
);
}
let detail_panel = container(scrollable(details).height(Fill))
.padding([10, 12])
.width(Fill)
.height(Fill)
.style(container::bordered_box);
let mut actions = row![Space::new().width(Fill)].spacing(8);
if recovered && report.save_as_required && allow_save_copy {
actions = actions.push(
button(text(crate::tr!("recovery-save-copy")).size(12))
.on_press(Message::RecoverySaveAs)
.style(button::primary)
.padding([6, 14]),
);
}
if report.log_path.is_some() || cfg!(target_arch = "wasm32") {
actions = actions.push(
button(text(crate::tr!("recovery-show-log")).size(12))
.on_press(Message::RecoveryShowLog)
.style(button::secondary)
.padding([6, 14]),
);
}
actions = actions.push(
button(text(crate::tr!("action-close")).size(12))
.on_press(Message::RecoveryClose)
.style(button::secondary)
.padding([6, 14]),
);
container(
column![
text(heading).size(20).style(status_style(status)),
text(&report.file_name).size(13),
text(description).size(11).style(muted_style),
Space::new().height(4),
metrics,
detail_panel,
actions,
]
.spacing(10)
.width(sizing.width)
.height(sizing.height),
)
.padding([12, 16])
.width(sizing.width)
.height(sizing.height)
.into()
}
pub fn view_prompt<'a>(
file_name: &'a str,
error: &'a str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let actions = row![
Space::new().width(Fill),
button(text(crate::tr!("recovery-decline")).size(12))
.on_press(Message::RecoveryDecline)
.style(button::secondary)
.padding([6, 14]),
button(text(crate::tr!("recovery-attempt")).size(12))
.on_press(Message::RecoveryAttempt)
.style(button::primary)
.padding([6, 14]),
]
.spacing(8);
container(
column![
text(crate::tr!("recovery-prompt-heading")).size(20),
text(file_name).size(13),
text(crate::tr!("recovery-prompt-description"))
.size(11)
.style(muted_style),
container(scrollable(text(error).size(10)).height(Fill))
.padding([10, 12])
.width(Fill)
.height(Fill)
.style(container::bordered_box),
actions,
]
.spacing(10)
.width(sizing.width)
.height(sizing.height),
)
.padding([12, 16])
.into()
}