fix(updater): Sichere In-App Updates mit ureq, SHA256-Integrität und atomarem AppImage-Tausch

This commit is contained in:
2026-10-06 12:23:14 +02:00
parent dfb5a64917
commit 91e2d78b4f
17 changed files with 1664 additions and 478 deletions
+174 -94
View File
@@ -81,11 +81,8 @@ pub const PEN_WIDTH_PRESETS: [(f32, &str); 4] = [
];
/// Dicken-Presets für den Textmarker:
pub const HIGHLIGHTER_WIDTH_PRESETS: [(f32, &str); 3] = [
(4.0, "4.0 mm"),
(6.0, "6.0 mm"),
(8.0, "8.0 mm"),
];
pub const HIGHLIGHTER_WIDTH_PRESETS: [(f32, &str); 3] =
[(4.0, "4.0 mm"), (6.0, "6.0 mm"), (8.0, "8.0 mm")];
/// Aktionen, die von der UI an die Anwendungslogik signalisiert werden.
#[derive(Debug, Clone, PartialEq)]
@@ -111,7 +108,10 @@ pub enum UiAction {
// Dokumenten- & Notizbuch-Verwaltung
OpenNotebook(String),
CreateNotebook(String),
RenameNotebook { id: String, new_title: String },
RenameNotebook {
id: String,
new_title: String,
},
DeleteNotebook(String),
RefreshNotebooks,
// Gitea Release & Updater
@@ -119,7 +119,10 @@ pub enum UiAction {
DownloadUpdateAsset(crate::updater::GiteaAsset),
InstallOrOpenUpdate(std::path::PathBuf),
OpenUrlInBrowser(String),
SaveUpdaterConfig { server_url: String, auth_token: Option<String> },
SaveUpdaterConfig {
server_url: String,
auth_token: Option<String>,
},
}
/// Zustand der grafischen Benutzeroberfläche.
@@ -312,25 +315,36 @@ fn tool_pill_button_with_indicator(
(
egui::Color32::from_rgba_premultiplied(45, 80, 145, 230),
egui::Color32::WHITE,
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(110, 165, 255, 190)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(110, 165, 255, 190),
),
)
} else if response.hovered() {
(
egui::Color32::from_rgba_premultiplied(255, 255, 255, 16),
egui::Color32::WHITE,
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 25)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 25),
),
)
} else {
(
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
egui::Color32::from_gray(215),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 15)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 15),
),
)
};
// Button-Hintergrund
ui.painter().rect_filled(rect, egui::Rounding::same(8.0), bg);
ui.painter().rect_stroke(rect, egui::Rounding::same(8.0), stroke);
ui.painter()
.rect_filled(rect, egui::Rounding::same(8.0), bg);
ui.painter()
.rect_stroke(rect, egui::Rounding::same(8.0), stroke);
// Text & Caret
let label = if is_menu_open {
@@ -359,7 +373,8 @@ fn tool_pill_button_with_indicator(
(indicator_color[2] * 255.0) as u8,
((indicator_color[3] * 255.0) as u8).max(180),
);
ui.painter().rect_filled(bar_rect, egui::Rounding::same(1.5), bar_color);
ui.painter()
.rect_filled(bar_rect, egui::Rounding::same(1.5), bar_color);
}
response.on_hover_text(tooltip)
@@ -376,13 +391,19 @@ fn tool_pill_button(
(
egui::Color32::from_rgba_premultiplied(45, 80, 145, 230),
egui::Color32::WHITE,
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(110, 165, 255, 190)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(110, 165, 255, 190),
),
)
} else {
(
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
egui::Color32::from_gray(215),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 15)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 15),
),
)
};
@@ -415,22 +436,33 @@ fn stroke_preview_button(
let (bg, border) = if is_active {
(
egui::Color32::from_rgba_premultiplied(50, 85, 150, 220),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(120, 175, 255, 200)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(120, 175, 255, 200),
),
)
} else if response.hovered() {
(
egui::Color32::from_rgba_premultiplied(255, 255, 255, 16),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 25)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 25),
),
)
} else {
(
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 14)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 14),
),
)
};
ui.painter().rect_filled(rect, egui::Rounding::same(6.0), bg);
ui.painter().rect_stroke(rect, egui::Rounding::same(6.0), border);
ui.painter()
.rect_filled(rect, egui::Rounding::same(6.0), bg);
ui.painter()
.rect_stroke(rect, egui::Rounding::same(6.0), border);
// Horizontale Vorschau-Linie zeichnen
let line_thickness = (stroke_width_mm * 2.5).clamp(1.0, 9.0);
@@ -456,7 +488,11 @@ fn stroke_preview_button(
egui::Align2::CENTER_CENTER,
label_text,
egui::FontId::proportional(10.0),
if is_active { egui::Color32::WHITE } else { egui::Color32::from_gray(190) },
if is_active {
egui::Color32::WHITE
} else {
egui::Color32::from_gray(190)
},
);
}
@@ -464,34 +500,32 @@ fn stroke_preview_button(
}
/// Kompakter Strichbreiten-Preset-Button für die Haupt-Toolbar.
fn preset_width_button(
ui: &mut egui::Ui,
is_active: bool,
text: &str,
) -> egui::Response {
fn preset_width_button(ui: &mut egui::Ui, is_active: bool, text: &str) -> egui::Response {
let (bg, text_color, stroke) = if is_active {
(
egui::Color32::from_rgba_premultiplied(50, 85, 150, 230),
egui::Color32::WHITE,
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(120, 175, 255, 190)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(120, 175, 255, 190),
),
)
} else {
(
egui::Color32::from_rgba_premultiplied(255, 255, 255, 6),
egui::Color32::from_gray(190),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 12)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 12),
),
)
};
let button = egui::Button::new(
egui::RichText::new(text)
.color(text_color)
.size(11.5),
)
.fill(bg)
.stroke(stroke)
.rounding(egui::Rounding::same(6.0))
.min_size(egui::vec2(0.0, 24.0));
let button = egui::Button::new(egui::RichText::new(text).color(text_color).size(11.5))
.fill(bg)
.stroke(stroke)
.rounding(egui::Rounding::same(6.0))
.min_size(egui::vec2(0.0, 24.0));
ui.add(button)
}
@@ -512,24 +546,26 @@ fn action_pill_button(
let (bg, stroke) = if enabled {
(
egui::Color32::from_rgba_premultiplied(255, 255, 255, 10),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 22)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 22),
),
)
} else {
(
egui::Color32::from_rgba_premultiplied(255, 255, 255, 3),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 8)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
),
)
};
let button = egui::Button::new(
egui::RichText::new(text)
.color(text_color)
.size(12.5),
)
.fill(bg)
.stroke(stroke)
.rounding(egui::Rounding::same(7.0))
.min_size(egui::vec2(0.0, 26.0));
let button = egui::Button::new(egui::RichText::new(text).color(text_color).size(12.5))
.fill(bg)
.stroke(stroke)
.rounding(egui::Rounding::same(7.0))
.min_size(egui::vec2(0.0, 26.0));
ui.add_enabled(enabled, button).on_hover_text(tooltip)
}
@@ -541,7 +577,10 @@ fn toolbar_divider(ui: &mut egui::Ui) {
ui.painter().vline(
rect.center().x,
rect.y_range(),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 25)),
egui::Stroke::new(
1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 25),
),
);
ui.add_space(2.0);
}
@@ -1014,14 +1053,22 @@ pub fn render_ui(
.color(egui::Color32::WHITE)
.size(12.5),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.add(egui::Button::new(egui::RichText::new("✕").size(11.0)).frame(false))
.clicked()
{
state.is_pen_menu_open = false;
}
});
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
|ui| {
if ui
.add(
egui::Button::new(
egui::RichText::new("✕").size(11.0),
)
.frame(false),
)
.clicked()
{
state.is_pen_menu_open = false;
}
},
);
});
ui.add_space(8.0);
@@ -1040,7 +1087,8 @@ pub fn render_ui(
let is_sel = (state.pen_color[0] - color[0]).abs() < 0.05
&& (state.pen_color[1] - color[1]).abs() < 0.05
&& (state.pen_color[2] - color[2]).abs() < 0.05;
if color_circle_swatch(ui, color, is_sel, label, 22.0).clicked() {
if color_circle_swatch(ui, color, is_sel, label, 22.0).clicked()
{
state.pen_color = color;
actions.push(UiAction::SetColor(color));
}
@@ -1079,7 +1127,14 @@ pub fn render_ui(
ui.spacing_mut().item_spacing = egui::vec2(6.0, 0.0);
for (preset, label) in PEN_WIDTH_PRESETS {
let is_sel = (state.pen_stroke_width_mm - preset).abs() < 0.05;
if stroke_preview_button(ui, is_sel, preset, label, state.pen_color).clicked()
if stroke_preview_button(
ui,
is_sel,
preset,
label,
state.pen_color,
)
.clicked()
{
state.pen_stroke_width_mm = preset;
actions.push(UiAction::SetStrokeWidthMm(preset));
@@ -1145,14 +1200,22 @@ pub fn render_ui(
.color(egui::Color32::WHITE)
.size(12.5),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.add(egui::Button::new(egui::RichText::new("✕").size(11.0)).frame(false))
.clicked()
{
state.is_highlighter_menu_open = false;
}
});
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
|ui| {
if ui
.add(
egui::Button::new(
egui::RichText::new("✕").size(11.0),
)
.frame(false),
)
.clicked()
{
state.is_highlighter_menu_open = false;
}
},
);
});
ui.add_space(8.0);
@@ -1168,10 +1231,12 @@ pub fn render_ui(
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0);
for (color, label) in HIGHLIGHTER_COLOR_PRESETS {
let is_sel = (state.highlighter_color[0] - color[0]).abs() < 0.05
let is_sel = (state.highlighter_color[0] - color[0]).abs()
< 0.05
&& (state.highlighter_color[1] - color[1]).abs() < 0.05
&& (state.highlighter_color[2] - color[2]).abs() < 0.05;
if color_circle_swatch(ui, color, is_sel, label, 22.0).clicked() {
if color_circle_swatch(ui, color, is_sel, label, 22.0).clicked()
{
state.highlighter_color = color;
actions.push(UiAction::SetColor(color));
}
@@ -1208,7 +1273,8 @@ pub fn render_ui(
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing = egui::vec2(6.0, 0.0);
for (preset, label) in HIGHLIGHTER_WIDTH_PRESETS {
let is_sel = (state.highlighter_stroke_width_mm - preset).abs() < 0.1;
let is_sel =
(state.highlighter_stroke_width_mm - preset).abs() < 0.1;
if stroke_preview_button(
ui,
is_sel,
@@ -1236,13 +1302,18 @@ pub fn render_ui(
ui.add_space(2.0);
if ui
.add(
egui::Slider::new(&mut state.highlighter_stroke_width_mm, 2.0..=16.0)
.suffix(" mm")
.step_by(0.5),
egui::Slider::new(
&mut state.highlighter_stroke_width_mm,
2.0..=16.0,
)
.suffix(" mm")
.step_by(0.5),
)
.changed()
{
actions.push(UiAction::SetStrokeWidthMm(state.highlighter_stroke_width_mm));
actions.push(UiAction::SetStrokeWidthMm(
state.highlighter_stroke_width_mm,
));
}
});
@@ -1299,16 +1370,18 @@ pub fn render_ui(
);
ui.separator();
egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| {
for idx in 0..notebook.pages.len() {
let is_current = idx == notebook.current_page_index;
let text = format!("Seite {}", idx + 1);
if tool_pill_button(ui, is_current, &text, "").clicked() {
actions.push(UiAction::GoToPage(idx));
state.show_page_jump_popup = false;
egui::ScrollArea::vertical()
.max_height(220.0)
.show(ui, |ui| {
for idx in 0..notebook.pages.len() {
let is_current = idx == notebook.current_page_index;
let text = format!("Seite {}", idx + 1);
if tool_pill_button(ui, is_current, &text, "").clicked() {
actions.push(UiAction::GoToPage(idx));
state.show_page_jump_popup = false;
}
}
}
});
});
ui.separator();
if action_pill_button(ui, true, "Schließen", "Fenster schließen").clicked() {
@@ -1471,13 +1544,13 @@ pub fn render_ui(
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if state.available_notebooks.len() > 1 {
if ui.button(egui::RichText::new("🗑").size(11.0).color(egui::Color32::from_rgb(255, 120, 120)))
if state.available_notebooks.len() > 1
&& ui
.button(egui::RichText::new("🗑").size(11.0).color(egui::Color32::from_rgb(255, 120, 120)))
.on_hover_text("Notizbuch unwiderruflich löschen")
.clicked()
{
actions.push(UiAction::DeleteNotebook(summary.id.clone()));
}
{
actions.push(UiAction::DeleteNotebook(summary.id.clone()));
}
if ui.button(egui::RichText::new("✏").size(11.0))
@@ -1701,10 +1774,10 @@ pub fn render_ui(
}
ui.add_space(8.0);
if !release.html_url.is_empty() {
if ui.button(egui::RichText::new("🌐 Release auf Gitea öffnen").size(12.0)).clicked() {
actions.push(UiAction::OpenUrlInBrowser(release.html_url.clone()));
}
if !release.html_url.is_empty()
&& ui.button(egui::RichText::new("🌐 Release auf Gitea öffnen").size(12.0)).clicked()
{
actions.push(UiAction::OpenUrlInBrowser(release.html_url.clone()));
}
});
}
@@ -1810,7 +1883,12 @@ pub fn render_ui(
.order(egui::Order::Tooltip)
.show(ctx, |ui| {
egui::Frame::none()
.fill(egui::Color32::from_rgba_unmultiplied(20, 25, 30, alpha.min(235)))
.fill(egui::Color32::from_rgba_unmultiplied(
20,
25,
30,
alpha.min(235),
))
.rounding(egui::Rounding::same(10.0))
.stroke(egui::Stroke::new(
1.0_f32,
@@ -1826,7 +1904,9 @@ pub fn render_ui(
.show(ui, |ui| {
ui.label(
egui::RichText::new(format!("✓ {}", msg))
.color(egui::Color32::from_rgba_unmultiplied(220, 255, 230, alpha))
.color(egui::Color32::from_rgba_unmultiplied(
220, 255, 230, alpha,
))
.size(13.0)
.strong(),
);