Compare commits
8 Commits
9b6c1d8411
...
11ada8521e
| Author | SHA1 | Date |
|---|---|---|
|
|
11ada8521e | 2 weeks ago |
|
|
9d88badac2 | 2 weeks ago |
|
|
4683064637 | 2 weeks ago |
|
|
7d9acb493a | 3 years ago |
|
|
136cd0a292 | 3 years ago |
|
|
ec8dbc5a91 | 3 years ago |
|
|
9c609ae28b | 3 years ago |
|
|
086cd2b934 | 3 years ago |
File diff suppressed because it is too large
Load Diff
@ -1,50 +1,238 @@
|
|||||||
use crate::{process_utils, system_info, InstallError};
|
use crate::{system_info, InstallError};
|
||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
/// Apply registry contents by writing a .reg file and importing with reg.exe.
|
/// Apply registry entries using native Windows registry APIs.
|
||||||
|
///
|
||||||
|
/// The entries are in .reg file format (without the header line).
|
||||||
|
/// Supported syntax:
|
||||||
|
/// - `[HKEY_xxx\path]` — create/open key
|
||||||
|
/// - `[-HKEY_xxx\path]` — delete entire key tree
|
||||||
|
/// - `"name"="string_value"` — set REG_SZ value
|
||||||
|
/// - `"name"=dword:XXXXXXXX` — set REG_DWORD value
|
||||||
|
/// - `"name"=-` — delete a value
|
||||||
|
/// - `@="string_value"` — set the default (unnamed) value
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub fn apply_registry(entries: &[&str]) -> Result<(), InstallError> {
|
pub fn apply_registry(entries: &[&str]) -> Result<(), InstallError> {
|
||||||
let combined = entries.join("\n\n");
|
let combined = entries.join("\n\n");
|
||||||
let filled = system_info::fill_string(&combined);
|
let filled = system_info::fill_string(&combined);
|
||||||
|
apply_reg_content(&filled)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn apply_reg_content(content: &str) -> Result<(), InstallError> {
|
||||||
|
use windows_sys::Win32::System::Registry::{
|
||||||
|
RegCloseKey, RegCreateKeyExW, RegDeleteValueW, RegSetValueExW,
|
||||||
|
KEY_WOW64_64KEY, KEY_WRITE, REG_DWORD, REG_OPTION_NON_VOLATILE, REG_SZ,
|
||||||
|
};
|
||||||
|
|
||||||
let content = format!("Windows Registry Editor Version 5.00\n\n{}", filled);
|
let mut current_hkey: Option<windows_sys::Win32::System::Registry::HKEY> = None;
|
||||||
|
|
||||||
let temp_dir = std::env::temp_dir();
|
for line in content.lines() {
|
||||||
// Include the process ID to avoid collisions when two instances run concurrently,
|
let line = line.trim();
|
||||||
// matching the unique-file guarantee of C#'s Path.GetTempFileName().
|
if line.is_empty() {
|
||||||
let reg_file = temp_dir.join(format!("cleanflash_reg_{}.tmp", std::process::id()));
|
continue;
|
||||||
|
}
|
||||||
// Write as UTF-16LE with BOM (Windows .reg format).
|
|
||||||
{
|
// Delete key: [-HKEY_xxx\path]
|
||||||
let mut f = fs::File::create(®_file)
|
if line.starts_with("[-") && line.ends_with(']') {
|
||||||
.map_err(|e| InstallError::new(format!("Failed to create temp reg file: {}", e)))?;
|
if let Some(hk) = current_hkey.take() {
|
||||||
let utf16: Vec<u16> = content.encode_utf16().collect();
|
unsafe { RegCloseKey(hk) };
|
||||||
// BOM
|
}
|
||||||
f.write_all(&[0xFF, 0xFE])
|
|
||||||
.map_err(|e| InstallError::new(format!("Failed to write BOM: {}", e)))?;
|
let key_path = &line[2..line.len() - 1];
|
||||||
for word in &utf16 {
|
if let Some((root, subkey)) = parse_root_and_subkey(key_path) {
|
||||||
f.write_all(&word.to_le_bytes())
|
delete_key_tree(root, subkey);
|
||||||
.map_err(|e| InstallError::new(format!("Failed to write reg data: {}", e)))?;
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open/create key: [HKEY_xxx\path]
|
||||||
|
if line.starts_with('[') && line.ends_with(']') {
|
||||||
|
if let Some(hk) = current_hkey.take() {
|
||||||
|
unsafe { RegCloseKey(hk) };
|
||||||
|
}
|
||||||
|
|
||||||
|
let key_path = &line[1..line.len() - 1];
|
||||||
|
if let Some((root, subkey)) = parse_root_and_subkey(key_path) {
|
||||||
|
let subkey_wide: Vec<u16> =
|
||||||
|
subkey.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
unsafe {
|
||||||
|
let mut hkey = std::ptr::null_mut();
|
||||||
|
let mut disposition: u32 = 0;
|
||||||
|
let result = RegCreateKeyExW(
|
||||||
|
root,
|
||||||
|
subkey_wide.as_ptr(),
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
REG_OPTION_NON_VOLATILE,
|
||||||
|
KEY_WRITE | KEY_WOW64_64KEY,
|
||||||
|
std::ptr::null(),
|
||||||
|
&mut hkey,
|
||||||
|
&mut disposition,
|
||||||
|
);
|
||||||
|
if result == 0 {
|
||||||
|
current_hkey = Some(hkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let reg_filename = reg_file.to_string_lossy().to_string();
|
// Value operations require an open key.
|
||||||
let result = process_utils::run_process("reg.exe", &["import", ®_filename]);
|
let Some(hkey) = current_hkey else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default value: @="value"
|
||||||
|
if let Some(rest) = line.strip_prefix("@=") {
|
||||||
|
if let Some(val) = parse_reg_string(rest) {
|
||||||
|
let val_wide: Vec<u16> =
|
||||||
|
val.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
unsafe {
|
||||||
|
RegSetValueExW(
|
||||||
|
hkey,
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
REG_SZ as u32,
|
||||||
|
val_wide.as_ptr() as *const u8,
|
||||||
|
(val_wide.len() * 2) as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Named value: "name"=...
|
||||||
|
if !line.starts_with('"') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((name, value_part)) = parse_name_and_value(line) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let name_wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
|
||||||
|
// Delete value: "name"=-
|
||||||
|
if value_part == "-" {
|
||||||
|
unsafe {
|
||||||
|
RegDeleteValueW(hkey, name_wide.as_ptr());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DWORD value: "name"=dword:XXXXXXXX
|
||||||
|
if let Some(hex_str) = value_part.strip_prefix("dword:") {
|
||||||
|
if let Ok(dword_val) = u32::from_str_radix(hex_str, 16) {
|
||||||
|
unsafe {
|
||||||
|
RegSetValueExW(
|
||||||
|
hkey,
|
||||||
|
name_wide.as_ptr(),
|
||||||
|
0,
|
||||||
|
REG_DWORD as u32,
|
||||||
|
&dword_val as *const u32 as *const u8,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let _ = fs::remove_file(®_file);
|
// String value: "name"="value"
|
||||||
|
if let Some(val) = parse_reg_string(value_part) {
|
||||||
|
let val_wide: Vec<u16> = val.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
unsafe {
|
||||||
|
RegSetValueExW(
|
||||||
|
hkey,
|
||||||
|
name_wide.as_ptr(),
|
||||||
|
0,
|
||||||
|
REG_SZ as u32,
|
||||||
|
val_wide.as_ptr() as *const u8,
|
||||||
|
(val_wide.len() * 2) as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !result.is_successful() {
|
if let Some(hk) = current_hkey {
|
||||||
return Err(InstallError::new(format!(
|
unsafe { RegCloseKey(hk) };
|
||||||
"Failed to apply changes to registry: error code {}\n\n{}",
|
|
||||||
result.exit_code, result.output
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a root key name and subkey path from a combined string like
|
||||||
|
/// `HKEY_LOCAL_MACHINE\Software\Something`.
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn parse_root_and_subkey(
|
||||||
|
path: &str,
|
||||||
|
) -> Option<(windows_sys::Win32::System::Registry::HKEY, &str)> {
|
||||||
|
use windows_sys::Win32::System::Registry::{
|
||||||
|
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (root_name, subkey) = path.split_once('\\')?;
|
||||||
|
let root = match root_name {
|
||||||
|
"HKEY_LOCAL_MACHINE" => HKEY_LOCAL_MACHINE,
|
||||||
|
"HKEY_CURRENT_USER" => HKEY_CURRENT_USER,
|
||||||
|
"HKEY_CLASSES_ROOT" => HKEY_CLASSES_ROOT,
|
||||||
|
"HKEY_USERS" => HKEY_USERS,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some((root, subkey))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete an entire registry key tree.
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn delete_key_tree(root: windows_sys::Win32::System::Registry::HKEY, subkey: &str) {
|
||||||
|
use windows_sys::Win32::System::Registry::{
|
||||||
|
RegCloseKey, RegDeleteTreeW, RegOpenKeyExW, KEY_WOW64_64KEY, KEY_WRITE,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((parent, leaf)) = subkey.rsplit_once('\\') {
|
||||||
|
let parent_wide: Vec<u16> = parent.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
let leaf_wide: Vec<u16> = leaf.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
unsafe {
|
||||||
|
let mut hkey = std::ptr::null_mut();
|
||||||
|
let result =
|
||||||
|
RegOpenKeyExW(root, parent_wide.as_ptr(), 0, KEY_WRITE | KEY_WOW64_64KEY, &mut hkey);
|
||||||
|
if result == 0 {
|
||||||
|
RegDeleteTreeW(hkey, leaf_wide.as_ptr());
|
||||||
|
RegCloseKey(hkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let subkey_wide: Vec<u16> = subkey.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
unsafe {
|
||||||
|
RegDeleteTreeW(root, subkey_wide.as_ptr());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a quoted string value: `"some value"` → `some value`.
|
||||||
|
/// Handles escaped backslashes (`\\` → `\`) inside the string.
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn parse_reg_string(s: &str) -> Option<String> {
|
||||||
|
let s = s.trim();
|
||||||
|
if !s.starts_with('"') || !s.ends_with('"') || s.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let inner = &s[1..s.len() - 1];
|
||||||
|
Some(inner.replace("\\\\", "\\"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse `"name"=value` into `(name, value)`.
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn parse_name_and_value(line: &str) -> Option<(String, &str)> {
|
||||||
|
let rest = &line[1..]; // skip opening "
|
||||||
|
let end_quote = rest.find('"')?;
|
||||||
|
let name = &rest[..end_quote];
|
||||||
|
let after_name = &rest[end_quote + 1..]; // skip closing "
|
||||||
|
let value_part = after_name.strip_prefix('=')?;
|
||||||
|
Some((name.to_string(), value_part))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
pub fn apply_registry(_entries: &[&str]) -> Result<(), InstallError> {
|
pub fn apply_registry(_entries: &[&str]) -> Result<(), InstallError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,70 @@
|
|||||||
|
use argh::FromArgs;
|
||||||
|
|
||||||
|
/// Clean Flash Player Installer
|
||||||
|
///
|
||||||
|
/// Install Clean Flash Player with GUI, TUI, or silent mode.
|
||||||
|
#[derive(FromArgs)]
|
||||||
|
pub struct InstallerArgs {
|
||||||
|
/// use the terminal UI (ratatui) instead of the graphical window
|
||||||
|
#[argh(switch)]
|
||||||
|
pub cli: bool,
|
||||||
|
|
||||||
|
/// run the installation non-interactively (no GUI or TUI)
|
||||||
|
#[argh(switch)]
|
||||||
|
pub install: bool,
|
||||||
|
|
||||||
|
/// suppress all output (only valid with --install)
|
||||||
|
#[argh(switch)]
|
||||||
|
pub silent: bool,
|
||||||
|
|
||||||
|
/// install the Modern Browsers (MV3) native host
|
||||||
|
#[argh(switch)]
|
||||||
|
pub native_host: bool,
|
||||||
|
|
||||||
|
/// install the Pepper API (PPAPI) plugin
|
||||||
|
#[argh(switch)]
|
||||||
|
pub pepper: bool,
|
||||||
|
|
||||||
|
/// install the Netscape API (NPAPI) plugin
|
||||||
|
#[argh(switch)]
|
||||||
|
pub netscape: bool,
|
||||||
|
|
||||||
|
/// install the ActiveX (OCX) plugin
|
||||||
|
#[argh(switch)]
|
||||||
|
pub activex: bool,
|
||||||
|
|
||||||
|
/// install the standalone Flash Player
|
||||||
|
#[argh(switch)]
|
||||||
|
pub player: bool,
|
||||||
|
|
||||||
|
/// create desktop shortcuts for standalone player (requires --player)
|
||||||
|
#[argh(switch)]
|
||||||
|
pub player_desktop: bool,
|
||||||
|
|
||||||
|
/// create start menu shortcuts for standalone player (requires --player)
|
||||||
|
#[argh(switch)]
|
||||||
|
pub player_start_menu: bool,
|
||||||
|
|
||||||
|
/// install the debug version
|
||||||
|
#[argh(switch)]
|
||||||
|
pub debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mode in which the application should run.
|
||||||
|
pub enum RunMode {
|
||||||
|
Gui,
|
||||||
|
Tui,
|
||||||
|
Silent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstallerArgs {
|
||||||
|
pub fn determine_mode(&self) -> RunMode {
|
||||||
|
if self.install {
|
||||||
|
RunMode::Silent
|
||||||
|
} else if self.cli {
|
||||||
|
RunMode::Tui
|
||||||
|
} else {
|
||||||
|
RunMode::Gui
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
use crate::cli_args::InstallerArgs;
|
||||||
|
use crate::install_flags::{self, InstallFlags};
|
||||||
|
use crate::installer;
|
||||||
|
use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError};
|
||||||
|
|
||||||
|
struct SilentProgressCallback {
|
||||||
|
silent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressCallback for SilentProgressCallback {
|
||||||
|
fn update_progress_label(&self, text: &str, _tick: bool) {
|
||||||
|
if !self.silent {
|
||||||
|
eprintln!("[*] {}", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick_progress(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_silent_install(args: &InstallerArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let version = update_checker::FLASH_VERSION;
|
||||||
|
|
||||||
|
let mut flags = InstallFlags::new();
|
||||||
|
flags.set_conditionally(args.native_host, install_flags::NATIVE_HOST);
|
||||||
|
flags.set_conditionally(args.pepper, install_flags::PEPPER);
|
||||||
|
flags.set_conditionally(args.netscape, install_flags::NETSCAPE);
|
||||||
|
flags.set_conditionally(args.activex, install_flags::ACTIVEX);
|
||||||
|
flags.set_conditionally(args.player, install_flags::PLAYER);
|
||||||
|
flags.set_conditionally(args.player_desktop, install_flags::PLAYER_DESKTOP);
|
||||||
|
flags.set_conditionally(args.player_start_menu, install_flags::PLAYER_START_MENU);
|
||||||
|
flags.set_conditionally(args.debug, install_flags::DEBUG);
|
||||||
|
|
||||||
|
if flags.is_none_set() {
|
||||||
|
if !args.silent {
|
||||||
|
eprintln!("No components selected. Use flags to select what to install:");
|
||||||
|
eprintln!(" --native-host Modern Browsers (MV3)");
|
||||||
|
eprintln!(" --pepper Pepper API (PPAPI)");
|
||||||
|
eprintln!(" --netscape Netscape API (NPAPI)");
|
||||||
|
eprintln!(" --activex ActiveX (OCX)");
|
||||||
|
eprintln!(" --player Standalone Flash Player");
|
||||||
|
eprintln!(" --player-desktop Desktop shortcuts (requires --player)");
|
||||||
|
eprintln!(" --player-start-menu Start Menu shortcuts (requires --player)");
|
||||||
|
eprintln!(" --debug Debug version");
|
||||||
|
eprintln!("\nExample: clean_flash_installer --install --native-host --pepper");
|
||||||
|
}
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !args.silent {
|
||||||
|
eprintln!("Clean Flash Player {} - Installation", version);
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback = SilentProgressCallback {
|
||||||
|
silent: args.silent,
|
||||||
|
};
|
||||||
|
|
||||||
|
let redir = redirection::disable_redirection();
|
||||||
|
|
||||||
|
let result = (|| -> Result<(), InstallError> {
|
||||||
|
uninstaller::uninstall(&callback)?;
|
||||||
|
installer::install(&callback, &mut flags)?;
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
|
||||||
|
redirection::enable_redirection(redir);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
if !args.silent {
|
||||||
|
eprintln!("[+] Installation completed successfully.");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if !args.silent {
|
||||||
|
eprintln!("[!] Installation failed: {}", e);
|
||||||
|
}
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,639 @@
|
|||||||
|
use crate::install_flags::{self, InstallFlags};
|
||||||
|
use crate::installer;
|
||||||
|
use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError};
|
||||||
|
use crossterm::{
|
||||||
|
event::{self, Event, KeyCode, KeyEventKind},
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
execute,
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
prelude::*,
|
||||||
|
widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Wrap},
|
||||||
|
};
|
||||||
|
use std::io::{self, stdout};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
const FLASH_VERSION: &str = update_checker::FLASH_VERSION;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Panel {
|
||||||
|
Disclaimer,
|
||||||
|
Choice,
|
||||||
|
PlayerChoice,
|
||||||
|
DebugChoice,
|
||||||
|
BeforeInstall,
|
||||||
|
Install,
|
||||||
|
Complete,
|
||||||
|
Failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProgressState {
|
||||||
|
label: String,
|
||||||
|
value: u32,
|
||||||
|
maximum: u32,
|
||||||
|
done: bool,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TuiInstallState {
|
||||||
|
panel: Panel,
|
||||||
|
// Disclaimer
|
||||||
|
disclaimer_accepted: bool,
|
||||||
|
// Choice
|
||||||
|
native_host: bool,
|
||||||
|
pepper: bool,
|
||||||
|
netscape: bool,
|
||||||
|
activex: bool,
|
||||||
|
choice_cursor: usize,
|
||||||
|
// Player choice
|
||||||
|
player: bool,
|
||||||
|
player_desktop: bool,
|
||||||
|
player_start_menu: bool,
|
||||||
|
player_cursor: usize,
|
||||||
|
// Debug
|
||||||
|
debug_chosen: bool,
|
||||||
|
// Progress
|
||||||
|
progress: Arc<Mutex<ProgressState>>,
|
||||||
|
// Failure
|
||||||
|
failure_detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TuiInstallState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
panel: Panel::Disclaimer,
|
||||||
|
disclaimer_accepted: false,
|
||||||
|
native_host: true,
|
||||||
|
pepper: false,
|
||||||
|
netscape: false,
|
||||||
|
activex: false,
|
||||||
|
choice_cursor: 0,
|
||||||
|
player: false,
|
||||||
|
player_desktop: false,
|
||||||
|
player_start_menu: false,
|
||||||
|
player_cursor: 0,
|
||||||
|
debug_chosen: false,
|
||||||
|
progress: Arc::new(Mutex::new(ProgressState {
|
||||||
|
label: "Preparing...".into(),
|
||||||
|
value: 0,
|
||||||
|
maximum: 10,
|
||||||
|
done: false,
|
||||||
|
error: None,
|
||||||
|
})),
|
||||||
|
failure_detail: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choice_items(&self) -> Vec<(&str, bool, bool)> {
|
||||||
|
let is_windows = cfg!(target_os = "windows");
|
||||||
|
vec![
|
||||||
|
("Modern Browsers (MV3) - Chrome/Firefox/Edge", self.native_host, true),
|
||||||
|
("Pepper API (PPAPI) - Chrome/Opera/Brave", self.pepper, is_windows),
|
||||||
|
("Netscape API (NPAPI) - Firefox/ESR/Waterfox", self.netscape, is_windows),
|
||||||
|
("ActiveX (OCX) - IE/Embedded/Desktop", self.activex, is_windows),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn player_items(&self) -> Vec<(&str, bool, bool)> {
|
||||||
|
vec![
|
||||||
|
("Install Standalone Flash Player", self.player, true),
|
||||||
|
("Create Desktop Shortcuts", self.player_desktop, self.player),
|
||||||
|
("Create Start Menu Shortcuts", self.player_start_menu, self.player),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_choice(&mut self) {
|
||||||
|
let is_windows = cfg!(target_os = "windows");
|
||||||
|
match self.choice_cursor {
|
||||||
|
0 => self.native_host = !self.native_host,
|
||||||
|
1 if is_windows => self.pepper = !self.pepper,
|
||||||
|
2 if is_windows => self.netscape = !self.netscape,
|
||||||
|
3 if is_windows => self.activex = !self.activex,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_player(&mut self) {
|
||||||
|
match self.player_cursor {
|
||||||
|
0 => {
|
||||||
|
self.player = !self.player;
|
||||||
|
if !self.player {
|
||||||
|
self.player_desktop = false;
|
||||||
|
self.player_start_menu = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 if self.player => self.player_desktop = !self.player_desktop,
|
||||||
|
2 if self.player => self.player_start_menu = !self.player_start_menu,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_flags(&self) -> InstallFlags {
|
||||||
|
let mut flags = InstallFlags::new();
|
||||||
|
flags.set_conditionally(self.native_host, install_flags::NATIVE_HOST);
|
||||||
|
flags.set_conditionally(self.pepper, install_flags::PEPPER);
|
||||||
|
flags.set_conditionally(self.netscape, install_flags::NETSCAPE);
|
||||||
|
flags.set_conditionally(self.activex, install_flags::ACTIVEX);
|
||||||
|
flags.set_conditionally(self.player, install_flags::PLAYER);
|
||||||
|
flags.set_conditionally(self.player_desktop, install_flags::PLAYER_DESKTOP);
|
||||||
|
flags.set_conditionally(self.player_start_menu, install_flags::PLAYER_START_MENU);
|
||||||
|
flags.set_conditionally(self.debug_chosen, install_flags::DEBUG);
|
||||||
|
flags
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_install(&mut self) {
|
||||||
|
self.panel = Panel::Install;
|
||||||
|
let mut flags = self.build_flags();
|
||||||
|
let max_ticks = flags.get_ticks();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = self.progress.lock().unwrap();
|
||||||
|
state.label = "Preparing...".into();
|
||||||
|
state.value = 0;
|
||||||
|
state.maximum = max_ticks;
|
||||||
|
state.done = false;
|
||||||
|
state.error = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = Arc::clone(&self.progress);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let callback = ThreadProgressCallback {
|
||||||
|
state: Arc::clone(&progress),
|
||||||
|
};
|
||||||
|
|
||||||
|
let redir = redirection::disable_redirection();
|
||||||
|
let result = (|| -> Result<(), InstallError> {
|
||||||
|
uninstaller::uninstall(&callback)?;
|
||||||
|
installer::install(&callback, &mut flags)?;
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
redirection::enable_redirection(redir);
|
||||||
|
|
||||||
|
let mut state = progress.lock().unwrap();
|
||||||
|
if let Err(e) = result {
|
||||||
|
state.error = Some(e.to_string());
|
||||||
|
}
|
||||||
|
state.done = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summary_text(&self) -> String {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if self.native_host { parts.push("Modern Browsers (via extension)"); }
|
||||||
|
if self.pepper { parts.push("Google Chrome"); }
|
||||||
|
if self.netscape { parts.push("Mozilla Firefox"); }
|
||||||
|
if self.activex { parts.push("Internet Explorer"); }
|
||||||
|
if self.player { parts.push("Standalone Player"); }
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
"You are about to uninstall Clean Flash Player.\n\
|
||||||
|
The installer will completely remove all versions of Flash Player."
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"You are about to install Clean Flash Player for:\n {}\n\n\
|
||||||
|
The installer will close browsers running Flash, uninstall previous versions, and install.",
|
||||||
|
parts.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThreadProgressCallback {
|
||||||
|
state: Arc<Mutex<ProgressState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressCallback for ThreadProgressCallback {
|
||||||
|
fn update_progress_label(&self, text: &str, tick: bool) {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.label = text.to_string();
|
||||||
|
if tick {
|
||||||
|
state.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick_progress(&self) {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_tui_installer() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
let result = run_app(&mut terminal);
|
||||||
|
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut state = TuiInstallState::new();
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
state.pepper = false;
|
||||||
|
state.netscape = false;
|
||||||
|
state.activex = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| draw_ui(f, &state))?;
|
||||||
|
|
||||||
|
// While installing, poll both events (with timeout) and progress
|
||||||
|
if state.panel == Panel::Install {
|
||||||
|
// Poll progress
|
||||||
|
{
|
||||||
|
let ps = state.progress.lock().unwrap();
|
||||||
|
if ps.done {
|
||||||
|
if let Some(ref err) = ps.error {
|
||||||
|
state.failure_detail = err.clone();
|
||||||
|
drop(ps);
|
||||||
|
state.panel = Panel::Failure;
|
||||||
|
} else {
|
||||||
|
drop(ps);
|
||||||
|
state.panel = Panel::Complete;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event::poll(std::time::Duration::from_millis(100))? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind == KeyEventKind::Press && (key.code == KeyCode::Char('q') || key.code == KeyCode::Esc) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind != KeyEventKind::Press {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => {
|
||||||
|
match state.panel {
|
||||||
|
Panel::Install => {} // can't quit during install
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
match state.panel {
|
||||||
|
Panel::Disclaimer => {
|
||||||
|
if state.disclaimer_accepted {
|
||||||
|
state.panel = Panel::Choice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Panel::Choice => {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
state.panel = Panel::PlayerChoice;
|
||||||
|
} else {
|
||||||
|
state.panel = Panel::BeforeInstall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Panel::PlayerChoice => state.panel = Panel::DebugChoice,
|
||||||
|
Panel::DebugChoice => state.panel = Panel::BeforeInstall,
|
||||||
|
Panel::BeforeInstall => state.start_install(),
|
||||||
|
Panel::Complete => break,
|
||||||
|
Panel::Failure => state.start_install(), // retry
|
||||||
|
Panel::Install => {} // handled by progress polling above
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
match state.panel {
|
||||||
|
Panel::Choice => state.panel = Panel::Disclaimer,
|
||||||
|
Panel::PlayerChoice => state.panel = Panel::Choice,
|
||||||
|
Panel::DebugChoice => state.panel = Panel::PlayerChoice,
|
||||||
|
Panel::BeforeInstall => {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
state.panel = Panel::DebugChoice;
|
||||||
|
} else {
|
||||||
|
state.panel = Panel::Choice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char(' ') => {
|
||||||
|
match state.panel {
|
||||||
|
Panel::Disclaimer => {
|
||||||
|
state.disclaimer_accepted = !state.disclaimer_accepted;
|
||||||
|
}
|
||||||
|
Panel::Choice => state.toggle_choice(),
|
||||||
|
Panel::PlayerChoice => state.toggle_player(),
|
||||||
|
Panel::DebugChoice => {
|
||||||
|
state.debug_chosen = !state.debug_chosen;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
match state.panel {
|
||||||
|
Panel::Choice => {
|
||||||
|
if state.choice_cursor > 0 {
|
||||||
|
state.choice_cursor -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Panel::PlayerChoice => {
|
||||||
|
if state.player_cursor > 0 {
|
||||||
|
state.player_cursor -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
match state.panel {
|
||||||
|
Panel::Choice => {
|
||||||
|
let max = state.choice_items().len().saturating_sub(1);
|
||||||
|
if state.choice_cursor < max {
|
||||||
|
state.choice_cursor += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Panel::PlayerChoice => {
|
||||||
|
let max = state.player_items().len().saturating_sub(1);
|
||||||
|
if state.player_cursor < max {
|
||||||
|
state.player_cursor += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_ui(f: &mut Frame, state: &TuiInstallState) {
|
||||||
|
let area = f.area();
|
||||||
|
|
||||||
|
// Layout: title area, main panel, footer
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3), // title
|
||||||
|
Constraint::Min(5), // content
|
||||||
|
Constraint::Length(3), // footer
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
// Title bar
|
||||||
|
let title = format!(
|
||||||
|
" Clean Flash Player {} Installer ",
|
||||||
|
FLASH_VERSION
|
||||||
|
);
|
||||||
|
let title_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Cyan))
|
||||||
|
.title(title)
|
||||||
|
.title_alignment(Alignment::Center);
|
||||||
|
f.render_widget(title_block, chunks[0]);
|
||||||
|
|
||||||
|
// Content
|
||||||
|
let content_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::DarkGray));
|
||||||
|
|
||||||
|
let inner = content_block.inner(chunks[1]);
|
||||||
|
f.render_widget(content_block, chunks[1]);
|
||||||
|
|
||||||
|
match state.panel {
|
||||||
|
Panel::Disclaimer => draw_disclaimer(f, inner, state),
|
||||||
|
Panel::Choice => draw_choice(f, inner, state),
|
||||||
|
Panel::PlayerChoice => draw_player_choice(f, inner, state),
|
||||||
|
Panel::DebugChoice => draw_debug_choice(f, inner, state),
|
||||||
|
Panel::BeforeInstall => draw_before_install(f, inner, state),
|
||||||
|
Panel::Install => draw_install(f, inner, state),
|
||||||
|
Panel::Complete => draw_complete(f, inner, state),
|
||||||
|
Panel::Failure => draw_failure(f, inner, state),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
let footer_text = match state.panel {
|
||||||
|
Panel::Disclaimer => "[Space] Toggle accept [Enter] Next [q/Esc] Quit",
|
||||||
|
Panel::Choice => "[↑↓] Move [Space] Toggle [Enter] Next [Backspace] Back [q] Quit",
|
||||||
|
Panel::PlayerChoice => "[↑↓] Move [Space] Toggle [Enter] Next [Backspace] Back [q] Quit",
|
||||||
|
Panel::DebugChoice => "[Space] Toggle debug [Enter] Next [Backspace] Back [q] Quit",
|
||||||
|
Panel::BeforeInstall => "[Enter] Install [Backspace] Back [q] Quit",
|
||||||
|
Panel::Install => "Installing...",
|
||||||
|
Panel::Complete => "[Enter/q] Quit",
|
||||||
|
Panel::Failure => "[Enter] Retry [q] Quit",
|
||||||
|
};
|
||||||
|
let footer = Paragraph::new(footer_text)
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::DarkGray)));
|
||||||
|
f.render_widget(footer, chunks[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_disclaimer(f: &mut Frame, area: Rect, state: &TuiInstallState) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(3), Constraint::Length(1)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let text = "I am aware that Adobe Flash Player is no longer supported, nor provided by Adobe Inc.\n\
|
||||||
|
Clean Flash Player is a third-party version of Flash Player built from the latest Flash Player \
|
||||||
|
version with adware removed.\n\n\
|
||||||
|
Adobe is not required by any means to provide support for this version of Flash Player.";
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::White));
|
||||||
|
f.render_widget(paragraph, chunks[0]);
|
||||||
|
|
||||||
|
let checkbox = if state.disclaimer_accepted {
|
||||||
|
"[x] I accept the above disclaimer"
|
||||||
|
} else {
|
||||||
|
"[ ] I accept the above disclaimer"
|
||||||
|
};
|
||||||
|
let cb_style = if state.disclaimer_accepted {
|
||||||
|
Style::default().fg(Color::Green)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
};
|
||||||
|
let cb = Paragraph::new(checkbox).style(cb_style);
|
||||||
|
f.render_widget(cb, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_choice(f: &mut Frame, area: Rect, state: &TuiInstallState) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(2), Constraint::Min(3)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let header = Paragraph::new("Which browser plugins would you like to install?")
|
||||||
|
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD));
|
||||||
|
f.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
let items: Vec<ListItem> = state
|
||||||
|
.choice_items()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, (label, checked, enabled))| {
|
||||||
|
let marker = if *checked { "[x]" } else { "[ ]" };
|
||||||
|
let text = format!("{} {}", marker, label);
|
||||||
|
let style = if !enabled {
|
||||||
|
Style::default().fg(Color::DarkGray)
|
||||||
|
} else if i == state.choice_cursor {
|
||||||
|
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::White)
|
||||||
|
};
|
||||||
|
ListItem::new(text).style(style)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(items);
|
||||||
|
f.render_widget(list, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_player_choice(f: &mut Frame, area: Rect, state: &TuiInstallState) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(2), Constraint::Min(3)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let header = Paragraph::new("Would you like to install the standalone Flash Player?")
|
||||||
|
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD));
|
||||||
|
f.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
let items: Vec<ListItem> = state
|
||||||
|
.player_items()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, (label, checked, enabled))| {
|
||||||
|
let marker = if *checked { "[x]" } else { "[ ]" };
|
||||||
|
let text = format!("{} {}", marker, label);
|
||||||
|
let style = if !enabled {
|
||||||
|
Style::default().fg(Color::DarkGray)
|
||||||
|
} else if i == state.player_cursor {
|
||||||
|
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::White)
|
||||||
|
};
|
||||||
|
ListItem::new(text).style(style)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(items);
|
||||||
|
f.render_widget(list, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_debug_choice(f: &mut Frame, area: Rect, state: &TuiInstallState) {
|
||||||
|
let text = "Would you like to install the debug version of Clean Flash Player?\n\
|
||||||
|
You should only choose the debug version if you are planning to create Flash applications.\n\
|
||||||
|
If you are not sure, simply press Enter to continue.";
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(3), Constraint::Length(1)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::White));
|
||||||
|
f.render_widget(paragraph, chunks[0]);
|
||||||
|
|
||||||
|
let checkbox = if state.debug_chosen {
|
||||||
|
"[x] Install debug version"
|
||||||
|
} else {
|
||||||
|
"[ ] Install debug version"
|
||||||
|
};
|
||||||
|
let cb_style = if state.debug_chosen {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::White)
|
||||||
|
};
|
||||||
|
let cb = Paragraph::new(checkbox).style(cb_style);
|
||||||
|
f.render_widget(cb, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_before_install(f: &mut Frame, area: Rect, state: &TuiInstallState) {
|
||||||
|
let text = state.summary_text();
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::White));
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_install(f: &mut Frame, area: Rect, state: &TuiInstallState) {
|
||||||
|
let ps = state.progress.lock().unwrap();
|
||||||
|
let label = ps.label.clone();
|
||||||
|
let ratio = if ps.maximum > 0 {
|
||||||
|
(ps.value as f64 / ps.maximum as f64).min(1.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
drop(ps);
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(2),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let header = Paragraph::new("Installation in progress...")
|
||||||
|
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD));
|
||||||
|
f.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
let status = Paragraph::new(label).style(Style::default().fg(Color::White));
|
||||||
|
f.render_widget(status, chunks[1]);
|
||||||
|
|
||||||
|
let gauge = Gauge::default()
|
||||||
|
.block(Block::default().borders(Borders::ALL))
|
||||||
|
.gauge_style(Style::default().fg(Color::Cyan).bg(Color::DarkGray))
|
||||||
|
.ratio(ratio);
|
||||||
|
f.render_widget(gauge, chunks[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_complete(f: &mut Frame, area: Rect, _state: &TuiInstallState) {
|
||||||
|
let text = "Clean Flash Player has been successfully installed!\n\n\
|
||||||
|
Don't forget, Flash Player is no longer compatible with new browsers.\n\
|
||||||
|
For browser recommendations and Flash Player updates, check out Clean Flash Player's website!";
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::Green));
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_failure(f: &mut Frame, area: Rect, state: &TuiInstallState) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(2)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let header = Paragraph::new(
|
||||||
|
"Oops! The installation process has encountered an unexpected problem.\nPress Enter to retry or q to quit.",
|
||||||
|
)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD));
|
||||||
|
f.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
let detail_text = if state.failure_detail.len() > 500 {
|
||||||
|
&state.failure_detail[..500]
|
||||||
|
} else {
|
||||||
|
&state.failure_detail
|
||||||
|
};
|
||||||
|
let detail = Paragraph::new(detail_text.to_string())
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::White));
|
||||||
|
f.render_widget(detail, chunks[1]);
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
use argh::FromArgs;
|
||||||
|
|
||||||
|
/// Clean Flash Player Uninstaller
|
||||||
|
///
|
||||||
|
/// Uninstall Clean Flash Player with GUI, TUI, or silent mode.
|
||||||
|
#[derive(FromArgs)]
|
||||||
|
pub struct UninstallerArgs {
|
||||||
|
/// use the terminal UI (ratatui) instead of the graphical window
|
||||||
|
#[argh(switch)]
|
||||||
|
pub cli: bool,
|
||||||
|
|
||||||
|
/// run the uninstallation non-interactively (no GUI or TUI)
|
||||||
|
#[argh(switch)]
|
||||||
|
pub uninstall: bool,
|
||||||
|
|
||||||
|
/// suppress all output (only valid with --uninstall)
|
||||||
|
#[argh(switch)]
|
||||||
|
pub silent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mode in which the application should run.
|
||||||
|
pub enum RunMode {
|
||||||
|
Gui,
|
||||||
|
Tui,
|
||||||
|
Silent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UninstallerArgs {
|
||||||
|
pub fn determine_mode(&self) -> RunMode {
|
||||||
|
if self.uninstall {
|
||||||
|
RunMode::Silent
|
||||||
|
} else if self.cli {
|
||||||
|
RunMode::Tui
|
||||||
|
} else {
|
||||||
|
RunMode::Gui
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
use crate::cli_args::UninstallerArgs;
|
||||||
|
use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError};
|
||||||
|
|
||||||
|
struct SilentProgressCallback {
|
||||||
|
silent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressCallback for SilentProgressCallback {
|
||||||
|
fn update_progress_label(&self, text: &str, _tick: bool) {
|
||||||
|
if !self.silent {
|
||||||
|
eprintln!("[*] {}", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick_progress(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_silent_uninstall(args: &UninstallerArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let version = update_checker::FLASH_VERSION;
|
||||||
|
|
||||||
|
if !args.silent {
|
||||||
|
eprintln!("Clean Flash Player {} - Silent Uninstall", version);
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback = SilentProgressCallback {
|
||||||
|
silent: args.silent,
|
||||||
|
};
|
||||||
|
|
||||||
|
let redir = redirection::disable_redirection();
|
||||||
|
let result: Result<(), InstallError> = uninstaller::uninstall(&callback);
|
||||||
|
redirection::enable_redirection(redir);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
if !args.silent {
|
||||||
|
eprintln!("[+] Uninstallation completed successfully.");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if !args.silent {
|
||||||
|
eprintln!("[!] Uninstallation failed: {}", e);
|
||||||
|
}
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,306 @@
|
|||||||
|
use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError};
|
||||||
|
use crossterm::{
|
||||||
|
event::{self, Event, KeyCode, KeyEventKind},
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
execute,
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
prelude::*,
|
||||||
|
widgets::{Block, Borders, Gauge, Paragraph, Wrap},
|
||||||
|
};
|
||||||
|
use std::io::{self, stdout};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
const FLASH_VERSION: &str = update_checker::FLASH_VERSION;
|
||||||
|
const UNINSTALL_TICKS: u32 = 10;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Panel {
|
||||||
|
Confirm,
|
||||||
|
Uninstall,
|
||||||
|
Complete,
|
||||||
|
Failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProgressState {
|
||||||
|
label: String,
|
||||||
|
value: u32,
|
||||||
|
maximum: u32,
|
||||||
|
done: bool,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TuiUninstallState {
|
||||||
|
panel: Panel,
|
||||||
|
progress: Arc<Mutex<ProgressState>>,
|
||||||
|
failure_detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TuiUninstallState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
panel: Panel::Confirm,
|
||||||
|
progress: Arc::new(Mutex::new(ProgressState {
|
||||||
|
label: "Preparing...".into(),
|
||||||
|
value: 0,
|
||||||
|
maximum: UNINSTALL_TICKS,
|
||||||
|
done: false,
|
||||||
|
error: None,
|
||||||
|
})),
|
||||||
|
failure_detail: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_uninstall(&mut self) {
|
||||||
|
self.panel = Panel::Uninstall;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = self.progress.lock().unwrap();
|
||||||
|
state.label = "Preparing...".into();
|
||||||
|
state.value = 0;
|
||||||
|
state.maximum = UNINSTALL_TICKS;
|
||||||
|
state.done = false;
|
||||||
|
state.error = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = Arc::clone(&self.progress);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let callback = ThreadProgressCallback {
|
||||||
|
state: Arc::clone(&progress),
|
||||||
|
};
|
||||||
|
|
||||||
|
let redir = redirection::disable_redirection();
|
||||||
|
let result: Result<(), InstallError> = uninstaller::uninstall(&callback);
|
||||||
|
redirection::enable_redirection(redir);
|
||||||
|
|
||||||
|
let mut state = progress.lock().unwrap();
|
||||||
|
if let Err(e) = result {
|
||||||
|
state.error = Some(e.to_string());
|
||||||
|
}
|
||||||
|
state.done = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThreadProgressCallback {
|
||||||
|
state: Arc<Mutex<ProgressState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressCallback for ThreadProgressCallback {
|
||||||
|
fn update_progress_label(&self, text: &str, tick: bool) {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.label = text.to_string();
|
||||||
|
if tick {
|
||||||
|
state.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick_progress(&self) {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_tui_uninstaller() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
let result = run_app(&mut terminal);
|
||||||
|
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut state = TuiUninstallState::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| draw_ui(f, &state))?;
|
||||||
|
|
||||||
|
if state.panel == Panel::Uninstall {
|
||||||
|
// Poll progress
|
||||||
|
{
|
||||||
|
let ps = state.progress.lock().unwrap();
|
||||||
|
if ps.done {
|
||||||
|
if let Some(ref err) = ps.error {
|
||||||
|
state.failure_detail = err.clone();
|
||||||
|
drop(ps);
|
||||||
|
state.panel = Panel::Failure;
|
||||||
|
} else {
|
||||||
|
drop(ps);
|
||||||
|
state.panel = Panel::Complete;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event::poll(std::time::Duration::from_millis(100))? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind == KeyEventKind::Press && (key.code == KeyCode::Char('q') || key.code == KeyCode::Esc) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind != KeyEventKind::Press {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => break,
|
||||||
|
KeyCode::Enter => {
|
||||||
|
match state.panel {
|
||||||
|
Panel::Confirm => state.start_uninstall(),
|
||||||
|
Panel::Complete => break,
|
||||||
|
Panel::Failure => state.start_uninstall(), // retry
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_ui(f: &mut Frame, state: &TuiUninstallState) {
|
||||||
|
let area = f.area();
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(5),
|
||||||
|
Constraint::Length(3),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
let title = format!(
|
||||||
|
" Clean Flash Player {} Uninstaller ",
|
||||||
|
FLASH_VERSION
|
||||||
|
);
|
||||||
|
let title_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Cyan))
|
||||||
|
.title(title)
|
||||||
|
.title_alignment(Alignment::Center);
|
||||||
|
f.render_widget(title_block, chunks[0]);
|
||||||
|
|
||||||
|
// Content
|
||||||
|
let content_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::DarkGray));
|
||||||
|
let inner = content_block.inner(chunks[1]);
|
||||||
|
f.render_widget(content_block, chunks[1]);
|
||||||
|
|
||||||
|
match state.panel {
|
||||||
|
Panel::Confirm => draw_confirm(f, inner),
|
||||||
|
Panel::Uninstall => draw_uninstall(f, inner, state),
|
||||||
|
Panel::Complete => draw_complete(f, inner),
|
||||||
|
Panel::Failure => draw_failure(f, inner, state),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
let footer_text = match state.panel {
|
||||||
|
Panel::Confirm => "[Enter] Uninstall [q/Esc] Quit",
|
||||||
|
Panel::Uninstall => "Uninstalling...",
|
||||||
|
Panel::Complete => "[Enter/q] Quit",
|
||||||
|
Panel::Failure => "[Enter] Retry [q] Quit",
|
||||||
|
};
|
||||||
|
let footer = Paragraph::new(footer_text)
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::DarkGray)));
|
||||||
|
f.render_widget(footer, chunks[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_confirm(f: &mut Frame, area: Rect) {
|
||||||
|
let text = "You are about to uninstall Clean Flash Player.\n\
|
||||||
|
Please close all browsers, including Google Chrome, Mozilla Firefox and Internet Explorer.\n\n\
|
||||||
|
The installer will completely remove all versions of Flash Player from this computer,\n\
|
||||||
|
including Clean Flash Player and older versions of Adobe Flash Player.\n\n\
|
||||||
|
Press Enter to proceed.";
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::White));
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_uninstall(f: &mut Frame, area: Rect, state: &TuiUninstallState) {
|
||||||
|
let ps = state.progress.lock().unwrap();
|
||||||
|
let label = ps.label.clone();
|
||||||
|
let ratio = if ps.maximum > 0 {
|
||||||
|
(ps.value as f64 / ps.maximum as f64).min(1.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
drop(ps);
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(2),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let header = Paragraph::new("Uninstallation in progress...")
|
||||||
|
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD));
|
||||||
|
f.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
let status = Paragraph::new(label).style(Style::default().fg(Color::White));
|
||||||
|
f.render_widget(status, chunks[1]);
|
||||||
|
|
||||||
|
let gauge = Gauge::default()
|
||||||
|
.block(Block::default().borders(Borders::ALL))
|
||||||
|
.gauge_style(Style::default().fg(Color::Cyan).bg(Color::DarkGray))
|
||||||
|
.ratio(ratio);
|
||||||
|
f.render_widget(gauge, chunks[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_complete(f: &mut Frame, area: Rect) {
|
||||||
|
let text = "All versions of Flash Player have been successfully uninstalled.\n\n\
|
||||||
|
If you ever change your mind, check out Clean Flash Player's website!";
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::Green));
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_failure(f: &mut Frame, area: Rect, state: &TuiUninstallState) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(2)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let header = Paragraph::new(
|
||||||
|
"Oops! The uninstallation process has encountered an unexpected problem.\nPress Enter to retry or q to quit.",
|
||||||
|
)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD));
|
||||||
|
f.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
let detail_text = if state.failure_detail.len() > 500 {
|
||||||
|
&state.failure_detail[..500]
|
||||||
|
} else {
|
||||||
|
&state.failure_detail
|
||||||
|
};
|
||||||
|
let detail = Paragraph::new(detail_text.to_string())
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.style(Style::default().fg(Color::White));
|
||||||
|
f.render_widget(detail, chunks[1]);
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Clean Flash Installer</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Clean Flash Player Installer</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.flashpatch.clean-flash-installer</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>34.0.0.330</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>34.0.0.330</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>clean_flash_installer</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Clean Flash Uninstaller</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Clean Flash Player Uninstaller</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.flashpatch.clean-flash-uninstaller</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>34.0.0.330</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>34.0.0.330</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>clean_flash_uninstaller</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Loading…
Reference in New Issue