mirror of
https://github.com/an-anime-team/sleepy-launcher.git
synced 2025-02-17 02:09:46 +03:00
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
48
src/main.rs
48
src/main.rs
|
@ -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
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,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}");
|
||||
}
|
||||
})));
|
||||
|
||||
|
|
|
@ -2,3 +2,4 @@ pub mod main;
|
|||
pub mod about;
|
||||
pub mod preferences;
|
||||
pub mod components;
|
||||
pub mod first_run;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue