Skip to main content

pacenotes_installer\wizard\scene/
restore.rs

1use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
2use windows_sys::Win32::UI::Controls as ctl;
3use windows_sys::Win32::UI::WindowsAndMessaging as wm;
4
5/// The restore scene: pick a state to go back to, read what that would do, then
6/// do it. Mostly preview, since this is the one route that can remove files a
7/// user has, and what exactly is about to happen is the question to answer first.
8pub unsafe extern "system" fn restore_page(
9    dialog: HWND,
10    message: u32,
11    wparam: WPARAM,
12    lparam: LPARAM,
13) -> isize {
14    match message {
15        wm::WM_INITDIALOG => {
16            crate::wizard::page::adopt_state(dialog, lparam);
17            crate::ui::set_text(
18                dialog,
19                crate::wizard::control_ids::IDC_RESTORE_PROMPT,
20                crate::i18n::IDS_RESTORE_PROMPT,
21            );
22            1
23        }
24        wm::WM_COMMAND => {
25            if crate::ui::hiword(wparam) == crate::ui::LBN_SELCHANGE
26                && crate::ui::loword(wparam) == crate::wizard::control_ids::IDC_RESTORE_POINTS
27            {
28                on_point_picked(dialog);
29            }
30            1
31        }
32        wm::WM_NOTIFY => match crate::wizard::page::notify_code(lparam) {
33            Some(ctl::PSN_SETACTIVE) => {
34                crate::wizard::page::set_sheet_caption(dialog);
35                crate::wizard::page::set_header(dialog, crate::i18n::IDS_RESTORE_TITLE);
36                crate::wizard::page::set_buttons(dialog, ctl::PSWIZB_BACK | ctl::PSWIZB_NEXT);
37                crate::wizard::page::set_next_text(dialog, crate::i18n::IDS_BTN_RESTORE);
38                fill_points(dialog);
39                crate::wizard::page::reply(dialog, 0)
40            }
41            // Back to the folder page this route came from.
42            Some(ctl::PSN_WIZBACK) => crate::wizard::page::reply(
43                dialog,
44                crate::wizard::control_ids::IDD_S3_FOLDER as isize,
45            ),
46            Some(ctl::PSN_WIZNEXT) => on_restore(dialog),
47            Some(ctl::PSN_QUERYCANCEL) => crate::wizard::page::query_cancel(dialog),
48            _ => 0,
49        },
50        _ => 0,
51    }
52}
53
54/// Lists the points newest first, with "remove everything" last; rebuilt on
55/// every activation, since a run since the last visit added one.
56unsafe fn fill_points(dialog: HWND) {
57    let Some(state) = crate::wizard::page::state(dialog) else {
58        return;
59    };
60    let points = crate::wizard::control_ids::IDC_RESTORE_POINTS;
61    let preview = crate::wizard::control_ids::IDC_RESTORE_PREVIEW;
62    let game_dir = std::path::PathBuf::from(&state.folder);
63    state.points = pacenotes_installer::restore::points(&game_dir);
64    state.restore_to = None;
65    state.restore_plan = pacenotes_installer::restore::Plan::default();
66
67    crate::ui::list_reset(dialog, points);
68    crate::ui::set_text_raw(dialog, preview, "");
69    crate::ui::enable(dialog, points, !state.points.is_empty());
70    if state.points.is_empty() {
71        crate::ui::set_text(dialog, preview, crate::i18n::IDS_RESTORE_EMPTY);
72        return;
73    }
74    // The newest point is compared with the folder as it is now.
75    let live = pacenotes_installer::ledger::Ledger::open(&game_dir)
76        .packages()
77        .clone();
78    for (index, point) in state.points.iter().enumerate() {
79        let after = match index {
80            0 => &live,
81            _ => &state.points[index - 1].snapshot,
82        };
83        crate::ui::list_add(dialog, points, &point_label(point, after));
84    }
85    let everything = crate::i18n::load_string(crate::i18n::IDS_RESTORE_ALL);
86    crate::ui::list_add(dialog, points, &everything);
87}
88
89/// The date, plus what the run after the point did, which is what tells two dates apart.
90fn point_label(
91    point: &pacenotes_installer::restore::Point,
92    after: &pacenotes_installer::restore::Snapshot,
93) -> String {
94    let changes = pacenotes_installer::restore::changes(&point.snapshot, after);
95    let when = pacenotes_installer::restore::human_time(&point.name);
96    if changes.is_empty() {
97        return when;
98    }
99    format!(
100        "{when}      +{} -{} ~{}",
101        changes.added, changes.removed, changes.updated
102    )
103}
104
105/// Works out the plan for the chosen row and shows it; the last row is a
106/// restore to an empty state, the same operation rather than a second path.
107unsafe fn on_point_picked(dialog: HWND) {
108    let control = crate::wizard::control_ids::IDC_RESTORE_POINTS;
109    let Some(row) = crate::ui::list_selection(dialog, control) else {
110        return;
111    };
112    let Some(state) = crate::wizard::page::state(dialog) else {
113        return;
114    };
115    let point =
116        state
117            .points
118            .get(row)
119            .cloned()
120            .unwrap_or_else(|| pacenotes_installer::restore::Point {
121                name: crate::i18n::load_string(crate::i18n::IDS_RESTORE_ALL),
122                snapshot: pacenotes_installer::restore::Snapshot::new(),
123            });
124    let ledger = pacenotes_installer::ledger::Ledger::open(&state.folder);
125    let cache = &state.cache;
126    state.restore_plan =
127        pacenotes_installer::restore::plan(&point.snapshot, ledger.packages(), &|sha256| {
128            cache.has_payload(sha256)
129        });
130    state.restore_to = Some(point);
131    let text = describe_restore(&state.restore_plan);
132    crate::ui::set_text_raw(
133        dialog,
134        crate::wizard::control_ids::IDC_RESTORE_PREVIEW,
135        &text,
136    );
137}
138
139/// The preview, removals first because they are what a user must read before
140/// agreeing; packages are named by their leaf, as every other scene names them.
141fn describe_restore(plan: &pacenotes_installer::restore::Plan) -> String {
142    if plan.is_empty() && plan.unavailable.is_empty() {
143        return crate::i18n::load_string(crate::i18n::IDS_RESTORE_NOTHING);
144    }
145    let put_back = |wanted: &pacenotes_installer::restore::PutBack| wanted.key.clone();
146    let sections = [
147        (crate::i18n::IDS_RESTORE_REMOVE, plan.remove.clone()),
148        (
149            crate::i18n::IDS_RESTORE_PUT_BACK,
150            plan.put_back.iter().map(put_back).collect(),
151        ),
152        (crate::i18n::IDS_RESTORE_KEEP, plan.keep.clone()),
153        (
154            crate::i18n::IDS_RESTORE_MISSING,
155            plan.unavailable.iter().map(put_back).collect(),
156        ),
157    ];
158    sections
159        .iter()
160        .filter(|(_, keys)| !keys.is_empty())
161        .map(|(heading, keys)| crate::i18n::load_string_arg(*heading, &leaves(keys)))
162        .collect::<Vec<_>>()
163        .join("\r\n\r\n")
164}
165
166/// One indented line per package.
167fn leaves(keys: &[String]) -> String {
168    keys.iter()
169        .map(|key| format!("    {}", key.rsplit('/').next().unwrap_or(key)))
170        .collect::<Vec<_>>()
171        .join("\r\n")
172}
173
174/// Hands over to scene 6, which runs the restore as it runs an install; by id,
175/// since this page is last in the sheet.
176unsafe fn on_restore(dialog: HWND) -> isize {
177    let Some(state) = crate::wizard::page::state(dialog) else {
178        return crate::wizard::page::reply(dialog, 0);
179    };
180    if state.restore_to.is_none() {
181        let pick = crate::i18n::load_string(crate::i18n::IDS_RESTORE_PICK);
182        crate::ui::message_box_warning(dialog, &pick);
183        return crate::wizard::page::stay(dialog);
184    }
185    // A progress page that does nothing would be theatre.
186    if state.restore_plan.is_empty() {
187        let nothing = crate::i18n::load_string(crate::i18n::IDS_RESTORE_NOTHING);
188        crate::ui::message_box_info(dialog, &nothing);
189        return crate::wizard::page::stay(dialog);
190    }
191    crate::wizard::page::reply(dialog, crate::wizard::control_ids::IDD_S6_INSTALL as isize)
192}