feat(ui): initial work on adding first run window

Added blank first run window with welcome page. On first start launcher will create launcher folder and `.first-run` file inside if needed. If file exists - launcher will open first run window instead of the main one (to prevent further data loadings in `init` functions)
This commit is contained in:
Observer KRypt0n_ 2023-02-23 20:37:02 +02:00
parent fcb24f803b
commit 3f5ce430f9
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
7 changed files with 274 additions and 22 deletions

View file

@ -3,6 +3,7 @@ use relm4::prelude::*;
use anime_launcher_sdk::config;
use anime_launcher_sdk::anime_game_core::prelude::*;
use anime_launcher_sdk::anime_game_core::genshin::prelude::*;
use anime_launcher_sdk::consts::launcher_dir;
use tracing_subscriber::prelude::*;
use tracing_subscriber::filter::*;
@ -37,19 +38,35 @@ lazy_static::lazy_static! {
pub static ref GAME: Game = Game::new(&CONFIG.game.path);
/// Path to launcher folder. Standard is `$HOME/.local/share/anime-game-launcher`
pub static ref LAUNCHER_FOLDER: PathBuf = launcher_dir().unwrap_or_default();
/// Path to `debug.log` file. Standard is `$HOME/.local/share/anime-game-launcher/debug.log`
pub static ref DEBUG_FILE: PathBuf = anime_launcher_sdk::consts::launcher_dir().unwrap_or_default().join("debug.log");
pub static ref DEBUG_FILE: PathBuf = LAUNCHER_FOLDER.join("debug.log");
/// Path to `background` file. Standard is `$HOME/.local/share/anime-game-launcher/background`
pub static ref BACKGROUND_FILE: PathBuf = anime_launcher_sdk::consts::launcher_dir().unwrap_or_default().join("background");
pub static ref BACKGROUND_FILE: PathBuf = LAUNCHER_FOLDER.join("background");
/// Path to `.keep-background` file. Used to mark launcher that it shouldn't update background picture
///
/// Standard is `$HOME/.local/share/anime-game-launcher/.keep-background`
pub static ref KEEP_BACKGROUND_FILE: PathBuf = anime_launcher_sdk::consts::launcher_dir().unwrap_or_default().join(".keep-background");
pub static ref KEEP_BACKGROUND_FILE: PathBuf = LAUNCHER_FOLDER.join(".keep-background");
/// Path to `.first-run` file. Used to mark launcher that it should run FirstRun window
///
/// Standard is `$HOME/.local/share/anime-game-launcher/.first-run`
pub static ref FIRST_RUN_FILE: PathBuf = LAUNCHER_FOLDER.join(".first-run");
}
fn main() {
// Create launcher folder if it isn't
if !LAUNCHER_FOLDER.exists() {
std::fs::create_dir_all(LAUNCHER_FOLDER.as_path()).expect("Failed to create launcher folder");
// This one is kinda critical buy well, I can't do something with it
std::fs::write(FIRST_RUN_FILE.as_path(), "").expect("Failed to create .first-run file");
}
// Force debug output
let force_debug = std::env::args().any(|arg| &arg == "--debug");
@ -98,13 +115,6 @@ fn main() {
gtk::glib::set_application_name("An Anime Game Launcher");
gtk::glib::set_program_name(Some("An Anime Game Launcher"));
// Set UI language
unsafe {
i18n::LANG = config::get().unwrap().launcher.language.parse().unwrap();
tracing::info!("Set UI language to {}", i18n::LANG);
}
// Create the app
let app = RelmApp::new(APP_ID);
@ -120,7 +130,21 @@ fn main() {
background-size: cover;
}}
", BACKGROUND_FILE.to_string_lossy()));
// Run FirstRun window if .first-run file persist
if FIRST_RUN_FILE.exists() {
app.run::<ui::first_run::main::FirstRunApp>(());
}
// Run the app
app.run::<ui::main::App>(());
// Run the app if everything's ready
else {
// Set UI language
unsafe {
i18n::LANG = config::get().unwrap().launcher.language.parse().unwrap();
tracing::info!("Set UI language to {}", i18n::LANG);
}
app.run::<ui::main::App>(());
}
}

140
src/ui/first_run/main.rs Normal file
View file

