From 02ba6e2cca0a019165151311d283738c5e081170 Mon Sep 17 00:00:00 2001 From: Angel J <78835633+iamanaws@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:21:16 -0700 Subject: [PATCH] refactor: move theme to uad-gui; drop core gui deps; mark PmCommand must-use --- Cargo.lock | 2 - crates/uad-cli/Cargo.toml | 1 - crates/uad-cli/README.md | 2 +- crates/uad-core/Cargo.toml | 4 - crates/uad-core/src/adb.rs | 2 +- crates/uad-core/src/config.rs | 10 +- crates/uad-core/src/lib.rs | 1 - crates/uad-core/src/theme.rs | 231 ---------------------------------- crates/uad-core/src/utils.rs | 15 +-- crates/uad-gui/Cargo.toml | 3 +- crates/uad-gui/src/gui.rs | 10 +- crates/uad-gui/src/theme.rs | 219 ++++++++++++++++++++++++++------ flake.lock | 6 +- flake.nix | 8 +- 14 files changed, 205 insertions(+), 309 deletions(-) delete mode 100644 crates/uad-core/src/theme.rs diff --git a/Cargo.lock b/Cargo.lock index 63ccf02..8a0a55e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4558,10 +4558,8 @@ version = "1.2.0" dependencies = [ "chrono", "csv", - "dark-light", "dirs", "flate2", - "iced", "log", "retry", "serde", diff --git a/crates/uad-cli/Cargo.toml b/crates/uad-cli/Cargo.toml index d0118ae..6045f95 100644 --- a/crates/uad-cli/Cargo.toml +++ b/crates/uad-cli/Cargo.toml @@ -44,7 +44,6 @@ format_push_string = "warn" large_include_file = "warn" shadow_unrelated = "warn" struct_field_names = "allow" # annoying -module_name_repetitions = "allow" # annoying disallowed_types = "deny" disallowed_methods = "deny" diff --git a/crates/uad-cli/README.md b/crates/uad-cli/README.md index 86a2a77..5376342 100644 --- a/crates/uad-cli/README.md +++ b/crates/uad-cli/README.md @@ -191,7 +191,7 @@ uad completions powershell > uad.ps1 ## Examples -### Remove all Facebook packages +### Remove Facebook packages ```bash uad list --search facebook diff --git a/crates/uad-core/Cargo.toml b/crates/uad-core/Cargo.toml index b73d746..f7d3a43 100644 --- a/crates/uad-core/Cargo.toml +++ b/crates/uad-core/Cargo.toml @@ -12,7 +12,6 @@ edition.workspace = true [features] default = [] -gui = ["dep:iced", "dep:dark-light"] self-update = ["dep:flate2", "dep:tar"] [dependencies] @@ -25,8 +24,6 @@ dirs.workspace = true ureq.workspace = true retry.workspace = true csv.workspace = true -iced = { workspace = true, optional = true } -dark-light = { workspace = true, optional = true } flate2 = { workspace = true, optional = true } tar = { workspace = true, optional = true } @@ -47,7 +44,6 @@ format_push_string = "warn" large_include_file = "warn" shadow_unrelated = "warn" struct_field_names = "allow" # annoying -module_name_repetitions = "allow" # annoying disallowed_types = "deny" disallowed_methods = "deny" diff --git a/crates/uad-core/src/adb.rs b/crates/uad-core/src/adb.rs index 8d06268..1104b61 100644 --- a/crates/uad-core/src/adb.rs +++ b/crates/uad-core/src/adb.rs @@ -229,7 +229,6 @@ impl Default for ACommand { pub struct ShellCommand(ACommand); impl ShellCommand { /// `pm` command builder - #[must_use] pub fn pm(mut self) -> PmCommand { self.0.0.arg("pm"); PmCommand(self) @@ -335,6 +334,7 @@ pub const PM_CLEAR_PACK: &str = "pm clear"; /// Builder object for an Android Package Manager command. /// #[derive(Debug)] +#[must_use] pub struct PmCommand(ShellCommand); impl PmCommand { /// `list packages -s` sub-command, [`PACK_PREFIX`] stripped. diff --git a/crates/uad-core/src/config.rs b/crates/uad-core/src/config.rs index eebfc63..5522b16 100644 --- a/crates/uad-core/src/config.rs +++ b/crates/uad-core/src/config.rs @@ -1,13 +1,15 @@ use crate::CACHE_DIR; use crate::CONFIG_DIR; +use crate::sync::User; use crate::utils::DisplayablePath; -use crate::{sync::User, theme::Theme}; use log::error; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use std::sync::LazyLock; +const DEFAULT_THEME: &str = "Auto (follow system theme)"; + #[derive(Default, Debug, Serialize, Deserialize, Clone)] pub struct Config { pub general: GeneralSettings, @@ -44,7 +46,7 @@ pub struct DeviceSettings { impl Default for GeneralSettings { fn default() -> Self { Self { - theme: Theme::default().to_string(), + theme: DEFAULT_THEME.to_string(), expert_mode: false, backup_folder: CACHE_DIR.join("backups"), } @@ -114,7 +116,7 @@ mod tests { let config = Config::load_configuration_file(); // non-deterministic //assert_eq!(config.devices.len(), 0); - assert_eq!(config.general.theme, Theme::default().to_string()); + assert_eq!(config.general.theme, DEFAULT_THEME); assert!(!config.general.expert_mode); assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups")); } @@ -136,7 +138,7 @@ mod tests { fn test_default_config() { let config = Config::default(); assert_eq!(config.devices.len(), 0); - assert_eq!(config.general.theme, Theme::default().to_string()); + assert_eq!(config.general.theme, DEFAULT_THEME); assert!(!config.general.expert_mode); assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups")); } diff --git a/crates/uad-core/src/lib.rs b/crates/uad-core/src/lib.rs index 76b1271..be3e5b4 100644 --- a/crates/uad-core/src/lib.rs +++ b/crates/uad-core/src/lib.rs @@ -11,7 +11,6 @@ pub mod adb; pub mod config; pub mod save; pub mod sync; -pub mod theme; pub mod uad_lists; pub mod update; pub mod utils; diff --git a/crates/uad-core/src/theme.rs b/crates/uad-core/src/theme.rs deleted file mode 100644 index 71a8cfe..0000000 --- a/crates/uad-core/src/theme.rs +++ /dev/null @@ -1,231 +0,0 @@ -#[cfg(feature = "gui")] -use dark_light; -#[cfg(feature = "gui")] -use iced::theme::{self, Mode, Palette, Style}; -#[cfg(feature = "gui")] -use iced::{Color, color}; -#[cfg(feature = "gui")] -use std::sync::LazyLock; - -/* -In-memory caching. -This fixes the perf bug -caused by Iced repeatedly calling `palette`. - -Coincidentally, this also ensures consistent colors across the GUI, -at the cost of requiring a restart to update the palette. -(this is just a patch, not a fix) -*/ -#[cfg(feature = "gui")] -pub static OS_COLOR_SCHEME: LazyLock = - LazyLock::new(|| dark_light::detect().unwrap_or(dark_light::Mode::Unspecified)); - -#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] -/// Color scheme -pub enum Theme { - #[default] - /// `Dark` or `Light`, according to `dark_light` - Auto, - /// `Dark`-ish and purple - Lupin, - /// white on black - Dark, - /// black on white - Light, -} - -#[cfg(feature = "gui")] -#[derive(Debug, Clone, Copy)] -pub struct BaseColors { - pub background: Color, - pub foreground: Color, -} - -#[cfg(feature = "gui")] -#[derive(Debug, Clone, Copy)] -pub struct NormalColors { - pub primary: Color, - #[allow(dead_code, reason = "Reserved for future palette updates")] - pub secondary: Color, - pub surface: Color, - pub error: Color, -} - -#[cfg(feature = "gui")] -#[derive(Debug, Clone, Copy)] -pub struct BrightColors { - pub primary: Color, - pub secondary: Color, - pub surface: Color, - pub error: Color, -} - -#[cfg(feature = "gui")] -#[derive(Debug, Clone, Copy)] -pub struct ColorPalette { - pub base: BaseColors, - pub normal: NormalColors, - pub bright: BrightColors, -} - -impl Theme { - pub const ALL: [Self; 4] = [Self::Auto, Self::Lupin, Self::Dark, Self::Light]; - - #[allow( - clippy::unreadable_literal, - reason = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/578#discussion_r1759653408" - )] - /// This `fn` _could_ be `const`, - /// but `deref`ing a lazy-`static` is non-`const`. - #[must_use] - #[cfg(feature = "gui")] - pub fn palette(self) -> ColorPalette { - const DARK: ColorPalette = ColorPalette { - base: BaseColors { - background: color!(0x111111), - foreground: color!(0x1C1C1C), - }, - normal: NormalColors { - primary: color!(0x5E4266), - secondary: color!(0x386E50), - surface: color!(0x828282), - error: color!(0x992B2B), - }, - bright: BrightColors { - primary: color!(0xBA84FC), - secondary: color!(0x49EB7A), - surface: color!(0xE0E0E0), - error: color!(0xC13047), - }, - }; - const LIGHT: ColorPalette = ColorPalette { - base: BaseColors { - background: color!(0xEEEEEE), - foreground: color!(0xE0E0E0), - }, - normal: NormalColors { - primary: color!(0x818181), - secondary: color!(0xF9D659), - surface: color!(0x818181), - error: color!(0x992B2B), - }, - bright: BrightColors { - primary: color!(0x673AB7), - secondary: color!(0x3797A4), - surface: color!(0x000000), - error: color!(0xC13047), - }, - }; - const LUPIN: ColorPalette = ColorPalette { - base: BaseColors { - background: color!(0x282A36), - foreground: color!(0x353746), - }, - normal: NormalColors { - primary: color!(0x58406F), - secondary: color!(0x386E50), - surface: color!(0xA2A4A3), - error: color!(0xA13034), - }, - bright: BrightColors { - primary: color!(0xBD94F9), - secondary: color!(0x49EB7A), - surface: color!(0xF4F8F3), - error: color!(0xE63E6D), - }, - }; - match self { - Self::Dark => DARK, - Self::Light => LIGHT, - Self::Lupin => LUPIN, - Self::Auto => match *OS_COLOR_SCHEME { - dark_light::Mode::Light => LIGHT, - dark_light::Mode::Dark | dark_light::Mode::Unspecified => DARK, - }, - } - } -} - -impl std::fmt::Display for Theme { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Dark => "Dark", - Self::Light => "Light", - Self::Lupin => "Lupin", - Self::Auto => "Auto (follow system theme)", - } - ) - } -} - -/// Converts a string to the GUI's Theme type -#[must_use] -pub fn string_to_theme(theme: &str) -> Theme { - match theme { - "Lupin" => Theme::Lupin, - "Dark" => Theme::Dark, - "Light" => Theme::Light, - _ => Theme::Auto, - } -} - -#[cfg(feature = "gui")] -impl theme::Base for Theme { - fn default(preference: Mode) -> Self { - match preference { - Mode::Light => Self::Light, - Mode::Dark => Self::Dark, - Mode::None => match *OS_COLOR_SCHEME { - dark_light::Mode::Light => Self::Light, - dark_light::Mode::Dark | dark_light::Mode::Unspecified => Self::Dark, - }, - } - } - - fn mode(&self) -> Mode { - let resolved = match self { - Self::Auto => match *OS_COLOR_SCHEME { - dark_light::Mode::Light => Self::Light, - dark_light::Mode::Dark | dark_light::Mode::Unspecified => Self::Dark, - }, - other => *other, - }; - - match resolved { - Self::Light => Mode::Light, - _ => Mode::Dark, - } - } - - fn base(&self) -> Style { - let p = Self::palette(*self); - Style { - background_color: p.base.background, - text_color: p.bright.surface, - } - } - - fn palette(&self) -> Option { - let p = Self::palette(*self); - Some(Palette { - background: p.base.background, - text: p.bright.surface, - primary: p.normal.primary, - success: p.bright.secondary, - warning: p.normal.surface, - danger: p.normal.error, - }) - } - - fn name(&self) -> &str { - match self { - Self::Dark => "Dark", - Self::Light => "Light", - Self::Lupin => "Lupin", - Self::Auto => "Auto", - } - } -} diff --git a/crates/uad-core/src/utils.rs b/crates/uad-core/src/utils.rs index 0a89cdd..3569162 100644 --- a/crates/uad-core/src/utils.rs +++ b/crates/uad-core/src/utils.rs @@ -3,7 +3,6 @@ use crate::{ adb::{ACommand as AdbCommand, PmListPacksFlag}, sync::{CorePackage, User}, - theme::Theme, uad_lists::{PackageHashMap, PackageState, Removal, UadList}, }; use chrono::{DateTime, offset::Utc}; @@ -128,22 +127,10 @@ pub fn fetch_packages( }; user_package.push(package); } - user_package.sort_by_key(|a| a.name.to_lowercase()); + user_package.sort_by_key(|package| package.name.to_lowercase()); user_package } -#[must_use] -pub fn string_to_theme(theme: &str) -> Theme { - match theme { - "Dark" => Theme::Dark, - "Light" => Theme::Light, - "Lupin" => Theme::Lupin, - // Auto uses `Display`, so it doesn't have a canonical repr - t if t.starts_with("Auto") => Theme::Auto, - _ => Theme::default(), - } -} - #[must_use] pub fn setup_uad_dir(dir: &Path) -> PathBuf { let dir = dir.join("uad"); diff --git a/crates/uad-gui/Cargo.toml b/crates/uad-gui/Cargo.toml index abc4f42..4a50636 100644 --- a/crates/uad-gui/Cargo.toml +++ b/crates/uad-gui/Cargo.toml @@ -22,7 +22,7 @@ no-self-update = [] img = ["image", "iced/image"] [dependencies] -uad-core = { path = "../uad-core", features = ["gui", "self-update"] } +uad-core = { path = "../uad-core", features = ["self-update"] } iced.workspace = true image = { workspace = true, optional = true } rfd.workspace = true @@ -54,7 +54,6 @@ format_push_string = "warn" large_include_file = "warn" shadow_unrelated = "warn" struct_field_names = "allow" # annoying -module_name_repetitions = "allow" # annoying disallowed_types = "deny" disallowed_methods = "deny" diff --git a/crates/uad-gui/src/gui.rs b/crates/uad-gui/src/gui.rs index e171552..925aedf 100644 --- a/crates/uad-gui/src/gui.rs +++ b/crates/uad-gui/src/gui.rs @@ -1,7 +1,7 @@ -use crate::theme::string_to_theme; #[cfg(feature = "img")] use crate::theme::OS_COLOR_SCHEME; use crate::theme::Theme; +use crate::theme::string_to_theme; use crate::views::about::{About as AboutView, Message as AboutMessage}; use crate::views::list::{ List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage, @@ -9,13 +9,13 @@ use crate::views::list::{ use crate::views::settings::{Message as SettingsMessage, Settings as SettingsView}; use crate::widgets::navigation_menu::nav_menu; use iced::font; +use iced::widget::column; #[cfg(feature = "img")] use iced::window::icon; -#[cfg(feature = "img")] -use image::ImageFormat; -use iced::widget::column; use iced::{Alignment, Element, Length, Settings, Task, window::Settings as Window}; use iced::{Subscription, event, keyboard}; +#[cfg(feature = "img")] +use image::ImageFormat; use log::{debug, error, info}; #[cfg(feature = "self-update")] use std::path::PathBuf; @@ -434,7 +434,7 @@ impl UadGui { pub fn start() -> iced::Result { #[cfg(feature = "img")] let logo: &[u8] = match *OS_COLOR_SCHEME { - // remember to keep `Unspecified` in sync with `src/core/theme` + // remember to keep `Unspecified` in sync with `src/theme` dark_light::Mode::Dark | dark_light::Mode::Unspecified => { include_bytes!("../../../resources/assets/logo-dark.png") } diff --git a/crates/uad-gui/src/theme.rs b/crates/uad-gui/src/theme.rs index 3c4c263..e18a428 100644 --- a/crates/uad-gui/src/theme.rs +++ b/crates/uad-gui/src/theme.rs @@ -1,72 +1,219 @@ use iced::theme::{self, Mode, Palette, Style}; +use iced::{Color, color}; +use std::sync::LazyLock; -pub use uad_core::theme::{BaseColors, BrightColors, ColorPalette, NormalColors, OS_COLOR_SCHEME}; +/* +In-memory caching. +This fixes the perf bug +caused by Iced repeatedly calling `palette`. -/// GUI-local wrapper around the core Theme to satisfy orphan rules for -/// iced's Catalog traits. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Theme(pub uad_core::theme::Theme); +Coincidentally, this also ensures consistent colors across the GUI, +at the cost of requiring a restart to update the palette. +(this is just a patch, not a fix) +*/ +pub static OS_COLOR_SCHEME: LazyLock = + LazyLock::new(|| dark_light::detect().unwrap_or(dark_light::Mode::Unspecified)); + +#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] +/// Color scheme +pub enum Theme { + #[default] + /// `Dark` or `Light`, according to `dark_light` + Auto, + /// `Dark`-ish and purple + Lupin, + /// white on black + Dark, + /// black on white + Light, +} + +#[derive(Debug, Clone, Copy)] +pub struct BaseColors { + pub background: Color, + pub foreground: Color, +} + +#[derive(Debug, Clone, Copy)] +pub struct NormalColors { + pub primary: Color, + #[allow(dead_code, reason = "Reserved for future palette updates")] + pub secondary: Color, + pub surface: Color, + pub error: Color, +} + +#[derive(Debug, Clone, Copy)] +pub struct BrightColors { + pub primary: Color, + pub secondary: Color, + pub surface: Color, + pub error: Color, +} + +#[derive(Debug, Clone, Copy)] +pub struct ColorPalette { + pub base: BaseColors, + pub normal: NormalColors, + pub bright: BrightColors, +} -#[allow( - non_upper_case_globals, - reason = "Keep variant-like names matching core Theme" -)] impl Theme { - pub const Auto: Self = Self(uad_core::theme::Theme::Auto); - pub const Lupin: Self = Self(uad_core::theme::Theme::Lupin); - pub const Dark: Self = Self(uad_core::theme::Theme::Dark); - pub const Light: Self = Self(uad_core::theme::Theme::Light); - pub const ALL: [Self; 4] = [Self::Auto, Self::Lupin, Self::Dark, Self::Light]; + #[allow( + clippy::unreadable_literal, + reason = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/578#discussion_r1759653408" + )] + /// This `fn` _could_ be `const`, + /// but `deref`ing a lazy-`static` is non-`const`. #[must_use] pub fn palette(self) -> ColorPalette { - self.0.palette() + const DARK: ColorPalette = ColorPalette { + base: BaseColors { + background: color!(0x111111), + foreground: color!(0x1C1C1C), + }, + normal: NormalColors { + primary: color!(0x5E4266), + secondary: color!(0x386E50), + surface: color!(0x828282), + error: color!(0x992B2B), + }, + bright: BrightColors { + primary: color!(0xBA84FC), + secondary: color!(0x49EB7A), + surface: color!(0xE0E0E0), + error: color!(0xC13047), + }, + }; + const LIGHT: ColorPalette = ColorPalette { + base: BaseColors { + background: color!(0xEEEEEE), + foreground: color!(0xE0E0E0), + }, + normal: NormalColors { + primary: color!(0x818181), + secondary: color!(0xF9D659), + surface: color!(0x818181), + error: color!(0x992B2B), + }, + bright: BrightColors { + primary: color!(0x673AB7), + secondary: color!(0x3797A4), + surface: color!(0x000000), + error: color!(0xC13047), + }, + }; + const LUPIN: ColorPalette = ColorPalette { + base: BaseColors { + background: color!(0x282A36), + foreground: color!(0x353746), + }, + normal: NormalColors { + primary: color!(0x58406F), + secondary: color!(0x386E50), + surface: color!(0xA2A4A3), + error: color!(0xA13034), + }, + bright: BrightColors { + primary: color!(0xBD94F9), + secondary: color!(0x49EB7A), + surface: color!(0xF4F8F3), + error: color!(0xE63E6D), + }, + }; + match self { + Self::Dark => DARK, + Self::Light => LIGHT, + Self::Lupin => LUPIN, + Self::Auto => match *OS_COLOR_SCHEME { + dark_light::Mode::Light => LIGHT, + dark_light::Mode::Dark | dark_light::Mode::Unspecified => DARK, + }, + } } } -impl From for Theme { - fn from(value: uad_core::theme::Theme) -> Self { - Self(value) - } -} - -impl From for uad_core::theme::Theme { - fn from(value: Theme) -> Self { - value.0 +impl std::fmt::Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Dark => "Dark", + Self::Light => "Light", + Self::Lupin => "Lupin", + Self::Auto => "Auto (follow system theme)", + } + ) } } /// Converts a string to the GUI's Theme type #[must_use] pub fn string_to_theme(theme: &str) -> Theme { - Theme(uad_core::theme::string_to_theme(theme)) + match theme { + "Lupin" => Theme::Lupin, + "Dark" => Theme::Dark, + "Light" => Theme::Light, + _ => Theme::Auto, + } } impl theme::Base for Theme { fn default(preference: Mode) -> Self { - Self(::default(preference)) + match preference { + Mode::Light => Self::Light, + Mode::Dark => Self::Dark, + Mode::None => match *OS_COLOR_SCHEME { + dark_light::Mode::Light => Self::Light, + dark_light::Mode::Dark | dark_light::Mode::Unspecified => Self::Dark, + }, + } } fn mode(&self) -> Mode { - ::mode(&self.0) + let resolved = match self { + Self::Auto => match *OS_COLOR_SCHEME { + dark_light::Mode::Light => Self::Light, + dark_light::Mode::Dark | dark_light::Mode::Unspecified => Self::Dark, + }, + other => *other, + }; + + match resolved { + Self::Light => Mode::Light, + _ => Mode::Dark, + } } fn base(&self) -> Style { - ::base(&self.0) + let p = Self::palette(*self); + Style { + background_color: p.base.background, + text_color: p.bright.surface, + } } fn palette(&self) -> Option { - ::palette(&self.0) + let p = Self::palette(*self); + Some(Palette { + background: p.base.background, + text: p.bright.surface, + primary: p.normal.primary, + success: p.bright.secondary, + warning: p.normal.surface, + danger: p.normal.error, + }) } fn name(&self) -> &str { - ::name(&self.0) - } -} - -impl std::fmt::Display for Theme { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) + match self { + Self::Dark => "Dark", + Self::Light => "Light", + Self::Lupin => "Lupin", + Self::Auto => "Auto", + } } } diff --git a/flake.lock b/flake.lock index 4822d9d..b3b5542 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1765270179, - "narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=", + "lastModified": 1776949667, + "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9", + "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index b468be1..c86de56 100644 --- a/flake.nix +++ b/flake.nix @@ -27,10 +27,10 @@ with pkgs; [ libglvnd - xorg.libX11 - xorg.libXcursor - xorg.libXi - xorg.libXrandr + libX11 + libXcursor + libXi + libXrandr libxkbcommon wayland ]