kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
pub mod entry;

mod locale;
mod operations;
mod platform;
mod style;
mod uninstaller;
mod view;
mod window_events;

use iced::{Element, Settings, Size, Subscription, Task, Theme, time, window};
use k580_ui::install_mode::{InstallMode, InstallScope};
use locale::Locale;
use operations::{
    InstallReport, InstallRequest, default_install_dir, install, launch_installed_app,
    open_install_folder,
};
use std::path::PathBuf;
use std::time::Duration;

#[derive(Debug, Clone)]
pub enum Message {
    ModeSelected(InstallMode),
    #[cfg(windows)]
    ScopeSelected(InstallScope),
    InstallDirChanged(String),
    BrowseInstallDir,
    InstallDirPicked(Option<PathBuf>),
    AddToPathToggled(bool),
    DesktopShortcutToggled(bool),
    FileAssociationToggled(bool),
    InstallPressed,
    InstallProgressTick,
    InstallFinished(Result<InstallReport, String>),
    PostInstallActionToggled(bool),
    DonePressed,
    PostInstallActionFinished(Result<(), String>),
    WindowOpened(window::Id),
    WindowDragStart,
    WindowMinimize,
    WindowToggleMaximize,
    WindowClose,
    WindowMaximizedChanged(bool),
}

pub struct Installer {
    mode: InstallMode,
    locale: Locale,
    scope: InstallScope,
    install_dir: String,
    add_to_path: bool,
    create_desktop_shortcut: bool,
    associate_580_files: bool,
    installing: bool,
    pending_install: Option<InstallRequest>,
    install_progress: f32,
    post_install_action: bool,
    post_install_error: Option<String>,
    result: Option<Result<InstallReport, String>>,
    window_id: Option<window::Id>,
    window_maximized: bool,
}

pub fn run() -> iced::Result {
    iced::application(Installer::new, Installer::update, Installer::view)
        .title(title)
        .theme(theme)
        .subscription(Installer::subscription)
        .style(style::app_style)
        .settings(Settings {
            antialiasing: true,
            ..Settings::default()
        })
        .window(window::Settings {
            size: Size::new(680.0, 760.0),
            min_size: Some(Size::new(640.0, 720.0)),
            position: window::Position::Centered,
            decorations: false,
            exit_on_close_request: false,
            ..window::Settings::default()
        })
        .run()
}

pub fn run_uninstaller(install_dir: PathBuf) -> iced::Result {
    uninstaller::run(install_dir)
}

fn title(state: &Installer) -> String {
    state
        .locale
        .t(locale::Text::WindowTitleInstaller)
        .to_owned()
}

fn theme(_state: &Installer) -> Theme {
    Theme::Dark
}