@ -0,0 +1,140 @@
use relm4::prelude::*;
use relm4::component::*;
use gtk::prelude::*;
use adw::prelude::*;
use crate::i18n::tr;
use super::welcome::*;
static mut MAIN_WINDOW: Option<adw::Window> = None;
pub struct FirstRunApp {
welcome: AsyncController<WelcomeApp>,
toast_overlay: adw::ToastOverlay,
carousel: adw::Carousel
}
#[derive(Debug, Clone)]
pub enum FirstRunAppMsg {
ScrollToTosWarning,
Toast {
title: String,
description: Option<String>
}
}
#[relm4::component(pub)]
impl SimpleComponent for FirstRunApp {
type Init = ();
type Input = FirstRunAppMsg;
type Output = ();
view! {
window = adw::Window {
set_title: Some("Welcome"), // TODO: update this based on currently open page
set_default_size: (780, 560),
#[local_ref]
toast_overlay -> adw::ToastOverlay {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
adw::HeaderBar {
add_css_class: "flat"
},
#[local_ref]
carousel -> adw::Carousel {
set_allow_mouse_drag: false,
append = model.welcome.widget(),
},
adw::CarouselIndicatorDots {
set_carousel: Some(&carousel),
set_height_request: 32
}
}
}
}
}
fn init(
_parent: Self::Init,
root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
tracing::info!("Initializing first run window");
let toast_overlay = adw::ToastOverlay::new();
let carousel = adw::Carousel::new();
let model = Self {
welcome: WelcomeApp::builder()
.launch(())
.detach(),
toast_overlay,
carousel
};
let toast_overlay = &model.toast_overlay;
let carousel = &model.carousel;
let widgets = view_output!();
unsafe {
MAIN_WINDOW = Some(widgets.window.clone());
}
ComponentParts { model, widgets } // will return soon
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
tracing::debug!("Called first run window event: {:?}", msg);
match msg {
FirstRunAppMsg::ScrollToTosWarning => {
self.carousel.scroll_to(self.welcome.widget(), true);
}
FirstRunAppMsg::Toast { title, description } => unsafe {
let toast = adw::Toast::new(&title);
toast.set_timeout(5);
if let Some(description) = description {
toast.set_button_label(Some(&tr("details")));
let dialog = adw::MessageDialog::new(MAIN_WINDOW.as_ref(), Some(&title), Some(&description));
dialog.add_response("close", &tr("close"));
dialog.add_response("save", &tr("save"));
dialog.set_response_appearance("save", adw::ResponseAppearance::Suggested);
#[allow(unused_must_use)]
dialog.connect_response(Some("save"), |_, _| {
let result = std::process::Command::new("xdg-open")
.arg(crate::DEBUG_FILE.as_os_str())
.output();
if let Err(err) = result {
tracing::error!("Failed to open debug file: {}", err);
}
});
toast.connect_button_clicked(move |_| {
dialog.show();
});
}
self.toast_overlay.add_toast(&toast);
}
}
}
}

2
src/ui/first_run/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod main;
pub mod welcome;

View file

@ -0,0 +1,87 @@
use relm4::prelude::*;
use relm4::component::*;
use adw::prelude::*;
use crate::*;
use super::main::FirstRunAppMsg;
pub struct WelcomeApp;
#[derive(Debug, Clone)]
pub enum WelcomeAppMsg {
Continue
}
#[relm4::component(async, pub)]
impl SimpleAsyncComponent for WelcomeApp {
type Init = ();
type Input = WelcomeAppMsg;
type Output = FirstRunAppMsg;
view! {
adw::PreferencesPage {
set_hexpand: true,
add = &adw::PreferencesGroup {
gtk::Image {
set_resource: Some("/org/app/images/icon.png"),
set_height_request: 128,
set_margin_top: 16
},
gtk::Label {
set_label: "An Anime Game Launcher",
set_margin_top: 32,
add_css_class: "title-1"
},
gtk::Label {
set_label: "Hi there! Welcome to the An Anime Game Launcher. We need to prepare some stuff and download default components before you could run the game",
set_justify: gtk::Justification::Center,
set_wrap: true,
set_margin_top: 32
}
},
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_halign: gtk::Align::Center,
set_spacing: 8,
gtk::Button {
set_label: "Continue",
add_css_class: "suggested-action",
connect_clicked => WelcomeAppMsg::Continue
}
}
}
}
}
async fn init(
_init: Self::Init,
root: Self::Root,
_sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let model = Self;
let widgets = view_output!();
AsyncComponentParts { model, widgets }
}
async fn update(&mut self, msg: Self::Input, sender: AsyncComponentSender<Self>) {
match msg {
#[allow(unused_must_use)]
WelcomeAppMsg::Continue => {
sender.output(Self::Output::ScrollToTosWarning);
}
}
}
}

View file

@ -346,15 +346,13 @@ impl SimpleComponent for App {
// TODO: reduce code somehow
group.add_action::<LauncherFolder>(&RelmAction::new_stateless(clone!(@strong sender => move |_| {
if let Some(dir) = anime_launcher_sdk::consts::launcher_dir() {
if let Err(err) = std::process::Command::new("xdg-open").arg(dir).spawn() {
sender.input(AppMsg::Toast {
title: tr("launcher-folder-opening-error"),
description: Some(err.to_string())
});
if let Err(err) = std::process::Command::new("xdg-open").arg(LAUNCHER_FOLDER.as_path()).spawn() {
sender.input(AppMsg::Toast {
title: tr("launcher-folder-opening-error"),
description: Some(err.to_string())
});
tracing::error!("Failed to open launcher folder: {err}");
}
tracing::error!("Failed to open launcher folder: {err}");
}
})));

View file

@ -2,3 +2,4 @@ pub mod main;
pub mod about;
pub mod preferences;
pub mod components;
pub mod first_run;

View file

@ -106,12 +106,12 @@ impl SimpleAsyncComponent for PreferencesApp {
#[allow(unused_must_use)]
PreferencesAppMsg::UpdateGameDiff(diff) => {
self.general.sender().send(GeneralAppMsg::UpdateGameDiff(diff));
},
}
#[allow(unused_must_use)]
PreferencesAppMsg::UpdatePatch(patch) => {
self.general.sender().send(GeneralAppMsg::UpdatePatch(patch));
},
}
PreferencesAppMsg::Toast { title, description } => unsafe {
let toast = adw::Toast::new(&title);