chore(lint): solve clippy warnings

This commit is contained in:
iamanaws 2025-10-23 21:30:20 -07:00
commit 7580105910
11 changed files with 801 additions and 596 deletions

View file

@ -53,6 +53,7 @@ pub fn to_trimmed_utf8(v: Vec<u8>) -> String {
}
#[must_use]
#[cfg(debug_assertions)]
fn is_version_triple(s: &str) -> bool {
let mut components = s.split('.');
for _ in 0..3 {
@ -439,7 +440,7 @@ impl UserInfo {
/// Check if the user was logged-in
/// at the time `pm list users` was invoked
#[must_use]
#[allow(dead_code)]
#[allow(dead_code, reason = "Currently unused by UI; kept for future features")]
pub const fn was_running(&self) -> bool {
self.running
}

View file

@ -1,3 +1,4 @@
use crate::core::theme::Theme;
use crate::gui::style;
use iced::Element;
use iced::widget::button;
@ -5,8 +6,8 @@ use iced::widget::button::Button;
/// Wrapper function for `iced::widget::button` with padding and style applied
pub fn button_primary<'a, Message>(
content: impl Into<Element<'a, Message, crate::core::theme::Theme>>,
) -> Button<'a, Message, crate::core::theme::Theme> {
content: impl Into<Element<'a, Message, Theme>>,
) -> Button<'a, Message, Theme> {
button(content)
.padding([5, 10])
.style(style::Button::Primary)

View file

@ -59,6 +59,8 @@ pub enum AdbError {
/// Run an arbitrary shell action via the typed ADB wrapper.
/// This replaces the deprecated `adb_shell_command`.
///
/// If `serial` is empty, it lets ADB choose the default device.
pub async fn run_adb_action<S: AsRef<str>>(
device_serial: S,
action: String,

View file

@ -37,7 +37,7 @@ pub struct BaseColors {
#[derive(Debug, Clone, Copy)]
pub struct NormalColors {
pub primary: Color,
#[allow(dead_code)]
#[allow(dead_code, reason = "Reserved for future palette updates")]
pub secondary: Color,
pub surface: Color,
pub error: Color,

View file

@ -138,7 +138,7 @@ pub fn string_to_theme(theme: &str) -> Theme {
pub fn setup_uad_dir(dir: &Path) -> PathBuf {
let dir = dir.join("uad");
if let Err(e) = fs::create_dir_all(&dir) {
error!("Can't create directory: {dir:?}");
error!("Can't create directory: {}", dir.display());
panic!("{e}");
}
dir
@ -155,8 +155,10 @@ pub fn open_url(dir: PathBuf) {
Ok(o) => {
if !o.status.success() {
// does Windows print UTF-16?
let stderr = String::from_utf8(o.stderr).unwrap().trim_end().to_string();
error!("Can't open the following URL: {stderr}");
match String::from_utf8(o.stderr) {
Ok(s) => error!("Can't open the following URL: {}", s.trim_end()),
Err(_e) => error!("Can't open the following URL: <non-UTF8 output>"),
}
}
}
Err(e) => error!("Failed to run command to open the file explorer: {e}"),
@ -213,10 +215,11 @@ impl fmt::Display for DisplayablePath {
error!("[PATH STEM]: No file stem found");
"[File steam not found]".to_string()
},
|p| match p.to_os_string().into_string() {
Ok(stem) => stem,
Err(e) => {
error!("[PATH ENCODING]: {e:?}");
|p| {
if let Ok(stem) = p.to_os_string().into_string() {
stem
} else {
error!("[PATH ENCODING]: {}", self.path.display());
"[PATH ENCODING ERROR]".to_string()
}
},

View file

@ -92,7 +92,10 @@ impl UadGui {
)
}
#[allow(clippy::too_many_lines)]
#[allow(
clippy::too_many_lines,
reason = "Root GUI update orchestrates many flows"
)]
fn update(&mut self, msg: Message) -> Task<Message> {
match msg {
Message::LoadDevices(devices_list) => {
@ -332,7 +335,7 @@ impl UadGui {
}
}
fn view(&self) -> Element<Message, Theme> {
fn view(&self) -> Element<'_, Message, Theme> {
let navigation_container = nav_menu(
&self.devices_list,
self.selected_device.clone(),

View file

@ -1,3 +1,9 @@
#![allow(
non_snake_case,
clippy::trivially_copy_pass_by_ref,
clippy::wildcard_imports,
reason = "Iced style modules use PascalCase and &Theme; wildcard for local convenience"
)]
use crate::core::theme::Theme;
use iced::widget::{
button, checkbox, container, overlay, pick_list, radio, scrollable, text, text_editor,
@ -187,7 +193,7 @@ impl pick_list::Catalog for Theme {
text_color: p.bright.surface,
placeholder_color: p.bright.surface,
handle_color: p.bright.surface,
background: Background::Color(p.base.background.into()),
background: Background::Color(p.base.background),
border: Border {
color: border_color,
width: 1.0,
@ -268,9 +274,9 @@ impl text_editor::Catalog for Theme {
type Class<'a> = text_editor::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as text_editor::Catalog>::Class<'a> {
Box::new(|t: &Theme, s: text_editor::Status| {
Box::new(|t: &Theme, _s: text_editor::Status| {
let p = t.palette();
let active = text_editor::Style {
text_editor::Style {
background: Background::Color(p.base.foreground),
border: Border {
color: Color::TRANSPARENT,
@ -284,12 +290,6 @@ impl text_editor::Catalog for Theme {
a: 0.3,
..p.normal.primary
},
};
match s {
text_editor::Status::Active
| text_editor::Status::Focused
| text_editor::Status::Hovered => active,
text_editor::Status::Disabled => active,
}
})
}
@ -327,11 +327,10 @@ impl iced::widget::rule::Catalog for Theme {
}
}
#[allow(non_snake_case)]
pub mod Container {
use super::*;
#[allow(dead_code)]
#[allow(dead_code, reason = "Used by other themes or future styles")]
pub fn Invisible(_: &Theme) -> container::Style {
container::Style::default()
}
@ -364,7 +363,7 @@ pub mod Container {
}
}
#[allow(dead_code)]
#[allow(dead_code, reason = "Currently unused in some views")]
pub fn Tooltip(theme: &Theme) -> container::Style {
let p = theme.palette();
container::Style {
@ -394,11 +393,13 @@ pub mod Container {
}
}
#[allow(non_snake_case)]
pub mod Button {
use super::*;
#[allow(dead_code)]
#[allow(
dead_code,
reason = "Helper used by multiple styles; may be inlined by compiler"
)]
fn base(border_color: Color) -> button::Style {
button::Style {
background: None,
@ -421,7 +422,10 @@ pub mod Button {
style
}
#[allow(dead_code)]
#[allow(
dead_code,
reason = "Alias kept for semantic clarity in some call-sites"
)]
pub fn SelfUpdate(theme: &Theme, status: button::Status) -> button::Style {
Primary(theme, status)
}
@ -451,7 +455,10 @@ pub mod Button {
style
}
#[allow(dead_code)]
#[allow(
dead_code,
reason = "Style exposed for disabled state buttons in some contexts"
)]
pub fn Unavailable(theme: &Theme, status: button::Status) -> button::Style {
UninstallPackage(theme, status)
}
@ -459,16 +466,6 @@ pub mod Button {
pub fn NormalPackage(theme: &Theme, status: button::Status) -> button::Style {
let p = theme.palette();
match status {
button::Status::Active | button::Status::Pressed => button::Style {
background: Some(Background::Color(p.base.foreground)),
text_color: p.bright.surface,
border: Border {
color: p.base.background,
width: 0.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
},
button::Status::Hovered => button::Style {
background: Some(Background::Color(Color {
a: 0.25,
@ -482,7 +479,7 @@ pub mod Button {
},
shadow: Shadow::default(),
},
button::Status::Disabled => button::Style {
_ => button::Style {
background: Some(Background::Color(p.base.foreground)),
text_color: p.bright.surface,
border: Border {
@ -512,7 +509,7 @@ pub mod Button {
}
}
#[allow(dead_code)]
#[allow(dead_code, reason = "Used in views where buttons must be invisible")]
pub fn Hidden(_: &Theme, _: button::Status) -> button::Style {
button::Style {
background: Some(Background::Color(Color::TRANSPARENT)),
@ -566,11 +563,10 @@ pub mod Button {
}
}
#[allow(non_snake_case)]
pub mod Scrollable {
use super::*;
#[allow(dead_code)]
#[allow(dead_code, reason = "Kept for future custom rails variations")]
fn rails(scroller_color: Color) -> (scrollable::Rail, scrollable::Rail) {
let rail = scrollable::Rail {
background: Some(Background::Color(Color::TRANSPARENT)),
@ -614,71 +610,69 @@ pub mod Scrollable {
}
}
#[allow(non_snake_case)]
pub mod CheckBox {
use super::*;
pub fn PackageEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
let pal = theme.palette();
let p = theme.palette();
checkbox::Style {
background: Background::Color(pal.base.background),
icon_color: pal.bright.primary,
background: Background::Color(p.base.background),
icon_color: p.bright.primary,
border: Border {
color: pal.base.background,
color: p.base.background,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(pal.bright.surface),
text_color: Some(p.bright.surface),
}
}
pub fn PackageDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
let pal = theme.palette();
let p = theme.palette();
checkbox::Style {
background: Background::Color(Color {
a: 0.55,
..pal.base.background
..p.base.background
}),
icon_color: pal.bright.primary,
icon_color: p.bright.primary,
border: Border {
color: pal.normal.primary,
color: p.normal.primary,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(pal.normal.primary),
text_color: Some(p.normal.primary),
}
}
pub fn SettingsEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
let pal = theme.palette();
let p = theme.palette();
checkbox::Style {
background: Background::Color(pal.base.background),
icon_color: pal.bright.primary,
background: Background::Color(p.base.background),
icon_color: p.bright.primary,
border: Border {
color: pal.bright.primary,
color: p.bright.primary,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(pal.bright.surface),
text_color: Some(p.bright.surface),
}
}
pub fn SettingsDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
let pal = theme.palette();
let p = theme.palette();
checkbox::Style {
background: Background::Color(pal.base.foreground),
icon_color: pal.bright.primary,
background: Background::Color(p.base.foreground),
icon_color: p.bright.primary,
border: Border {
color: pal.normal.primary,
color: p.normal.primary,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(pal.bright.surface),
text_color: Some(p.bright.surface),
}
}
}
#[allow(non_snake_case)]
pub mod Text {
use super::*;
@ -708,7 +702,10 @@ pub mod Text {
}
}
#[allow(dead_code)]
#[allow(
dead_code,
reason = "Convenience factory used by dynamic text coloring"
)]
pub fn Color(c: Color) -> impl Fn(&Theme) -> text::Style {
move |_t: &Theme| text::Style { color: Some(c) }
}

View file

@ -23,12 +23,20 @@ pub enum Message {
}
impl About {
#[allow(
clippy::unused_self,
reason = "Trait-like shape required by GUI architecture"
)]
pub fn update(&mut self, msg: Message) {
if let Message::UrlPressed(url) = msg {
open_url(url);
}
// other events are handled by UadGui update()
}
#[allow(
clippy::unused_self,
reason = "Trait-like shape required by GUI architecture"
)]
pub fn view(&self, update_state: &UpdateState) -> Element<'_, Message, Theme, Renderer> {
let about_text = text(format!(
"Universal Android Debloater Next Generation ({NAME}) is a free and open-source community project \naiming at simplifying the removal of pre-installed apps on any Android device."

View file

@ -14,6 +14,7 @@ use crate::gui::views::settings::Settings;
use crate::gui::widgets::modal::Modal;
use crate::gui::widgets::package_row::{Message as RowMessage, PackageRow};
use crate::gui::widgets::text;
use iced::widget::scrollable::{Direction, Scrollbar};
use iced::widget::{
Column, Space, button, checkbox, column, container, horizontal_space, pick_list, radio, row,
scrollable, text_editor, text_input, tooltip, vertical_rule,
@ -113,7 +114,6 @@ impl From<Removal> for SummaryEntry {
}
impl List {
#[allow(clippy::too_many_lines)]
pub fn update(
&mut self,
settings: &mut Settings,
@ -121,272 +121,39 @@ impl List {
list_update_state: &mut UadListState,
message: Message,
) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index;
match message {
Message::ModalHide => {
self.selection_modal = false;
self.error_modal = None;
self.export_modal = false;
Task::none()
}
Message::ModalValidate => {
let mut commands = vec![];
self.selected_packages.sort_unstable();
self.selected_packages.dedup();
for selection in &self.selected_packages {
commands.append(&mut build_action_pkg_commands(
&self.phone_packages,
selected_device,
&settings.device,
*selection,
));
}
self.selection_modal = false;
Task::batch(commands)
}
Message::RestoringDevice(output) => {
if let Ok(p) = output {
self.loading_state = LoadingState::RestoringDevice(
self.phone_packages[i_user][p.index].name.clone(),
);
} else {
self.loading_state = LoadingState::RestoringDevice("Error [TODO]".to_string());
}
Task::none()
}
Message::LoadUadList(remote) => {
info!("{:-^65}", "-");
info!(
"ANDROID_SDK: {} | DEVICE: {}",
selected_device.android_sdk, selected_device.model
);
info!("{:-^65}", "-");
self.loading_state = LoadingState::DownloadingList;
Task::perform(
Self::init_apps_view(remote, selected_device.clone()),
Message::LoadPhonePackages,
)
}
Message::LoadPhonePackages((uad_list, list_state)) => {
self.loading_state = LoadingState::LoadingPackages;
self.uad_lists.clone_from(&uad_list);
*list_update_state = list_state;
Task::perform(
Self::load_packages(
uad_list,
selected_device.adb_id.clone(),
selected_device.user_list.clone(),
),
Message::ApplyFilters,
)
}
Message::ApplyFilters(packages) => {
self.phone_packages = packages;
self.filtered_packages = (0..self.phone_packages[i_user].len()).collect();
self.selected_package_state = Some(PackageState::Enabled);
self.selected_removal = Some(Removal::Recommended);
self.selected_list = Some(UadList::All);
self.selected_user = Some(User::default());
Self::filter_package_lists(self);
self.loading_state = LoadingState::Ready;
Task::none()
Message::ModalHide => self.on_modal_hide(),
Message::ModalValidate => self.on_modal_validate(settings, selected_device),
Message::RestoringDevice(output) => self.on_restoring_device(output),
Message::LoadUadList(remote) => self.on_load_uad_list(remote, selected_device),
Message::LoadPhonePackages(payload) => {
self.on_load_phone_packages(payload, selected_device, list_update_state)
}
Message::ApplyFilters(packages) => self.on_apply_filters(packages),
Message::ToggleAllSelected(selected) => {
for i in self.filtered_packages.clone() {
if self.phone_packages[i_user][i].selected != selected {
#[expect(unused_must_use, reason = "side-effect")]
self.update(
settings,
selected_device,
list_update_state,
Message::List(i, RowMessage::ToggleSelection(selected)),
);
}
}
self.all_selected = selected;
Task::none()
}
Message::SearchInputChanged(letter) => {
self.input_value = letter;
Self::filter_package_lists(self);
Task::none()
}
Message::ListSelected(list) => {
self.selected_list = Some(list);
Self::filter_package_lists(self);
Task::none()
}
Message::PackageStateSelected(package_state) => {
self.selected_package_state = Some(package_state);
Self::filter_package_lists(self);
Task::none()
}
Message::RemovalSelected(removal) => {
self.selected_removal = Some(removal);
Self::filter_package_lists(self);
Task::none()
}
Message::List(i_package, row_message) => {
#[expect(unused_must_use, reason = "side-effect")]
{
self.phone_packages[i_user][i_package]
.update(&row_message)
.map(move |row_message| Message::List(i_package, row_message));
}
let package = &mut self.phone_packages[i_user][i_package];
match row_message {
RowMessage::ToggleSelection(toggle) => {
if package.removal == Removal::Unsafe && !settings.general.expert_mode {
package.selected = false;
return Task::none();
}
if settings.device.multi_user_mode {
for u in selected_device.user_list.iter().filter(|&u| !u.protected) {
if let Some(pkg) = self
.phone_packages
.get_mut(u.index)
.and_then(|pkgs| pkgs.get_mut(i_package))
{
pkg.selected = toggle;
if toggle
&& !self.selected_packages.contains(&(u.index, i_package))
{
self.selected_packages.push((u.index, i_package));
}
}
}
if !toggle {
self.selected_packages.retain(|&x| x.1 != i_package);
}
} else {
package.selected = toggle;
if toggle {
if !self.selected_packages.contains(&(i_user, i_package)) {
self.selected_packages.push((i_user, i_package));
}
} else {
self.selected_packages
.retain(|&x| x.1 != i_package || x.0 != i_user);
}
}
Task::none()
}
RowMessage::ActionPressed => {
self.phone_packages[i_user][i_package].selected = true;
Task::batch(build_action_pkg_commands(
&self.phone_packages,
selected_device,
&settings.device,
(i_user, i_package),
))
}
RowMessage::PackagePressed => {
self.description = package.clone().description;
self.description_content =
text_editor::Content::with_text(&package.description);
package.current = true;
if self.current_package_index != i_package {
self.phone_packages[i_user][self.current_package_index].current = false;
}
self.current_package_index = i_package;
Task::none()
}
}
}
Message::ApplyActionOnSelection => {
self.selection_modal = true;
Task::none()
}
Message::UserSelected(user) => {
self.selected_user = Some(user);
self.filtered_packages = (0..self.phone_packages[user.index].len()).collect();
Self::filter_package_lists(self);
Task::none()
}
Message::ChangePackageState(res) => {
match res {
Ok(p) => {
let package = &mut self.phone_packages[p.i_user][p.index];
package.state = package.state.opposite(settings.device.disable_mode);
package.selected = false;
self.selected_packages
.retain(|&x| x.1 != p.index && x.0 != p.i_user);
Self::filter_package_lists(self);
}
Err(AdbError::Generic(err)) => {
self.error_modal = Some(err);
}
}
Task::none()
self.on_toggle_all_selected(selected, settings, selected_device, list_update_state)
}
Message::SearchInputChanged(letter) => self.on_search_input_changed(letter),
Message::ListSelected(list) => self.on_list_selected(list),
Message::PackageStateSelected(state) => self.on_package_state_selected(state),
Message::RemovalSelected(removal) => self.on_removal_selected(removal),
Message::List(i, row_msg) => self.on_list_row(i, &row_msg, settings, selected_device),
Message::ApplyActionOnSelection => self.on_apply_action_on_selection(),
Message::UserSelected(user) => self.on_user_selected(user),
Message::ChangePackageState(res) => self.on_change_package_state(res, settings),
Message::ModalUserSelected(user) => {
self.selected_user = Some(user);
self.update(
settings,
selected_device,
list_update_state,
Message::UserSelected(user),
)
}
Message::ClearSelectedPackages => {
self.selected_packages = Vec::new();
Task::none()
}
Message::ADBSatisfied(result) => {
self.is_adb_satisfied = result;
Task::none()
}
Message::UpdateFailed => {
self.loading_state = LoadingState::FailedToUpdate;
Task::none()
}
Message::GoToUrl(url) => {
open_url(url);
Task::none()
}
Message::ExportSelection => Task::perform(
export_selection(self.phone_packages[i_user].clone()),
Message::SelectionExported,
),
Message::SelectionExported(export) => {
match export {
Ok(_) => self.export_modal = true,
Err(err) => error!("Failed to export current selection: {err:?}"),
}
Task::none()
self.on_modal_user_selected(user, settings, selected_device, list_update_state)
}
Message::ClearSelectedPackages => self.on_clear_selected_packages(),
Message::ADBSatisfied(result) => self.on_adb_satisfied(result),
Message::UpdateFailed => self.on_update_failed(),
Message::GoToUrl(url) => Self::on_go_to_url(url),
Message::ExportSelection => self.on_export_selection(),
Message::SelectionExported(res) => self.on_selection_exported(res),
Message::Nothing => Task::none(),
Message::DescriptionEdit(action) => {
match action {
text_editor::Action::Edit(_) => {
// Do nothing - ignore all editing operations
}
text_editor::Action::Scroll { lines: _ } => {}
// Allow all other actions (movement, selection, clicking, scrolling, etc.)
_ => {
self.description_content.perform(action);
}
}
Task::none()
}
Message::CopyError(err) => {
self.copy_confirmation = true;
Task::batch(vec![
iced::clipboard::write::<Message>(err),
Task::perform(
// intentional delay
async { std::thread::sleep(std::time::Duration::from_secs(1)) },
|()| Message::HideCopyConfirmation,
),
])
}
Message::HideCopyConfirmation => {
self.copy_confirmation = false;
Task::none()
}
Message::DescriptionEdit(action) => self.on_description_edit(action),
Message::CopyError(err) => self.on_copy_error(err),
Message::HideCopyConfirmation => self.on_hide_copy_confirmation(),
}
}
@ -510,7 +277,10 @@ impl List {
.into()
}
#[allow(clippy::too_many_lines)]
#[allow(
clippy::too_many_lines,
reason = "Complex layout; further refactor later"
)]
fn ready_view(
&self,
settings: &Settings,
@ -672,7 +442,10 @@ impl List {
}
}
#[allow(clippy::too_many_lines)]
#[allow(
clippy::too_many_lines,
reason = "Modal construction is verbose by nature"
)]
fn apply_selection_modal(
&self,
device: &Phone,
@ -712,7 +485,7 @@ impl List {
.style(style::Container::Frame)
.padding([10, 0])
.center_y(Length::Shrink)
.center_x(Length::Shrink);
.center_x(Length::Fill);
let users_ctn = container(radio_btn_users)
.padding(10)
@ -777,7 +550,7 @@ impl List {
s.0 == self.selected_user.expect(PACK_NO_USER_MSG).index
})
.fold(
column![].spacing(6).width(Length::Fill),
column![].spacing(6).width(Length::Shrink),
|col, selection| {
col.push(
row![
@ -792,14 +565,13 @@ impl List {
.uad_list
.to_string()
)]
.width(50),
.width(55),
row![text(
self.phone_packages[selection.0][selection.1]
.name
.clone()
),]
.width(540),
horizontal_space(),
row![match self.phone_packages[selection.0]
[selection.1]
.state
@ -821,7 +593,7 @@ impl List {
},]
.width(70),
]
.width(Length::Fill)
.width(Length::Shrink)
.spacing(20),
)
},
@ -833,8 +605,13 @@ impl List {
},
)
.padding(10)
.width(Length::Fill),
.width(Length::Shrink)
.style(style::Container::Invisible),
)
.direction(Direction::Both {
vertical: Scrollbar::default(),
horizontal: Scrollbar::default(),
})
.style(style::Scrollable::Description),
)
.width(Length::Fill)
@ -885,6 +662,7 @@ impl List {
.style(style::Container::Background)
.into()
}
fn filter_package_lists(&mut self) {
let list_filter: UadList = self.selected_list.expect("UAD-list type must be selected");
let package_filter: PackageState = self
@ -911,6 +689,7 @@ impl List {
.map(|(i, _)| i)
.collect();
}
#[expect(clippy::unused_async, reason = "1 call-site")]
async fn load_packages<S: AsRef<str>>(
uad_list: PackageHashMap,
@ -946,6 +725,326 @@ impl List {
}
}
}
// === Split handlers to keep update short ===
fn on_modal_hide(&mut self) -> Task<Message> {
self.selection_modal = false;
self.error_modal = None;
self.export_modal = false;
Task::none()
}
fn on_modal_validate(
&mut self,
settings: &Settings,
selected_device: &mut Phone,
) -> Task<Message> {
let mut commands = vec![];
self.selected_packages.sort_unstable();
self.selected_packages.dedup();
for selection in &self.selected_packages {
commands.append(&mut build_action_pkg_commands(
&self.phone_packages,
selected_device,
&settings.device,
*selection,
));
}
self.selection_modal = false;
Task::batch(commands)
}
fn on_restoring_device(&mut self, output: Result<PackageInfo, AdbError>) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index;
if let Ok(p) = output {
self.loading_state =
LoadingState::RestoringDevice(self.phone_packages[i_user][p.index].name.clone());
} else {
self.loading_state = LoadingState::RestoringDevice("Error [TODO]".to_string());
}
Task::none()
}
fn on_load_uad_list(&mut self, remote: bool, selected_device: &Phone) -> Task<Message> {
info!("{:-^65}", "-");
info!(
"ANDROID_SDK: {} | DEVICE: {}",
selected_device.android_sdk, selected_device.model
);
info!("{:-^65}", "-");
self.loading_state = LoadingState::DownloadingList;
Task::perform(
Self::init_apps_view(remote, selected_device.clone()),
Message::LoadPhonePackages,
)
}
fn on_load_phone_packages(
&mut self,
payload: (PackageHashMap, UadListState),
selected_device: &Phone,
list_update_state: &mut UadListState,
) -> Task<Message> {
let (uad_list, list_state) = payload;
self.loading_state = LoadingState::LoadingPackages;
self.uad_lists.clone_from(&uad_list);
*list_update_state = list_state;
Task::perform(
Self::load_packages(
uad_list,
selected_device.adb_id.clone(),
selected_device.user_list.clone(),
),
Message::ApplyFilters,
)
}
fn on_apply_filters(&mut self, packages: Vec<Vec<PackageRow>>) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index;
self.phone_packages = packages;
self.filtered_packages = (0..self.phone_packages[i_user].len()).collect();
self.selected_package_state = Some(PackageState::Enabled);
self.selected_removal = Some(Removal::Recommended);
self.selected_list = Some(UadList::All);
self.selected_user = Some(User::default());
Self::filter_package_lists(self);
self.loading_state = LoadingState::Ready;
Task::none()
}
fn on_toggle_all_selected(
&mut self,
selected: bool,
settings: &mut Settings,
selected_device: &mut Phone,
list_update_state: &mut UadListState,
) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index;
for i in self.filtered_packages.clone() {
if self.phone_packages[i_user][i].selected != selected {
#[expect(unused_must_use, reason = "side-effect")]
self.update(
settings,
selected_device,
list_update_state,
Message::List(i, RowMessage::ToggleSelection(selected)),
);
}
}
self.all_selected = selected;
Task::none()
}
fn on_search_input_changed(&mut self, letter: String) -> Task<Message> {
self.input_value = letter;
Self::filter_package_lists(self);
Task::none()
}
fn on_list_selected(&mut self, list: UadList) -> Task<Message> {
self.selected_list = Some(list);
Self::filter_package_lists(self);
Task::none()
}
fn on_package_state_selected(&mut self, package_state: PackageState) -> Task<Message> {
self.selected_package_state = Some(package_state);
Self::filter_package_lists(self);
Task::none()
}
fn on_removal_selected(&mut self, removal: Removal) -> Task<Message> {
self.selected_removal = Some(removal);
Self::filter_package_lists(self);
Task::none()
}
fn on_list_row(
&mut self,
i_package: usize,
row_message: &RowMessage,
settings: &Settings,
selected_device: &mut Phone,
) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index;
#[expect(unused_must_use, reason = "side-effect")]
{
self.phone_packages[i_user][i_package]
.update(row_message)
.map(move |row_message| Message::List(i_package, row_message));
}
let package = &mut self.phone_packages[i_user][i_package];
match *row_message {
RowMessage::ToggleSelection(toggle) => {
if package.removal == Removal::Unsafe && !settings.general.expert_mode {
package.selected = false;
return Task::none();
}
if settings.device.multi_user_mode {
for u in selected_device.user_list.iter().filter(|&u| !u.protected) {
if let Some(pkg) = self
.phone_packages
.get_mut(u.index)
.and_then(|pkgs| pkgs.get_mut(i_package))
{
pkg.selected = toggle;
if toggle && !self.selected_packages.contains(&(u.index, i_package)) {
self.selected_packages.push((u.index, i_package));
}
}
}
if !toggle {
self.selected_packages.retain(|&x| x.1 != i_package);
}
} else {
package.selected = toggle;
if toggle {
if !self.selected_packages.contains(&(i_user, i_package)) {
self.selected_packages.push((i_user, i_package));
}
} else {
self.selected_packages
.retain(|&x| x.1 != i_package || x.0 != i_user);
}
}
Task::none()
}
RowMessage::ActionPressed => {
self.phone_packages[i_user][i_package].selected = true;
Task::batch(build_action_pkg_commands(
&self.phone_packages,
selected_device,
&settings.device,
(i_user, i_package),
))
}
RowMessage::PackagePressed => {
self.description = package.clone().description;
self.description_content = text_editor::Content::with_text(&package.description);
package.current = true;
if self.current_package_index != i_package {
self.phone_packages[i_user][self.current_package_index].current = false;
}
self.current_package_index = i_package;
Task::none()
}
}
}
fn on_apply_action_on_selection(&mut self) -> Task<Message> {
self.selection_modal = true;
Task::none()
}
fn on_user_selected(&mut self, user: User) -> Task<Message> {
self.selected_user = Some(user);
self.filtered_packages = (0..self.phone_packages[user.index].len()).collect();
Self::filter_package_lists(self);
Task::none()
}
fn on_change_package_state(
&mut self,
res: Result<PackageInfo, AdbError>,
settings: &Settings,
) -> Task<Message> {
match res {
Ok(p) => {
let package = &mut self.phone_packages[p.i_user][p.index];
package.state = package.state.opposite(settings.device.disable_mode);
package.selected = false;
self.selected_packages
.retain(|&x| x.1 != p.index && x.0 != p.i_user);
Self::filter_package_lists(self);
}
Err(AdbError::Generic(err)) => {
self.error_modal = Some(err);
}
}
Task::none()
}
fn on_modal_user_selected(
&mut self,
user: User,
settings: &mut Settings,
selected_device: &mut Phone,
list_update_state: &mut UadListState,
) -> Task<Message> {
self.selected_user = Some(user);
self.update(
settings,
selected_device,
list_update_state,
Message::UserSelected(user),
)
}
fn on_clear_selected_packages(&mut self) -> Task<Message> {
self.selected_packages = Vec::new();
Task::none()
}
fn on_adb_satisfied(&mut self, result: bool) -> Task<Message> {
self.is_adb_satisfied = result;
Task::none()
}
fn on_update_failed(&mut self) -> Task<Message> {
self.loading_state = LoadingState::FailedToUpdate;
Task::none()
}
fn on_go_to_url(url: PathBuf) -> Task<Message> {
open_url(url);
Task::none()
}
fn on_export_selection(&mut self) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index;
Task::perform(
export_selection(self.phone_packages[i_user].clone()),
Message::SelectionExported,
)
}
fn on_selection_exported(&mut self, export: Result<bool, String>) -> Task<Message> {
match export {
Ok(_) => self.export_modal = true,
Err(err) => error!("Failed to export current selection: {err:?}"),
}
Task::none()
}
fn on_description_edit(&mut self, action: text_editor::Action) -> Task<Message> {
match action {
text_editor::Action::Scroll { lines: _ } | text_editor::Action::Edit(_) => {}
_ => {
self.description_content.perform(action);
}
}
Task::none()
}
fn on_copy_error(&mut self, err: String) -> Task<Message> {
self.copy_confirmation = true;
Task::batch(vec![
iced::clipboard::write::<Message>(err),
Task::perform(
// intentional delay
async { std::thread::sleep(std::time::Duration::from_secs(1)) },
|()| Message::HideCopyConfirmation,
),
])
}
fn on_hide_copy_confirmation(&mut self) -> Task<Message> {
self.copy_confirmation = false;
Task::none()
}
}
fn error_view<'a>(
@ -978,11 +1077,7 @@ fn error_view<'a>(
} else {
Some(Message::CopyError(error.to_string()))
})
.style(if copy_confirmation {
style::Button::Primary
} else {
style::Button::Primary
}),
.style(style::Button::Primary),
button(
text("Close")
.width(Length::Fill)
@ -1048,7 +1143,7 @@ fn build_action_pkg_commands(
&& packages
.get(u.index)
.and_then(|user_pkgs| user_pkgs.get(selection.1))
.is_some_and(|p| p.selected || settings.multi_user_mode)
.is_some_and(|row_pkg| row_pkg.selected || settings.multi_user_mode)
}) {
let u_pkg = &packages[u.index][selection.1];
let wanted_state = if settings.multi_user_mode {

View file

@ -66,7 +66,6 @@ pub enum Message {
}
impl Settings {
#[allow(clippy::too_many_lines)]
pub fn update(
&mut self,
phone: &Phone,
@ -76,187 +75,290 @@ impl Settings {
selected_user: Option<User>,
) -> Task<Message> {
match msg {
Message::ModalHide => {
self.modal = None;
Task::none()
Message::ModalHide => self.handle_modal_hide(),
Message::ExpertMode(toggled) => self.handle_expert_mode(phone, toggled),
Message::DisableMode(toggled) => self.handle_disable_mode(phone, toggled),
Message::MultiUserMode(toggled) => self.handle_multi_user_mode(phone, toggled),
Message::ApplyTheme(theme) => self.handle_apply_theme(phone, theme),
Message::UrlPressed(url) => Self::handle_url_pressed(url),
Message::LoadDeviceSettings => self.handle_load_device_settings(phone),
Message::BackupSelected(d_path) => self.handle_backup_selected(d_path),
Message::BackupDevice => self.handle_backup_device(phone, packages),
Message::DeviceBackedUp(result) => self.handle_device_backed_up(phone, result),
Message::RestoreDevice => {
self.handle_restore_device(phone, packages, nb_running_async_adb_commands)
}
Message::ExpertMode(toggled) => {
self.general.expert_mode = toggled;
debug!("Config change: {self:?}");
Config::save_changes(self, &phone.adb_id);
Task::none()
}
Message::DisableMode(toggled) => {
if phone.android_sdk >= 23 {
self.device.disable_mode = toggled;
debug!("Config change: {self:?}");
Config::save_changes(self, &phone.adb_id);
}
Task::none()
}
Message::MultiUserMode(toggled) => {
self.device.multi_user_mode = toggled;
debug!("Config change: {self:?}");
Config::save_changes(self, &phone.adb_id);
Task::none()
}
Message::ApplyTheme(theme) => {
self.general.theme = theme.to_string();
debug!("Config change: {self:?}");
Config::save_changes(self, &phone.adb_id);
Task::none()
}
Message::UrlPressed(url) => {
open_url(url);
Task::none()
}
Message::LoadDeviceSettings => {
let backups =
list_available_backups(&self.general.backup_folder.join(&phone.adb_id));
let backup = BackupSettings {
backups: backups.clone(),
selected: backups.first().cloned(),
users: phone.user_list.clone(),
selected_user: phone.user_list.first().copied(),
backup_state: String::default(),
};
match Config::load_configuration_file()
.devices
.iter()
.find(|d| d.device_id == phone.adb_id)
{
Some(device) => {
self.device.clone_from(device);
self.device.backup = backup;
}
None => {
self.device = DeviceSettings {
device_id: phone.adb_id.clone(),
multi_user_mode: supports_multi_user(phone),
disable_mode: false,
backup,
}
}
}
Task::none()
}
Message::BackupSelected(d_path) => {
self.device.backup.selected = Some(d_path.clone());
self.device.backup.users = list_available_backup_user(d_path);
Task::none()
}
Message::BackupDevice => Task::perform(
backup_phone(
phone.user_list.clone(),
self.device.device_id.clone(),
packages.to_vec(),
),
Message::DeviceBackedUp,
),
Message::DeviceBackedUp(is_backed_up) => {
match is_backed_up {
Ok(_) => {
info!("[BACKUP] Backup successfully created");
self.device.backup.backups = list_available_backups(
&self.general.backup_folder.join(phone.adb_id.clone()),
);
self.device.backup.selected = self.device.backup.backups.first().cloned();
}
Err(err) => {
error!("[BACKUP FAILED] Backup creation failed: {err:?}");
}
}
Task::none()
}
Message::RestoreDevice => match restore_backup(phone, packages, &self.device) {
Ok(r_packages) => {
let mut commands = vec![];
*nb_running_async_adb_commands = 0;
for p in &r_packages {
let p_info = PackageInfo {
i_user: 0,
index: p.index,
removal: "RESTORE".to_string(),
};
for command in p.commands.clone() {
*nb_running_async_adb_commands += 1;
commands.push(Task::perform(
// This is "safe" thanks to serde:
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/issues/760
run_adb_action(phone.adb_id.clone(), command, p_info.clone()),
Message::RestoringDevice,
));
}
}
if r_packages.is_empty() {
if get_android_sdk(&phone.adb_id) == 0 {
self.device.backup.backup_state = "Device is not connected".to_string();
} else {
self.device.backup.backup_state =
"Device state is already restored".to_string();
}
}
info!(
"[RESTORE] Restoring backup {}",
self.device.backup.selected.as_ref().unwrap()
);
Task::batch(commands)
}
Err(e) => {
self.device.backup.backup_state.clone_from(&e);
error!("{} - {}", self.device.backup.selected.as_ref().unwrap(), e);
Task::none()
}
},
// Trigger an action in mod.rs (Message::SettingsAction(msg))
Message::RestoringDevice(_) => Task::none(),
Message::FolderChosen(result) => {
self.is_loading = false;
Message::FolderChosen(result) => self.handle_folder_chosen(phone, result),
Message::ChooseBackUpFolder => self.handle_choose_backup_folder(),
Message::ExportPackages => Self::handle_export_packages(selected_user, packages),
Message::PackagesExported(result) => self.handle_packages_exported(result),
}
}
if let Ok(path) = result {
self.general.backup_folder = path;
Config::save_changes(self, &phone.adb_id);
#[expect(unused_must_use, reason = "side-effect")]
{
self.update(
phone,
packages,
nb_running_async_adb_commands,
Message::LoadDeviceSettings,
selected_user,
);
fn handle_modal_hide(&mut self) -> Task<Message> {
self.modal = None;
Task::none()
}
fn handle_expert_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
self.general.expert_mode = toggled;
debug!("Config change: {self:?}");
Config::save_changes(self, &phone.adb_id);
Task::none()
}
fn handle_disable_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
if phone.android_sdk >= 23 {
self.device.disable_mode = toggled;
debug!("Config change: {self:?}");
Config::save_changes(self, &phone.adb_id);
}
Task::none()
}
fn handle_multi_user_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
self.device.multi_user_mode = toggled;
debug!("Config change: {self:?}");
Config::save_changes(self, &phone.adb_id);
Task::none()
}
fn handle_apply_theme(&mut self, phone: &Phone, theme: Theme) -> Task<Message> {
self.general.theme = theme.to_string();
debug!("Config change: {self:?}");
Config::save_changes(self, &phone.adb_id);
Task::none()
}
fn handle_url_pressed(url: PathBuf) -> Task<Message> {
open_url(url);
Task::none()
}
fn handle_load_device_settings(&mut self, phone: &Phone) -> Task<Message> {
self.load_device_settings(phone);
Task::none()
}
fn load_device_settings(&mut self, phone: &Phone) {
let backups = list_available_backups(&self.general.backup_folder.join(&phone.adb_id));
let backup = BackupSettings {
backups: backups.clone(),
selected: backups.first().cloned(),
users: phone.user_list.clone(),
selected_user: phone.user_list.first().copied(),
backup_state: String::default(),
};
match Config::load_configuration_file()
.devices
.iter()
.find(|d| d.device_id == phone.adb_id)
{
Some(device) => {
self.device.clone_from(device);
self.device.backup = backup;
}
None => {
self.device = DeviceSettings {
device_id: phone.adb_id.clone(),
multi_user_mode: supports_multi_user(phone),
disable_mode: false,
backup,
};
}
}
}
fn handle_backup_selected(&mut self, d_path: DisplayablePath) -> Task<Message> {
self.device.backup.selected = Some(d_path.clone());
self.device.backup.users = list_available_backup_user(d_path);
Task::none()
}
fn handle_backup_device(
&mut self,
phone: &Phone,
packages: &[Vec<PackageRow>],
) -> Task<Message> {
Task::perform(
backup_phone(
phone.user_list.clone(),
self.device.device_id.clone(),
packages.to_vec(),
),
Message::DeviceBackedUp,
)
}
fn handle_device_backed_up(
&mut self,
phone: &Phone,
result: Result<bool, String>,
) -> Task<Message> {
match result {
Ok(_) => {
info!("[BACKUP] Backup successfully created");
self.device.backup.backups =
list_available_backups(&self.general.backup_folder.join(phone.adb_id.clone()));
self.device.backup.selected = self.device.backup.backups.first().cloned();
}
Err(err) => {
error!("[BACKUP FAILED] Backup creation failed: {err:?}");
}
}
Task::none()
}
fn handle_restore_device(
&mut self,
phone: &Phone,
packages: &[Vec<PackageRow>],
nb_running_async_adb_commands: &mut u32,
) -> Task<Message> {
match restore_backup(phone, packages, &self.device) {
Ok(r_packages) => {
let mut commands = vec![];
*nb_running_async_adb_commands = 0;
for p in &r_packages {
let p_info = PackageInfo {
i_user: 0,
index: p.index,
removal: "RESTORE".to_string(),
};
for command in p.commands.clone() {
*nb_running_async_adb_commands += 1;
commands.push(Task::perform(
run_adb_action(phone.adb_id.clone(), command, p_info.clone()),
Message::RestoringDevice,
));
}
}
Task::none()
}
Message::ChooseBackUpFolder => {
if self.is_loading {
Task::none()
} else {
self.is_loading = true;
Task::perform(open_folder(), Message::FolderChosen)
if r_packages.is_empty() {
if get_android_sdk(&phone.adb_id) == 0 {
self.device.backup.backup_state = "Device is not connected".to_string();
} else {
self.device.backup.backup_state =
"Device state is already restored".to_string();
}
}
info!(
"[RESTORE] Restoring backup {}",
self.device.backup.selected.as_ref().unwrap()
);
Task::batch(commands)
}
Message::ExportPackages => Task::perform(
export_packages(selected_user.unwrap_or_default(), packages.to_vec()),
Message::PackagesExported,
),
Message::PackagesExported(exported) => {
match exported {
Ok(_) => self.modal = Some(PopUpModal::ExportUninstalled),
Err(err) => error!("Failed to export list of uninstalled packages: {err:?}"),
}
Err(e) => {
self.device.backup.backup_state.clone_from(&e);
error!("{} - {}", self.device.backup.selected.as_ref().unwrap(), e);
Task::none()
}
}
}
#[allow(clippy::too_many_lines)]
fn handle_folder_chosen(
&mut self,
phone: &Phone,
result: Result<PathBuf, Error>,
) -> Task<Message> {
self.is_loading = false;
if let Ok(path) = result {
self.general.backup_folder = path;
Config::save_changes(self, &phone.adb_id);
self.load_device_settings(phone);
}
Task::none()
}
fn handle_choose_backup_folder(&mut self) -> Task<Message> {
if self.is_loading {
Task::none()
} else {
self.is_loading = true;
Task::perform(open_folder(), Message::FolderChosen)
}
}
fn handle_export_packages(
selected_user: Option<User>,
packages: &[Vec<PackageRow>],
) -> Task<Message> {
Task::perform(
export_packages(selected_user.unwrap_or_default(), packages.to_vec()),
Message::PackagesExported,
)
}
fn handle_packages_exported(&mut self, result: Result<bool, String>) -> Task<Message> {
match result {
Ok(_) => self.modal = Some(PopUpModal::ExportUninstalled),
Err(err) => error!("Failed to export list of uninstalled packages: {err:?}"),
}
Task::none()
}
pub fn view(
&self,
phone: &Phone,
apps_view: &AppsView,
) -> Element<'_, Message, Theme, Renderer> {
let content = if phone.adb_id.is_empty() {
self.build_no_device_content()
} else {
self.build_device_content(phone, apps_view)
};
if let Some(PopUpModal::ExportUninstalled) = self.modal {
return Self::render_export_modal(content);
}
container(scrollable(content))
.padding(10)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn build_no_device_content(&self) -> Element<'_, Message, Theme, Renderer> {
column![
text("Theme").size(26),
self.theme_container(),
text("General").size(26),
self.general_container(),
text("Current device").size(26),
Self::no_device_container(),
text("Backup / Restore").size(26),
Self::no_device_container(),
]
.width(Length::Fill)
.spacing(20)
.into()
}
fn build_device_content(
&self,
phone: &Phone,
apps_view: &AppsView,
) -> Element<'_, Message, Theme, Renderer> {
column![
text("Theme").size(26),
self.theme_container(),
text("General").size(26),
self.general_container(),
text("Current device").size(26),
Self::warning_container(phone),
self.device_specific_container(phone),
text("Backup / Restore").size(26),
self.backup_restore_container(phone, apps_view),
]
.width(Length::Fill)
.spacing(20)
.into()
}
fn theme_container(&self) -> Element<'_, Message, Theme, Renderer> {
let radio_btn_theme = Theme::ALL
.iter()
.fold(row![].spacing(10), |column, option| {
@ -270,12 +372,16 @@ impl Settings {
.size(24),
)
});
let theme_ctn = container(radio_btn_theme)
container(radio_btn_theme)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame);
.style(style::Container::Frame)
.into()
}
fn general_container(&self) -> Element<'_, Message, Theme, Renderer> {
let expert_mode_checkbox = checkbox(
"Allow to uninstall packages marked as \"unsafe\" (I KNOW WHAT I AM DOING)",
self.general.expert_mode,
@ -306,7 +412,7 @@ impl Settings {
.spacing(10)
.align_y(Alignment::Center);
let general_ctn = container(
container(
column![
expert_mode_checkbox,
expert_mode_descr,
@ -318,9 +424,12 @@ impl Settings {
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame);
.style(style::Container::Frame)
.into()
}
let warning_ctn = container(
fn warning_container(phone: &Phone) -> Element<'static, Message, Theme, Renderer> {
container(
row![
text("The following settings only affect the currently selected device:")
.style(style::Text::Danger),
@ -332,8 +441,11 @@ impl Settings {
)
.padding(10)
.width(Length::Fill)
.style(style::Container::BorderedFrame);
.style(style::Container::BorderedFrame)
.into()
}
fn device_specific_container(&self, phone: &Phone) -> Element<'_, Message, Theme, Renderer> {
let multi_user_mode_descr = row![
text("This will not affect the following protected work profile users: ")
.style(style::Text::Commentary),
@ -375,8 +487,6 @@ impl Settings {
.height(22)
.style(style::Button::Unavailable);
// Disabling package without root isn't really possible before Android Oreo (8.0)
// see https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/ADB-reference
let disable_mode_checkbox = checkbox(
"Clear and disable packages instead of uninstalling them",
self.device.disable_mode,
@ -400,7 +510,7 @@ impl Settings {
.width(Length::Fill)
};
let device_specific_ctn = container(
container(
column![
multi_user_mode_checkbox,
multi_user_mode_descr,
@ -412,8 +522,15 @@ impl Settings {
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame);
.style(style::Container::Frame)
.into()
}
fn backup_restore_container(
&self,
phone: &Phone,
apps_view: &AppsView,
) -> Element<'_, Message, Theme, Renderer> {
let backup_pick_list = pick_list(
self.device.backup.backups.clone(),
self.device.backup.selected.clone(),
@ -475,101 +592,75 @@ impl Settings {
.align_y(Alignment::Center)
};
let no_device_ctn = || {
container(text("No device detected").style(style::Text::Danger))
.padding(10)
.width(Length::Fill)
.style(style::Container::BorderedFrame)
};
let export_row = row![
export_btn,
"Export uninstalled packages with their description",
Space::new(Length::Fill, Length::Shrink),
text(format!(
"Selected: user {}",
apps_view.selected_user.unwrap_or_default().id
)),
]
.spacing(10)
.align_y(Alignment::Center);
let content = if phone.adb_id.is_empty() {
column![
text("Theme").size(26),
theme_ctn,
text("General").size(26),
general_ctn,
text("Current device").size(26),
no_device_ctn(),
text("Backup / Restore").size(26),
no_device_ctn(),
]
.width(Length::Fill)
.spacing(20)
} else {
let export_row = row![
export_btn,
"Export uninstalled packages with their description",
Space::new(Length::Fill, Length::Shrink),
text(format!(
"Selected: user {}",
apps_view.selected_user.unwrap_or_default().id
)),
]
.spacing(10)
.align_y(Alignment::Center);
let backup_restore_ctn =
container(column![backup_row, restore_row, export_row].spacing(10))
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame);
column![
text("Theme").size(26),
theme_ctn,
text("General").size(26),
general_ctn,
text("Current device").size(26),
warning_ctn,
device_specific_ctn,
text("Backup / Restore").size(26),
backup_restore_ctn,
]
.width(Length::Fill)
.spacing(20)
};
if let Some(PopUpModal::ExportUninstalled) = self.modal {
let title = container(row![text("Success").size(24)].align_y(Alignment::Center))
.width(Length::Fill)
.style(style::Container::Frame)
.padding([10, 0])
.center_y(Length::Shrink)
.center_x(Length::Shrink);
let text_box = row![
text(format!("Exported uninstalled packages into file.\nFile is exported in same directory where {NAME} is located.")).width(Length::Fill),
].padding(20);
let file_row = row![
text(generate_backup_name(chrono::Local::now())).style(style::Text::Commentary)
]
.padding(20);
let modal_btn_row = row![
Space::new(Length::Fill, Length::Shrink),
button(text("Close").width(Length::Shrink))
.width(Length::Shrink)
.on_press(Message::ModalHide),
Space::new(Length::Fill, Length::Shrink),
];
let ctn = container(column![title, text_box, file_row, modal_btn_row])
.height(Length::Shrink)
.width(500)
.padding(10)
.style(style::Container::Frame);
return Modal::new(content.padding(10), ctn)
.on_blur(Message::ModalHide)
.into();
}
container(scrollable(content))
container(column![backup_row, restore_row, export_row].spacing(10))
.padding(10)
.width(Length::Fill)
.height(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame)
.into()
}
fn no_device_container() -> Element<'static, Message, Theme, Renderer> {
container(text("No device detected").style(style::Text::Danger))
.padding(10)
.width(Length::Fill)
.style(style::Container::BorderedFrame)
.into()
}
fn render_export_modal<'a>(
content: Element<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
let title = container(row![text("Success").size(24)].align_y(Alignment::Center))
.width(Length::Fill)
.style(style::Container::Frame)
.padding([10, 0])
.center_y(Length::Shrink)
.center_x(Length::Shrink);
let text_box = row![
text(format!(
"Exported uninstalled packages into file.\nFile is exported in same directory where {NAME} is located."
))
.width(Length::Fill),
]
.padding(20);
let file_row =
row![text(generate_backup_name(chrono::Local::now())).style(style::Text::Commentary)]
.padding(20);
let modal_btn_row = row![
Space::new(Length::Fill, Length::Shrink),
button(text("Close").width(Length::Shrink))
.width(Length::Shrink)
.on_press(Message::ModalHide),
Space::new(Length::Fill, Length::Shrink),
];
let ctn = container(column![title, text_box, file_row, modal_btn_row])
.height(Length::Shrink)
.width(500)
.padding(10)
.style(style::Container::Frame);
let padded_content: Element<'a, Message, Theme, Renderer> =
container(content).padding(10).into();
Modal::new(padded_content, ctn)
.on_blur(Message::ModalHide)
.into()
}
}

View file

@ -47,6 +47,10 @@ impl PackageRow {
}
}
#[allow(
clippy::unused_self,
reason = "Consistent component API; may change later"
)]
pub fn update(&mut self, _message: &Message) -> Task<Message> {
Task::none()
}