impl Installer {
    fn new() -> (Self, Task<Message>) {
        let mode = InstallMode::System;
        let scope = InstallScope::User;
        (
            Self {
                mode,
                locale: Locale::system(),
                scope,
                install_dir: default_install_dir(mode, scope).display().to_string(),
                add_to_path: true,
                create_desktop_shortcut: true,
                associate_580_files: true,
                installing: false,
                pending_install: None,
                install_progress: 0.0,
                post_install_action: true,
                post_install_error: None,
                result: None,
                window_id: None,
                window_maximized: false,
            },
            Task::none(),
        )
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::ModeSelected(mode) => {
                self.mode = mode;
                if mode == InstallMode::Portable {
                    self.scope = InstallScope::User;
                }
                self.install_dir = default_install_dir(self.mode, self.scope)
                    .display()
                    .to_string();
                self.result = None;
                self.pending_install = None;
                self.post_install_action = true;
                self.post_install_error = None;
            }
            #[cfg(windows)]
            Message::ScopeSelected(scope) => {
                self.scope = scope;
                self.install_dir = default_install_dir(self.mode, self.scope)
                    .display()
                    .to_string();
                self.result = None;
                self.pending_install = None;
                self.post_install_action = true;
                self.post_install_error = None;
            }
            Message::InstallDirChanged(value) => {
                self.install_dir = value;
                self.result = None;
                self.post_install_error = None;
            }
            Message::BrowseInstallDir => {
                let current = PathBuf::from(self.install_dir.clone());
                return Task::perform(
                    async move { pick_folder(current) },
                    Message::InstallDirPicked,
                );
            }
            Message::InstallDirPicked(Some(path)) => {
                self.install_dir = path.display().to_string();
                self.result = None;
                self.post_install_error = None;
            }
            Message::InstallDirPicked(None) => {}
            Message::AddToPathToggled(value) => {
                self.add_to_path = value;
                self.result = None;
                self.post_install_error = None;
            }
            Message::DesktopShortcutToggled(value) => {
                self.create_desktop_shortcut = value;
                self.result = None;
                self.post_install_error = None;
            }
            Message::FileAssociationToggled(value) => {
                self.associate_580_files = value;
                self.result = None;
                self.post_install_error = None;
            }
            Message::InstallPressed => {
                if self.installing {
                    return Task::none();
                }
                let request = match self.request() {
                    Ok(request) => request,
                    Err(error) => {
                        self.result = Some(Err(error));
                        return Task::none();
                    }
                };
                self.installing = true;
                self.pending_install = Some(request);
                self.install_progress = 0.06;
                self.post_install_error = None;
                self.result = None;
            }
            Message::InstallProgressTick => {
                if self.installing {
                    if let Some(request) = self.pending_install.take() {
                        self.install_progress = 0.18;
                        return Task::perform(
                            async move { install(request) },
                            Message::InstallFinished,
                        );
                    }
                    self.install_progress = if self.install_progress >= 0.92 {
                        0.18
                    } else {
                        self.install_progress + 0.08
                    };
                }
            }
            Message::InstallFinished(result) => {
                self.installing = false;
                self.pending_install = None;
                self.install_progress = 1.0;
                self.post_install_action = result.is_ok();
                self.result = Some(result);
            }
            Message::PostInstallActionToggled(value) => {
                self.post_install_action = value;
                self.post_install_error = None;
            }
            Message::DonePressed => {
                let Some(Ok(report)) = self.result.clone() else {
                    return self.close_window();
                };
                if !self.post_install_action {
                    return self.close_window();
                }
                self.post_install_error = None;
                return Task::perform(
                    async move {
                        match report.mode {
                            InstallMode::Portable => open_install_folder(report.install_dir),
                            InstallMode::System => launch_installed_app(report.k580_path),
                        }
                    },
                    Message::PostInstallActionFinished,
                );
            }
            Message::PostInstallActionFinished(result) => match result {
                Ok(()) => return self.close_window(),
                Err(error) => {
                    self.post_install_error = Some(error);
                }
            },
            Message::WindowOpened(id) => {
                self.window_id = Some(id);
                return Task::batch([
                    window::run(id, |window| platform::set_rounded_corners(window)).discard(),
                    window::is_maximized(id).map(Message::WindowMaximizedChanged),
                ]);
            }
            Message::WindowDragStart => {
                return self.window_id.map_or_else(Task::none, window::drag);
            }
            Message::WindowMinimize => {
                return self
                    .window_id
                    .map_or_else(Task::none, |id| window::minimize(id, true));
            }
            Message::WindowToggleMaximize => {
                let Some(id) = self.window_id else {
                    return Task::none();
                };
                self.window_maximized = !self.window_maximized;
                return Task::batch([
                    window::toggle_maximize(id),
                    window::is_maximized(id).map(Message::WindowMaximizedChanged),
                ]);
            }
            Message::WindowClose => {
                return self.close_window();
            }
            Message::WindowMaximizedChanged(maximized) => {
                self.window_maximized = maximized;
            }
        }
        Task::none()
    }

    fn subscription(&self) -> Subscription<Message> {
        let window_events = Subscription::batch([
            window::open_events().map(Message::WindowOpened),
            iced::event::listen_with(|event, _status, _window| {
                window_events::close_request(event).then_some(Message::WindowClose)
            }),
        ]);
        if self.installing {
            Subscription::batch([
                window_events,
                time::every(Duration::from_millis(120)).map(|_| Message::InstallProgressTick),
            ])
        } else {
            window_events
        }
    }

    fn view(&self) -> Element<'_, Message> {
        view::view(self)
    }

    fn request(&self) -> Result<InstallRequest, String> {
        let install_dir = self.install_dir.trim();
        if install_dir.is_empty() {
            return Err("installation folder is empty".to_owned());
        }
        Ok(InstallRequest {
            mode: self.mode,
            scope: self.scope,
            install_dir: PathBuf::from(install_dir),
            add_to_path: self.add_to_path,
            create_desktop_shortcut: self.create_desktop_shortcut,
            associate_580_files: self.associate_580_files,
        })
    }

    fn close_window(&self) -> Task<Message> {
        self.window_id.map_or_else(iced::exit, window::close)
    }
}

fn pick_folder(current: PathBuf) -> Option<PathBuf> {
    let mut dialog = rfd::FileDialog::new();
    if current.is_dir() {
        dialog = dialog.set_directory(current);
    }
    dialog.pick_folder()
}

impl Installer {
    pub fn mode(&self) -> InstallMode {
        self.mode
    }

    pub fn locale(&self) -> Locale {
        self.locale
    }

    #[cfg(windows)]
    pub fn scope(&self) -> InstallScope {
        self.scope
    }

    pub fn install_dir(&self) -> &str {
        &self.install_dir
    }

    pub fn add_to_path(&self) -> bool {
        self.add_to_path
    }

    pub fn create_desktop_shortcut(&self) -> bool {
        self.create_desktop_shortcut
    }

    pub fn associate_580_files(&self) -> bool {
        self.associate_580_files
    }

    pub fn installing(&self) -> bool {
        self.installing
    }

    pub fn install_progress(&self) -> f32 {
        self.install_progress
    }

    pub fn post_install_action(&self) -> bool {
        self.post_install_action
    }

    pub fn post_install_error(&self) -> Option<&str> {
        self.post_install_error.as_deref()
    }

    pub fn result(&self) -> Option<&Result<InstallReport, String>> {
        self.result.as_ref()
    }

    pub fn window_maximized(&self) -> bool {
        self.window_maximized
    }
}