diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3dd19..3f29625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## [4.0.2] — 2026-06-21 + +### Changed +- Sürgü maksimum değeri artık diskteki kullanılabilir alan kadar (diğer bölümler hariç) +- Düzenleme modunda seçili bölümün kapasitesi sürgü aralığına dahil edildi +- `form_input_text` boyut alanı doğrudan `add_size_str`'e bağlandı, yanında formatlı MB gösterimi eklendi +- Boyut etiketi artık `"123 MB"` yerine `"123.456 MB"` formatında (parse edilebilen değerlerde) + +### Fixed +- Boyut input alanına yazılan değer `add_size_str`'e geri yazılmıyordu — text edit salt okunur kalıyordu + +--- + +## [4.0.1] — 2026-06-19 + +### Added +- `c_root_bg()` tema fonksiyonu +- `show_root_password`, `show_root_password_confirm` alanları (`UsersStep`) +- `form_input_text` / `form_input_password`'a `Frame` sarmalı ve `.frame(false)` + +### Changed +- `form_input_text`: Frame içine alındı, stroke/fill/rounding eklendi, yükseklik 32→10 +- `form_input_password`: SVG göz ikonları (emoji yerine), Frame sarmalı, `override_text_color` +- Disk grid seçim renkleri `theme::c_*()` → sabit `(237,237,224,1)` +- Disk listesi header ve sütun başlıkları: `Align::Center` → `Align::LEFT`, `vertical_centered` kaldırıldı +- Root şifre alanları raw `TextEdit` → `form_input_password` (göz ikonu + frame ile) +- Kullanıcı formu Frame rengi `theme::c_bg_widget()` → `(240,243,198)` +- Manuel disk listesinde marka/model etiketi sıralaması değiştirildi + +### Fixed +- `users.rs:154` — `Color32` import hatası (missing `egui::` prefix) + +--- + ## [4.0.0] — 2026-06-18 ### Added diff --git a/assets/icons/fail-icon.svg b/assets/icons/fail-icon.svg new file mode 100644 index 0000000..3e337e6 --- /dev/null +++ b/assets/icons/fail-icon.svg @@ -0,0 +1 @@ +cross \ No newline at end of file diff --git a/assets/icons/warning-icon.svg b/assets/icons/warning-icon.svg new file mode 100644 index 0000000..3061c76 --- /dev/null +++ b/assets/icons/warning-icon.svg @@ -0,0 +1 @@ +warning \ No newline at end of file diff --git a/locales/en.toml b/locales/en.toml index 9fa6037..a10fc3c 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -268,6 +268,9 @@ mp_select_hint = "Select…" mp_add_btn = "✓ Add" mp_size_hint = "Size (MB, 0=remaining):" mp_delete_confirm_yes = "✓ Yes, Delete" +mp_new_partition = "New Partition" +mp_edit_partition = "Edit" +mp_save = "✓ Save" plan_root_suffix = " (root)" disk_min_required = " ↳ %{err} (min. %{n} GB required)" diff --git a/locales/tr.toml b/locales/tr.toml index c488ced..140a547 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -240,6 +240,9 @@ mp_select_hint = "Seç…" mp_add_btn = "✓ Ekle" mp_size_hint = "Boyut (MB, 0=kalan):" mp_delete_confirm_yes = "✓ Evet, Sil" +mp_new_partition = "Yeni Bölüm" +mp_edit_partition = "Düzenle" +mp_save = "✓ Kaydet" plan_root_suffix = " (kök)" disk_min_required = " ↳ %{err} (min. %{n} GB gerekli)" diff --git a/src/funct.rs b/src/funct.rs index 44d960e..7e60b90 100644 --- a/src/funct.rs +++ b/src/funct.rs @@ -18,18 +18,27 @@ pub fn form_input_text( )-> eframe::egui::Response { let width_percent = 0.90; // Mevcut genişliğin %90'ı - let height = 32.0; // Standart yükseklik + let height = 10.0; // Standart yükseklik let hesaplanan_genislik = ui.available_width() * width_percent; let boyut = eframe::egui::vec2(hesaplanan_genislik, height); ui.spacing_mut().button_padding = eframe::egui::vec2(10.0, 10.0); + egui::Frame::none() + .fill(egui::Color32::from_rgb(255, 255, 255)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200))) + .rounding(4.0) + .inner_margin(eframe::egui::Margin::symmetric(2.0, 2.0)) + .show(ui, |ui| { ui.add_sized( boyut, eframe::egui::TextEdit::singleline(text) .vertical_align(eframe::egui::Align::Center) + .frame(false), ) + }).inner } + pub fn form_input_password( ui: &mut eframe::egui::Ui, text: &mut String, @@ -38,28 +47,59 @@ pub fn form_input_password( let width_percent = 0.90; let height = 32.0; let toplam_genislik = ui.available_width() * width_percent; - let dugme_genislik = 40.0; - let alan_genislik = toplam_genislik - dugme_genislik - 4.0; - let boyut = eframe::egui::vec2(alan_genislik, height); + + let buton_genislik = 32.0; + let alan_genislik = toplam_genislik - buton_genislik - 4.0; - ui.spacing_mut().button_padding = eframe::egui::vec2(10.0, 10.0); + let mut response: Option = None; - ui.horizontal(|ui| { - let r = ui.add_sized( - boyut, - eframe::egui::TextEdit::singleline(text) - .password(!*show_password) - .vertical_align(eframe::egui::Align::Center), - ); - let göz = if *show_password { "🙈" } else { "👁" }; - if ui.add( - eframe::egui::Button::new(göz) - .min_size(eframe::egui::vec2(dugme_genislik, height)) - ).clicked() { - *show_password = !*show_password; - } - r - }).inner + ui.visuals_mut().override_text_color = Some(eframe::egui::Color32::from_rgb(30, 30, 30)); + + // SVG içeriklerini string olarak tanımlıyoruz + let goz_acik_svg = r#""#; + let goz_kapali_svg = r#""#; + + eframe::egui::Frame::none() + .fill(eframe::egui::Color32::WHITE) + .stroke(eframe::egui::Stroke::new(1.0, eframe::egui::Color32::from_rgb(200, 200, 200))) + .rounding(6.0) + .inner_margin(eframe::egui::Margin::symmetric(2.0, 2.0)) + .show(ui, |ui| { + ui.horizontal(|ui| { + // 1. Şifre Metin Alanı + let r = ui.add_sized( + eframe::egui::vec2(alan_genislik, height), + eframe::egui::TextEdit::singleline(text) + .password(!*show_password) + .frame(false) + ); + response = Some(r); + + // Duruma göre ilgili SVG string'ini seçip byte dizisine (&[u8]) çeviriyoruz + let (uri, svg_bytes) = if *show_password { + ("bytes://goz_acik.svg", goz_acik_svg.as_bytes()) + } else { + ("bytes://goz_kapali.svg", goz_kapali_svg.as_bytes()) + }; + + // Image::from_bytes kullanarak imajı güvenli bir şekilde yüklüyoruz + let svg_gosterici = eframe::egui::Image::from_bytes(uri, svg_bytes) + .tint(eframe::egui::Color32::from_rgb(120, 120, 120)) + .max_width(18.0) + .max_height(18.0); + + // 2. SVG İkonlu Buton + let btn = eframe::egui::Button::image(svg_gosterici) + .frame(false) + .min_size(eframe::egui::vec2(buton_genislik, height)); + + if ui.add(btn).on_hover_cursor(eframe::egui::CursorIcon::PointingHand).clicked() { + *show_password = !*show_password; + } + }); + }); + + response.expect("Metin alanı yüklenemedi") } pub fn form_label(ui: &mut eframe::egui::Ui, text: &str) { diff --git a/src/installer.rs b/src/installer.rs index 18ef3d0..60e59ff 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -449,15 +449,31 @@ impl InstallerStep for WelcomeStep { egui::Grid::new("checks_grid") .num_columns(3) .spacing([10.0, 6.0]) - .striped(true) .show(ui, |ui| { for check in &self.checks { - let (icon, color) = match check.status { - CheckStatus::Pass => ("✅", theme::c_success()), - CheckStatus::Warn => ("⚠️", theme::c_warning()), - CheckStatus::Fail => ("❌", theme::c_error()), - }; - ui.colored_label(color, icon); + match check.status { + CheckStatus::Pass => { + ui.add( + egui::Image::new(egui::include_image!("../assets/icons/check.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_success()) + ); + } + CheckStatus::Warn => { + ui.add( + egui::Image::new(egui::include_image!("../assets/icons/warning-icon.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_warning()) + ); + } + CheckStatus::Fail => { + ui.add( + egui::Image::new(egui::include_image!("../assets/icons/fail-icon.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_error()) + ); + } + } ui.label(egui::RichText::new(&check.name).strong().color(theme::c_text())); ui.label( egui::RichText::new(&check.message) diff --git a/src/steps/partition.rs b/src/steps/partition.rs index be09a15..4fd7b8d 100644 --- a/src/steps/partition.rs +++ b/src/steps/partition.rs @@ -3,6 +3,7 @@ use eframe::egui; use rust_i18n::t; use crate::ui::theme; +use crate::funct::{form_label, form_input_text}; // sysinfo::Disks mount point başına bir kayıt döndürür (partition'lar), // ham disk listesi için lsblk kullanıyoruz. @@ -220,6 +221,12 @@ const COMMON_MOUNTPOINTS: &[&str] = &["/", "/boot", "/boot/efi", "/home", "/var" struct ManualState { /// "Bölüm Ekle" diyaloğu açık mı? adding: bool, + /// "Bölüm Düzenle" diyaloğı açık mı? + editing: bool, + /// Düzenlenecek bölüm indeksi + edit_idx: Option, + /// Seçili satır indeksi + selected_idx: Option, /// Diyalog: seçili dosya sistemi add_fstype: usize, // FsType listesindeki indeks /// Diyalog: boyut alanı (MB, metin girişi) @@ -363,8 +370,17 @@ impl InstallerStep for PartitionStep { // ── Mod seçimi ───────────────────────────────────────── ui.horizontal(|ui| { - ui.selectable_value(&mut state.erase_disk, true, t!("erase_disk_label")); - ui.selectable_value(&mut state.erase_disk, false, t!("manual_partition_label")); + if state.erase_disk { + let _ = theme::primary_button(ui, &t!("erase_disk_label")); + if theme::secondary_button(ui, &t!("manual_partition_label")).clicked() { + state.erase_disk = false; + } + } else { + if theme::secondary_button(ui, &t!("erase_disk_label")).clicked() { + state.erase_disk = true; + } + let _ = theme::primary_button(ui, &t!("manual_partition_label")); + } }); ui.add_space(10.0); @@ -415,13 +431,11 @@ impl PartitionStep { // arka plan egui::Frame::none() .fill(if selected { - crate::ui::theme::c_accent_dim() + egui::Color32::from_rgba_unmultiplied(237, 237, 224, 1) } else if response.hovered() { egui::Color32::from_rgba_unmultiplied(60, 60, 80, 180) - } else if too_small { - egui::Color32::from_rgba_unmultiplied(60, 30, 30, 180) } else { - crate::ui::theme::c_bg_widget() + egui::Color32::from_rgba_unmultiplied(237, 237, 224, 1) }) .stroke(egui::Stroke::new( if selected { 2.0 } else { 1.0 }, @@ -509,15 +523,15 @@ impl PartitionStep { egui::vec2(tam_genislik * col_widths[j], 30.0), egui::Sense::hover(), ); - let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center)); + let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::LEFT)); egui::Frame::none() .fill(header_color) - .stroke(egui::Stroke::new(1.0, header_color)) .inner_margin(egui::Margin::symmetric(8.0, 4.0)) .show(child_ui, |ui| { - ui.vertical_centered(|ui| { - ui.label(egui::RichText::new(h).strong().color(header_text).size(13.0)); - }); + ui.label(egui::RichText::new(h) + .strong() + .color(header_text) + .size(13.0)); }); } ui.end_row(); @@ -628,7 +642,6 @@ impl PartitionStep { impl PartitionStep { fn show_manual_mode(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) { - let accent = crate::ui::theme::c_accent(); let kart_genislik = 180.0; let kart_yukseklik = 160.0; @@ -653,11 +666,11 @@ impl PartitionStep { egui::Frame::none() .fill(if selected { - crate::ui::theme::c_accent_dim() + egui::Color32::from_rgba_unmultiplied(237, 237, 224, 1) } else if response.hovered() { egui::Color32::from_rgba_unmultiplied(60, 60, 80, 180) } else { - crate::ui::theme::c_bg_widget() + egui::Color32::from_rgba_unmultiplied(237, 237, 224, 1) }) .stroke(egui::Stroke::new( if selected { 2.0 } else { 1.0 }, @@ -680,17 +693,20 @@ impl PartitionStep { } else { &disk.vendor }; + + ui.label( + egui::RichText::new(marka) + .size(11.0) + .color(crate::ui::theme::c_text_dim()), + ); + ui.label( egui::RichText::new(format!("{} GB", disk.size_gb)) .size(14.0) .strong() .color(crate::ui::theme::c_text()), ); - ui.label( - egui::RichText::new(marka) - .size(11.0) - .color(crate::ui::theme::c_text_dim()), - ); + }); if response.clicked() { @@ -715,12 +731,17 @@ impl PartitionStep { return; } }; + let disk_size_gb = state.available_disks.iter() + .find(|d| d.name == disk) + .map(|d| d.size_gb) + .unwrap_or(256); + let max_mb = (disk_size_gb as f64) * 1024.0; // ── Mevcut bölüm listesi tablosu ─────────────────────── ui.label(egui::RichText::new(t!("mp_plan_title")).strong().color(crate::ui::theme::c_text())); ui.add_space(6.0); - let mut delete_idx: Option = None; + let delete_idx: Option = None; let col_widths = [0.28, 0.12, 0.18, 0.27, 0.15]; let tam_genislik = ui.available_width(); @@ -735,12 +756,11 @@ impl PartitionStep { grid.show(ui, |ui| { // ── Başlık satırı ───────────────────────────────── - let headers: [String; 5] = [ + let headers: [String; 4] = [ t!("mp_device").to_string(), t!("mp_size").to_string(), t!("mp_fstype").to_string(), - t!("mp_mountpoint").to_string(), - t!("mp_action").to_string(), + t!("mp_mountpoint").to_string(), ]; for (j, h) in headers.into_iter().enumerate() { let (rect, _) = ui.allocate_exact_size( @@ -753,16 +773,17 @@ impl PartitionStep { .stroke(egui::Stroke::new(1.0, header_color)) .inner_margin(egui::Margin::symmetric(8.0, 4.0)) .show(child_ui, |ui| { - ui.vertical_centered(|ui| { - ui.label(egui::RichText::new(h).strong().color(header_text).size(13.0)); - }); + ui.label(egui::RichText::new(h).strong().color(header_text).size(13.0)); }); } ui.end_row(); // ── Veri satırları ──────────────────────────────── for (row_idx, part) in state.custom_partitions.iter().enumerate() { - let row_bg = if row_idx % 2 == 0 { + let is_selected = self.manual.selected_idx == Some(row_idx); + let row_bg = if is_selected { + egui::Color32::from_rgba_unmultiplied(80, 100, 180, 80) + } else if row_idx % 2 == 0 { egui::Color32::from_rgba_unmultiplied(60, 60, 80, 60) } else { egui::Color32::TRANSPARENT @@ -790,22 +811,32 @@ impl PartitionStep { ]; for (j, (deger, is_action)) in hucreler.iter().enumerate() { - let (rect, _response) = ui.allocate_exact_size( + let sense = if *is_action { + egui::Sense::click() + } else { + egui::Sense::click() + }; + let (rect, response) = ui.allocate_exact_size( egui::vec2(tam_genislik * col_widths[j], 34.0), - egui::Sense::click(), + sense, ); + + if !*is_action { + if response.clicked() { + self.manual.selected_idx = Some(row_idx); + } + response.on_hover_cursor(egui::CursorIcon::PointingHand); + } + let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center)); if *is_action { egui::Frame::none() - .fill(row_bg) - .stroke(egui::Stroke::new(0.0, egui::Color32::TRANSPARENT)) + .fill(egui::Color32::TRANSPARENT) .inner_margin(egui::Margin::symmetric(8.0, 4.0)) .show(child_ui, |ui| { ui.vertical_centered(|ui| { - if ui.small_button("🗑").on_hover_text(t!("mp_delete")).clicked() { - delete_idx = Some(row_idx); - } + ui.label(egui::RichText::new(deger).size(13.0).color(crate::ui::theme::c_text())); }); }); } else { @@ -827,6 +858,16 @@ impl PartitionStep { } }); + // ── Bölümleme Planı (manuel) ────────────────────────── + if !state.custom_partitions.is_empty() { + ui.add_space(12.0); + ui.separator(); + ui.add_space(8.0); + ui.label(egui::RichText::new(t!("partition_plan_title")).strong().color(crate::ui::theme::c_text())); + ui.add_space(6.0); + manual_partition_bar(ui, &state.custom_partitions); + } + // Silme onayı if let Some(idx) = delete_idx { self.manual.delete_idx = Some(idx); @@ -843,13 +884,14 @@ impl PartitionStep { ui.label(t!("mp_confirm_delete", dev = dev)); ui.add_space(8.0); ui.horizontal(|ui| { - if ui.button(t!("mp_delete_confirm_yes")).clicked() { + if theme::danger_button(ui, &t!("mp_delete_confirm_yes")).clicked() { if idx < state.custom_partitions.len() { state.custom_partitions.remove(idx); } self.manual.delete_idx = None; + self.manual.selected_idx = None; } - if ui.button(t!("cancel")).clicked() { + if theme::secondary_button(ui, &t!("cancel")).clicked() { self.manual.delete_idx = None; } }); @@ -858,126 +900,199 @@ impl PartitionStep { ui.add_space(8.0); - // ── "Bölüm Ekle" butonu ──────────────────────────────── - let btn = egui::Button::new(format!("➕ {}", t!("mp_add"))) - .fill(accent.gamma_multiply(0.15)); - if ui.add(btn).clicked() { - self.manual.adding = true; - let next_num = state.custom_partitions.len() + 1; - self.manual.add_size_str.clear(); - self.manual.add_mountpoint.clear(); - self.manual.add_fstype = 0; - // Akıllı varsayılan bağlama noktası - self.manual.add_mountpoint = match next_num { - 1 if self.is_uefi => "/boot/efi".to_string(), - 1 => "/".to_string(), - 2 if self.is_uefi => "swap".to_string(), - 2 => "swap".to_string(), - 3 => "/".to_string(), - _ => String::new(), - }; - } + // ── Butonlar: Yeni Bölüm / Düzenle / Sil ─────────── + ui.horizontal(|ui| { + let has_selection = self.manual.selected_idx.is_some(); + let selected = self.manual.selected_idx + .and_then(|i| state.custom_partitions.get(i)); - // ── "Bölüm Ekle" diyaloğu ────────────────────────────── - if self.manual.adding { - egui::Window::new(t!("mp_add")) + // Yeni Bölüm + if theme::primary_button(ui, &format!("➕ {}", t!("mp_new_partition"))).clicked() { + self.manual.adding = true; + self.manual.editing = false; + self.manual.edit_idx = None; + let next_num = state.custom_partitions.len() + 1; + self.manual.add_size_str.clear(); + self.manual.add_mountpoint.clear(); + self.manual.add_fstype = 0; + self.manual.add_encrypt = false; + self.manual.add_luks_password.clear(); + self.manual.add_mountpoint = match next_num { + 1 if self.is_uefi => "/boot/efi".to_string(), + 1 => "/".to_string(), + 2 if self.is_uefi => "swap".to_string(), + 2 => "swap".to_string(), + 3 => "/".to_string(), + _ => String::new(), + }; + } + + // Düzenle + ui.add_enabled_ui(has_selection, |ui| { + if theme::secondary_button(ui, &format!("✏️ {}", t!("mp_edit_partition"))).clicked() { + if let Some(part) = selected { + self.manual.editing = true; + self.manual.adding = false; + self.manual.edit_idx = self.manual.selected_idx; + self.manual.add_size_str = if part.size_mb == 0 { + String::new() + } else { + part.size_mb.to_string() + }; + self.manual.add_mountpoint = part.mountpoint.clone(); + self.manual.add_fstype = FS_OPTIONS.iter() + .position(|(_, ft)| *ft == part.fstype) + .unwrap_or(0); + self.manual.add_encrypt = part.encrypt; + self.manual.add_luks_password = part.luks_password.clone(); + } + } + }); + + // Sil + ui.add_enabled_ui(has_selection, |ui| { + if theme::danger_button(ui, &format!("🗑 {}", t!("mp_delete"))).clicked() { + self.manual.delete_idx = self.manual.selected_idx; + } + }); + }); + + // ── Bölüm Ekle / Düzenle diyaloğu ───────────────── + let dialog_open = self.manual.adding || self.manual.editing; + if dialog_open { + let is_edit = self.manual.editing; + let title = if is_edit { t!("mp_edit_partition") } else { t!("mp_new_partition") }; + egui::Window::new(title) .collapsible(false) .resizable(false) .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) .show(ui.ctx(), |ui| { - egui::Grid::new("add_part_grid") - .num_columns(2) - .spacing([12.0, 8.0]) - .show(ui, |ui| { - - // Dosya sistemi (boyut önerisi ile) - ui.label(t!("mp_fstype")); - let prev_fs = self.manual.add_fstype; - let fs_label = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?"); - theme::show_combo_box(ui, "fs_combo", fs_label, ui.available_width(), |ui, w| { - for (i, (label, _)) in FS_OPTIONS.iter().enumerate() { - theme::selectable_value(ui, &mut self.manual.add_fstype, i, *label, w); - } - }); - // FS tipi değişince akıllı boyut öner - if prev_fs != self.manual.add_fstype && self.manual.add_size_str.is_empty() { - let fs = FS_OPTIONS.get(self.manual.add_fstype).map(|x| &x.1); - match fs { - Some(FsType::Fat32) => self.manual.add_size_str = "300".to_string(), - Some(FsType::Swap) => self.manual.add_size_str = format!("{}", recommended_swap_mb()), - _ => {} - } - } - ui.end_row(); - - // Boyut - ui.label(t!("mp_size_hint")); - ui.text_edit_singleline(&mut self.manual.add_size_str); - ui.end_row(); - - // Bağlama noktası (2 aşamalı: combo + input) - ui.label(t!("mp_mountpoint")); - ui.horizontal(|ui| { - let current_mp = if self.manual.add_mountpoint.is_empty() { - t!("mp_select_hint").to_string() - } else { - self.manual.add_mountpoint.clone() - }; - theme::show_combo_box(ui, "mp_combo", ¤t_mp, 120.0, |ui, w| { - for mp in COMMON_MOUNTPOINTS { - theme::selectable_value(ui, &mut self.manual.add_mountpoint, mp.to_string(), *mp, w); - } - }); - let mp_field = egui::TextEdit::singleline(&mut self.manual.add_mountpoint) - .hint_text(t!("mp_add_dialog_hint")); - ui.add(mp_field); - }); - ui.end_row(); - - // LUKS şifreleme - ui.label(t!("luks_enable_root")); - let fstype = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.1.clone()).unwrap_or(FsType::Ext4); - let can_encrypt = matches!(fstype, FsType::Ext4 | FsType::Ext3 | FsType::Btrfs | FsType::Xfs); - ui.add_enabled_ui(can_encrypt, |ui| { - ui.checkbox(&mut self.manual.add_encrypt, ""); - }); - if !can_encrypt { self.manual.add_encrypt = false; } - ui.end_row(); - - if self.manual.add_encrypt { - ui.label(t!("luks_password")); - let pw = &mut self.manual.add_luks_password; - ui.horizontal(|ui| { - if self.show_luks_password { - ui.text_edit_singleline(pw); - } else { - ui.add(egui::TextEdit::singleline(pw).password(true)); - } - let eye = if self.show_luks_password { "🙈" } else { "👁" }; - if ui.small_button(eye).clicked() { - self.show_luks_password = !self.show_luks_password; - } - }); - ui.end_row(); + ui.label(t!("mp_fstype")); + let prev_fs = self.manual.add_fstype; + let fs_label = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?"); + theme::show_combo_box(ui, "fs_combo", fs_label, ui.available_width(), |ui, w| { + for (i, (label, _)) in FS_OPTIONS.iter().enumerate() { + theme::selectable_value(ui, &mut self.manual.add_fstype, i, *label, w); } }); + // FS tipi değişince akıllı boyut öner + if !is_edit && prev_fs != self.manual.add_fstype && self.manual.add_size_str.is_empty() { + let fs = FS_OPTIONS.get(self.manual.add_fstype).map(|x| &x.1); + match fs { + Some(FsType::Fat32) => self.manual.add_size_str = "300".to_string(), + Some(FsType::Swap) => self.manual.add_size_str = format!("{}", recommended_swap_mb()), + _ => {} + } + } + + ui.add_space(6.0); + form_label(ui, &t!("mp_size_hint")); + ui.horizontal(|ui| { + form_input_text(ui, &mut self.manual.add_size_str); + let parsed: u64 = self.manual.add_size_str.trim().parse().unwrap_or(0); + if parsed > 0 { + ui.label(format!("{} MB", format_number_thousands(parsed))); + } else { + ui.label("MB"); + } + }); + // Slider — kullanılabilir alan kadar + let used_mb: u64 = state.custom_partitions.iter() + .enumerate() + .filter(|(i, p)| p.size_mb > 0 && Some(*i) != self.manual.edit_idx) + .map(|(_, p)| p.size_mb) + .sum(); + let avail_mb = (max_mb as u64).saturating_sub(used_mb).max(1); + let mut size_val = self.manual.add_size_str.trim().parse::().unwrap_or(0.0); + ui.add_space(2.0); + if ui.add(egui::Slider::new(&mut size_val, 0.0..=avail_mb as f64) + .show_value(false) + .max_decimals(0) + ).changed() { + if size_val == 0.0 { + self.manual.add_size_str = "0".to_string(); + } else { + self.manual.add_size_str = format!("{}", size_val as u64); + } + } + + ui.add_space(6.0); + ui.label(t!("mp_mountpoint")); + ui.horizontal(|ui| { + let current_mp = if self.manual.add_mountpoint.is_empty() { + t!("mp_select_hint").to_string() + } else { + self.manual.add_mountpoint.clone() + }; + theme::show_combo_box(ui, "mp_combo", ¤t_mp, 120.0, |ui, w| { + for mp in COMMON_MOUNTPOINTS { + theme::selectable_value(ui, &mut self.manual.add_mountpoint, mp.to_string(), *mp, w); + } + }); + form_input_text(ui, &mut self.manual.add_mountpoint); + }); + + ui.add_space(6.0); + ui.label(t!("luks_enable_root")); + let fstype = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.1.clone()).unwrap_or(FsType::Ext4); + let can_encrypt = matches!(fstype, FsType::Ext4 | FsType::Ext3 | FsType::Btrfs | FsType::Xfs); + ui.add_enabled_ui(can_encrypt, |ui| { + ui.checkbox(&mut self.manual.add_encrypt, ""); + }); + if !can_encrypt { self.manual.add_encrypt = false; } + + if self.manual.add_encrypt { + ui.add_space(6.0); + ui.label(t!("luks_password")); + let pw = &mut self.manual.add_luks_password; + ui.horizontal(|ui| { + if self.show_luks_password { + ui.text_edit_singleline(pw); + } else { + ui.add(egui::TextEdit::singleline(pw).password(true)); + } + let eye = if self.show_luks_password { "🙈" } else { "👁" }; + if ui.small_button(eye).clicked() { + self.show_luks_password = !self.show_luks_password; + } + }); + } ui.add_space(8.0); ui.horizontal(|ui| { - let ok_btn = egui::Button::new(t!("mp_add_btn")).fill(accent.gamma_multiply(0.2)); - if ui.add(ok_btn).clicked() { + let ok_label = if is_edit { t!("mp_save") } else { t!("mp_add_btn") }; + if theme::primary_button(ui, &ok_label).clicked() { let size_mb: u64 = self.manual.add_size_str .trim().parse().unwrap_or(0); let mp = self.manual.add_mountpoint.trim().to_string(); let fstype = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.1.clone()).unwrap_or(FsType::Ext4); - let part_num = state.custom_partitions.len() + 1; - let device = crate::installer::part_path(&disk, part_num as u32); + let encrypt = self.manual.add_encrypt; + let luks_password = if encrypt { self.manual.add_luks_password.clone() } else { String::new() }; if mp.is_empty() { self.manual.error = Some(t!("mp_error_empty_mountpoint").to_string()); + } else if is_edit { + if let Some(idx) = self.manual.edit_idx { + if idx < state.custom_partitions.len() { + let device = state.custom_partitions[idx].device.clone(); + state.custom_partitions[idx] = CustomPartition { + device, + size_mb, + fstype, + mountpoint: mp, + encrypt, + luks_password, + luks_name: String::new(), + }; + } + } + self.manual.editing = false; + self.manual.edit_idx = None; + self.manual.selected_idx = None; + self.manual.error = None; } else { - let encrypt = self.manual.add_encrypt; - let luks_password = if encrypt { self.manual.add_luks_password.clone() } else { String::new() }; + let part_num = state.custom_partitions.len() + 1; + let device = crate::installer::part_path(&disk, part_num as u32); state.custom_partitions.push(CustomPartition { device, size_mb, @@ -993,8 +1108,10 @@ impl PartitionStep { self.manual.adding = false; } } - if ui.button(t!("cancel")).clicked() { + if theme::secondary_button(ui, &t!("cancel")).clicked() { self.manual.adding = false; + self.manual.editing = false; + self.manual.edit_idx = None; } }); }); @@ -1107,9 +1224,7 @@ impl PartitionStep { ui.add_space(8.0); // "Logical Volume Ekle" butonu - let lv_btn = egui::Button::new(format!("➕ {}", t!("lvm_lv_add"))) - .fill(accent.gamma_multiply(0.15)); - if ui.add(lv_btn).clicked() { + if theme::primary_button(ui, &format!("➕ {}", t!("lvm_lv_add"))).clicked() { self.manual.adding_lv = true; self.manual.add_lv_name.clear(); self.manual.add_lv_vg_idx = 0; @@ -1185,8 +1300,7 @@ impl PartitionStep { ui.add_space(8.0); ui.horizontal(|ui| { - let ok_btn = egui::Button::new(t!("mp_add_btn")).fill(accent.gamma_multiply(0.2)); - if ui.add(ok_btn).clicked() { + if theme::primary_button(ui, &t!("mp_add_btn")).clicked() { let size_mb: u64 = self.manual.add_lv_size_str.trim().parse().unwrap_or(0); let mp = self.manual.add_lv_mountpoint.trim().to_string(); let lv_name = self.manual.add_lv_name.trim().to_string(); @@ -1219,7 +1333,7 @@ impl PartitionStep { self.manual.error = Some(t!("lvm_lv_error_vg").to_string()); } } - if ui.button(t!("cancel")).clicked() { + if theme::secondary_button(ui, &t!("cancel")).clicked() { self.manual.adding_lv = false; } }); @@ -1245,7 +1359,84 @@ impl PartitionStep { } // ───────────────────────────────────────────── -// Yardımcı: renkli bölüm şeridi +// Yardımcı: renkli bölüm şeridi (manuel) +// ───────────────────────────────────────────── + +fn manual_partition_bar(ui: &mut egui::Ui, parts: &[CustomPartition]) { + let bar_height = 28.0; + let (rect, _) = ui.allocate_exact_size( + egui::vec2(ui.available_width(), bar_height), + egui::Sense::hover(), + ); + let painter = ui.painter(); + + if parts.is_empty() { + return; + } + + // Each partition gets at least MIN_VISIBLE of the bar so all are visible + let sized_total: f32 = parts.iter().map(|p| p.size_mb as f32).sum(); + let min_visible = 0.04; + + let mut raw: Vec = parts.iter().map(|p| { + if sized_total > 0.0 { + (p.size_mb as f32 / sized_total).max(min_visible) + } else { + min_visible + } + }).collect(); + + // Normalize to sum 1.0 + let sum: f32 = raw.iter().sum(); + for v in raw.iter_mut() { + *v /= sum; + } + + let mut segments: Vec<(&str, f32, egui::Color32)> = Vec::new(); + + for (i, part) in parts.iter().enumerate() { + let label = part.mountpoint.as_str(); + let color = if part.fstype == FsType::Fat32 { + egui::Color32::from_rgb(80, 160, 220) + } else if part.fstype == FsType::Swap { + egui::Color32::from_rgb(220, 160, 40) + } else if part.mountpoint == "/" { + egui::Color32::from_rgb(80, 190, 100) + } else { + egui::Color32::from_rgb(160, 130, 200) + }; + segments.push((label, raw[i], color)); + } + + let mut x = rect.left(); + let rounding = egui::Rounding::same(4.0); + for (i, (label, frac, color)) in segments.iter().enumerate() { + let w = frac * rect.width(); + let seg_rect = egui::Rect::from_min_size( + egui::pos2(x, rect.top()), + egui::vec2(w - 1.0, bar_height), + ); + let seg_rounding = if i == 0 { + egui::Rounding { nw: rounding.nw, sw: rounding.sw, ne: 0.0, se: 0.0 } + } else if i == segments.len() - 1 { + egui::Rounding { nw: 0.0, sw: 0.0, ne: rounding.ne, se: rounding.se } + } else { + egui::Rounding::ZERO + }; + painter.rect_filled(seg_rect, seg_rounding, *color); + painter.text( + seg_rect.center(), + egui::Align2::CENTER_CENTER, + *label, + egui::FontId::proportional(12.0), + egui::Color32::WHITE, + ); + x += w; + } +} + +// ───────────────────────────────────────────── +// Yardımcı: renkli bölüm şeridi (otomatik) // ───────────────────────────────────────────── fn partition_bar(ui: &mut egui::Ui, plan: &PartitionPlan) { @@ -1304,3 +1495,16 @@ fn partition_bar(ui: &mut egui::Ui, plan: &PartitionPlan) { x += w; } } + +/// Sayıyı nokta ile binlik ayraçlı formata çevirir (örn. 7000000 → 7.000.000). +fn format_number_thousands(n: u64) -> String { + let s = n.to_string(); + let mut result = String::new(); + for (i, c) in s.chars().enumerate() { + if i > 0 && (s.len() - i) % 3 == 0 { + result.push('.'); + } + result.push(c); + } + result +} diff --git a/src/steps/users.rs b/src/steps/users.rs index af3aace..83550b0 100644 --- a/src/steps/users.rs +++ b/src/steps/users.rs @@ -8,6 +8,8 @@ pub struct UsersStep { use_same_for_admin: bool, show_password: bool, show_password_confirm: bool, + show_root_password: bool, // <-- Eklendi + show_root_password_confirm: bool, // <-- Eklendi } impl Default for UsersStep { @@ -16,6 +18,8 @@ impl Default for UsersStep { use_same_for_admin: true, show_password: false, show_password_confirm: false, + show_root_password: false, + show_root_password_confirm: false, } } } @@ -151,7 +155,7 @@ impl InstallerStep for UsersStep { // ─── Kullanıcı Bilgileri Formu ────────────────────────────────── egui::Frame::group(ui.style()) - .fill(theme::c_bg_widget()) + .fill(egui::Color32::from_rgb(240, 243, 198)) .rounding(6.0) .inner_margin(egui::Margin::symmetric(10.0, 10.0)) .show(ui, |ui| { @@ -200,6 +204,7 @@ impl InstallerStep for UsersStep { { let show_pwd = &mut self.show_password; let before = state.password.clone(); + form_input_password(ui, &mut state.password, show_pwd); // Kullanıcı şifreyi değiştirdiyse OEM bayrağını kaldır if state.oem_mode && state.password != before { @@ -254,7 +259,7 @@ impl InstallerStep for UsersStep { } else { ui.add_space(8.0); egui::Frame::group(ui.style()) - .fill(theme::c_bg_widget()) + .fill(theme::c_root_bg()) .rounding(6.0) .inner_margin(egui::Margin::same(12.0)) .show(ui, |ui| { @@ -263,33 +268,21 @@ impl InstallerStep for UsersStep { ui.add_space(8.0); // Root şifresi - ui.label(egui::RichText::new(t!("admin_password_label")).strong().color(theme::c_text())); - ui.horizontal(|ui| { - ui.add( - eframe::egui::TextEdit::singleline(&mut state.root_password) - .password(true) - .desired_width(f32::INFINITY) - .min_size(eframe::egui::vec2(0.0, 32.0)) - ); - - if !state.root_password.is_empty() && state.root_password.len() < 8 { - ui.colored_label(theme::c_error(), t!("admin_password_short")); - } - }); + + // DÜZELTME: 3. parametre olarak show_root_password eklendi + let show_root_pwd = &mut self.show_root_password; + form_input_password(ui, &mut state.root_password, show_root_pwd); + ui.add_space(8.0); // Şifre doğrulama ui.label(egui::RichText::new(t!("admin_password_confirm_label")).strong().color(theme::c_text())); - ui.horizontal(|ui| { - ui.add( - eframe::egui::TextEdit::singleline(&mut state.root_password_confirm) - .password(true) - .desired_width(f32::INFINITY) - .min_size(eframe::egui::vec2(0.0, 32.0)) - ); - }); + // DÜZELTME: 3. parametre olarak show_root_password_confirm eklendi + let show_root_pwd_conf = &mut self.show_root_password_confirm; + form_input_password(ui, &mut state.root_password_confirm, show_root_pwd_conf); + if state.root_password == state.root_password_confirm { ui.colored_label(theme::c_success(), "✅"); } else { diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 592d47b..b52802e 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -47,6 +47,7 @@ pub fn c_error() -> Color32 { color_from_hex(ERROR.load(Ordering::Relaxed)) pub fn c_sidebar() -> Color32 { color_from_hex(SIDEBAR.load(Ordering::Relaxed)) } pub fn c_sidebar_text() -> Color32 { color_from_hex(SIDEBAR_TEXT.load(Ordering::Relaxed)) } pub fn c_footer_bg() -> Color32 { color_from_hex(0xf7f7f7) } +pub fn c_root_bg() -> Color32 { color_from_hex(0xf7f7f7) } pub fn set_theme_mode(is_dark: bool) { if is_dark { @@ -271,7 +272,9 @@ pub fn error_box(ui: &mut egui::Ui, msg: &str) { .inner_margin(egui::Margin::same(10.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.colored_label(c_error(), "❌"); + ui.add(egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg")) + .max_width(18.0).max_height(18.0) + .tint(c_error())); ui.colored_label(c_error(), msg); }); }); @@ -286,7 +289,9 @@ pub fn warning_box(ui: &mut egui::Ui, msg: &str) { .inner_margin(egui::Margin::same(10.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.colored_label(c_warning(), "⚠️"); + ui.add(egui::Image::new(egui::include_image!("../../assets/icons/warning-icon.svg")) + .max_width(18.0).max_height(18.0) + .tint(c_warning())); ui.colored_label(c_warning(), msg); }); }); @@ -301,7 +306,9 @@ pub fn success_box(ui: &mut egui::Ui, msg: &str) { .inner_margin(egui::Margin::same(10.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.colored_label(c_success(), "✅"); + ui.add(egui::Image::new(egui::include_image!("../../assets/icons/check.svg")) + .max_width(18.0).max_height(18.0) + .tint(c_success())); ui.colored_label(c_success(), msg); }); });