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:
parent
fcb24f803b
commit
3f5ce430f9
7 changed files with 274 additions and 22 deletions
46
src/main.rs
46
src/main.rs
|
@ -3,6 +3,7 @@ use relm4::prelude::*;
|
||||||
use anime_launcher_sdk::config;
|
use anime_launcher_sdk::config;
|
||||||
use anime_launcher_sdk::anime_game_core::prelude::*;
|
use anime_launcher_sdk::anime_game_core::prelude::*;
|
||||||
use anime_launcher_sdk::anime_game_core::genshin::prelude::*;
|
use anime_launcher_sdk::anime_game_core::genshin::prelude::*;
|
||||||
|
use anime_launcher_sdk::consts::launcher_dir;
|
||||||
|
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::filter::*;
|
use tracing_subscriber::filter::*;
|
||||||
|
@ -37,19 +38,35 @@ lazy_static::lazy_static! {
|
||||||
|
|
||||||
pub static ref GAME: Game = Game::new(&CONFIG.game.path);
|
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`
|
/// 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`
|
/// 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
|
/// 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`
|
/// 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() {
|
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
|
// Force debug output
|
||||||
let force_debug = std::env::args().any(|arg| &arg == "--debug");
|
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_application_name("An Anime Game Launcher");
|
||||||
gtk::glib::set_program_name(Some("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
|
// Create the app
|
||||||
let app = RelmApp::new(APP_ID);
|
let app = RelmApp::new(APP_ID);
|
||||||
|
|
||||||
|
@ -121,6 +131,20 @@ fn main() {
|
||||||
}}
|
}}
|
||||||
", BACKGROUND_FILE.to_string_lossy()));
|
", BACKGROUND_FILE.to_string_lossy()));
|
||||||
|
|
||||||
// Run the app
|
// Run FirstRun window if .first-run file persist
|
||||||
|
if FIRST_RUN_FILE.exists() {
|
||||||
|
app.run::<ui::first_run::main::FirstRunApp>(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>(());
|
app.run::<ui::main::App>(());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
140
src/ui/first_run/main.rs
Normal file
140
src/ui/first_run/main.rs
Normal 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
2
src/ui/first_run/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod main;
|
||||||
|
pub mod welcome;
|
87
src/ui/first_run/welcome.rs
Normal file
87
src/ui/first_run/welcome.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -346,8 +346,7 @@ impl SimpleComponent for App {
|
||||||
// TODO: reduce code somehow
|
// TODO: reduce code somehow
|
||||||
|
|
||||||
group.add_action::<LauncherFolder>(&RelmAction::new_stateless(clone!(@strong sender => move |_| {
|
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(LAUNCHER_FOLDER.as_path()).spawn() {
|
||||||
if let Err(err) = std::process::Command::new("xdg-open").arg(dir).spawn() {
|
|
||||||
sender.input(AppMsg::Toast {
|
sender.input(AppMsg::Toast {
|
||||||
title: tr("launcher-folder-opening-error"),
|
title: tr("launcher-folder-opening-error"),
|
||||||
description: Some(err.to_string())
|
description: Some(err.to_string())
|
||||||
|
@ -355,7 +354,6 @@ impl SimpleComponent for App {
|
||||||
|
|
||||||
tracing::error!("Failed to open launcher folder: {err}");
|
tracing::error!("Failed to open launcher folder: {err}");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})));
|
})));
|
||||||
|
|
||||||
group.add_action::<GameFolder>(&RelmAction::new_stateless(clone!(@strong sender => move |_| {
|
group.add_action::<GameFolder>(&RelmAction::new_stateless(clone!(@strong sender => move |_| {
|
||||||
|
|
|
@ -2,3 +2,4 @@ pub mod main;
|
||||||
pub mod about;
|
pub mod about;
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
|
pub mod first_run;
|
||||||
|
|
|
@ -106,12 +106,12 @@ impl SimpleAsyncComponent for PreferencesApp {
|
||||||
#[allow(unused_must_use)]
|
#[allow(unused_must_use)]
|
||||||
PreferencesAppMsg::UpdateGameDiff(diff) => {
|
PreferencesAppMsg::UpdateGameDiff(diff) => {
|
||||||
self.general.sender().send(GeneralAppMsg::UpdateGameDiff(diff));
|
self.general.sender().send(GeneralAppMsg::UpdateGameDiff(diff));
|
||||||
},
|
}
|
||||||
|
|
||||||
#[allow(unused_must_use)]
|
#[allow(unused_must_use)]
|
||||||
PreferencesAppMsg::UpdatePatch(patch) => {
|
PreferencesAppMsg::UpdatePatch(patch) => {
|
||||||
self.general.sender().send(GeneralAppMsg::UpdatePatch(patch));
|
self.general.sender().send(GeneralAppMsg::UpdatePatch(patch));
|
||||||
},
|
}
|
||||||
|
|
||||||
PreferencesAppMsg::Toast { title, description } => unsafe {
|
PreferencesAppMsg::Toast { title, description } => unsafe {
|
||||||
let toast = adw::Toast::new(&title);
|
let toast = adw::Toast::new(&title);
|
||||||
|
|
Loading…
Reference in a new issue