Running games list (Closes #398)
This commit is contained in:
@@ -14,7 +14,11 @@ use wgui::{
|
||||
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
|
||||
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
|
||||
};
|
||||
use wlx_common::{audio, dash_interface::BoxDashInterface, timestep::Timestep};
|
||||
use wlx_common::{
|
||||
audio,
|
||||
dash_interface::BoxDashInterface,
|
||||
timestep::{self, Timestep},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
assets,
|
||||
@@ -218,8 +222,10 @@ impl<T: 'static> Frontend<T> {
|
||||
self.process_task(&mut params, task)?;
|
||||
}
|
||||
|
||||
let time_ms = timestep::get_micros() / 1000;
|
||||
|
||||
if let Some(mut tab) = self.current_tab.take() {
|
||||
tab.update(self, params.data)?;
|
||||
tab.update(self, time_ms as u32, params.data)?;
|
||||
|
||||
self.current_tab = Some(tab);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ impl<T> Tab<T> for TabApps<T> {
|
||||
TabType::Apps
|
||||
}
|
||||
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
|
||||
for task in self.tasks.drain() {
|
||||
|
||||
@@ -9,7 +9,8 @@ use wgui::{
|
||||
use crate::{
|
||||
frontend::Frontend,
|
||||
tab::{Tab, TabType},
|
||||
views::game_list,
|
||||
util::steam_utils::SteamUtils,
|
||||
views::{game_list, running_games_list},
|
||||
};
|
||||
|
||||
pub struct TabGames<T> {
|
||||
@@ -17,6 +18,8 @@ pub struct TabGames<T> {
|
||||
pub state: ParserState,
|
||||
|
||||
view_game_list: game_list::View,
|
||||
view_running_games_list: running_games_list::View,
|
||||
steam_utils: SteamUtils,
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
@@ -25,17 +28,22 @@ impl<T> Tab<T> for TabGames<T> {
|
||||
TabType::Games
|
||||
}
|
||||
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, _data: &mut T) -> anyhow::Result<()> {
|
||||
self.view_game_list.update(&mut frontend.layout, &frontend.executor)?;
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, time_ms: u32, _data: &mut T) -> anyhow::Result<()> {
|
||||
self
|
||||
.view_game_list
|
||||
.update(&mut frontend.layout, &mut self.steam_utils, &frontend.executor)?;
|
||||
self.view_running_games_list.update(&mut frontend.layout, time_ms)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TabGames<T> {
|
||||
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
|
||||
let globals = frontend.layout.state.globals.clone();
|
||||
|
||||
let state = wgui::parser::parse_from_assets(
|
||||
&ParseDocumentParams {
|
||||
globals: frontend.layout.state.globals.clone(),
|
||||
globals: globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/tab/games.xml"),
|
||||
extra: Default::default(),
|
||||
},
|
||||
@@ -44,19 +52,32 @@ impl<T> TabGames<T> {
|
||||
)?;
|
||||
|
||||
let game_list_parent = state.get_widget_id("game_list_parent")?;
|
||||
let id_running_games_list_parent = state.get_widget_id("running_games_list_parent")?;
|
||||
|
||||
let view_game_list = game_list::View::new(game_list::Params {
|
||||
executor: frontend.executor.clone(),
|
||||
frontend_tasks: frontend.tasks.clone(),
|
||||
globals: frontend.layout.state.globals.clone(),
|
||||
globals: globals.clone(),
|
||||
layout: &mut frontend.layout,
|
||||
parent_id: game_list_parent,
|
||||
})?;
|
||||
|
||||
let mut steam_utils = SteamUtils::new()?;
|
||||
|
||||
let view_running_games_list = running_games_list::View::new(running_games_list::Params {
|
||||
globals: globals.clone(),
|
||||
layout: &mut frontend.layout,
|
||||
parent_id: id_running_games_list_parent,
|
||||
steam_utils: &mut steam_utils,
|
||||
frontend_tasks: frontend.tasks.clone(),
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
view_game_list,
|
||||
view_running_games_list,
|
||||
marker: PhantomData,
|
||||
steam_utils,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub trait Tab<T> {
|
||||
#[allow(dead_code)]
|
||||
fn get_type(&self) -> TabType;
|
||||
|
||||
fn update(&mut self, _: &mut Frontend<T>, _: &mut T) -> anyhow::Result<()> {
|
||||
fn update(&mut self, _frontend: &mut Frontend<T>, _time_ms: u32, _user_data: &mut T) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ impl<T> Tab<T> for TabMonado<T> {
|
||||
TabType::Games
|
||||
}
|
||||
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::Refresh => self.refresh(frontend, data)?,
|
||||
|
||||
@@ -80,7 +80,7 @@ impl<T> Tab<T> for TabSettings<T> {
|
||||
TabType::Settings
|
||||
}
|
||||
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
|
||||
let mut changed = false;
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
|
||||
@@ -5,3 +5,4 @@ pub mod popup_manager;
|
||||
pub mod steam_utils;
|
||||
pub mod toast_manager;
|
||||
pub mod various;
|
||||
pub mod wgui_simple;
|
||||
|
||||
@@ -35,6 +35,7 @@ pub struct AppManifest {
|
||||
// TODO @oo8dev: game sort methods
|
||||
#[allow(dead_code)]
|
||||
pub enum GameSortMethod {
|
||||
None,
|
||||
NameAsc,
|
||||
NameDesc,
|
||||
PlayDateDesc,
|
||||
@@ -127,9 +128,6 @@ pub fn launch(app_id: &AppID) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO @oo8dev: running games list (#398)
|
||||
/*
|
||||
|
||||
pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
|
||||
log::info!("Stopping Steam game with AppID {}", app_id);
|
||||
|
||||
@@ -220,7 +218,7 @@ pub fn list_running_games() -> anyhow::Result<Vec<RunningGame>> {
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
} */
|
||||
}
|
||||
|
||||
fn call_steam(arg: &str) -> anyhow::Result<()> {
|
||||
match std::process::Command::new("xdg-open").arg(arg).spawn() {
|
||||
@@ -286,6 +284,7 @@ impl SteamUtils {
|
||||
.collect();
|
||||
|
||||
match sort_method {
|
||||
GameSortMethod::None => {}
|
||||
GameSortMethod::NameAsc => {
|
||||
games.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
}
|
||||
|
||||
23
dash-frontend/src/util/wgui_simple.rs
Normal file
23
dash-frontend/src/util/wgui_simple.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use wgui::{
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
renderer_vk::text::TextStyle,
|
||||
widget::label::{WidgetLabel, WidgetLabelParams},
|
||||
};
|
||||
|
||||
pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> {
|
||||
let label = WidgetLabel::create(
|
||||
&mut layout.state.globals.get(),
|
||||
WidgetLabelParams {
|
||||
content,
|
||||
style: TextStyle {
|
||||
wrap: true,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
layout.add_child(parent, label, Default::default())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -180,7 +180,7 @@ impl View {
|
||||
let tasks = tasks.clone();
|
||||
Box::new(move |_, ev| {
|
||||
if let Some(mode) = ev.value.and_then(|v| {
|
||||
CompositorMode::from_str(&*v)
|
||||
CompositorMode::from_str(&v)
|
||||
.inspect_err(|_| {
|
||||
log::error!(
|
||||
"Invalid value for compositor: '{v}'. Valid values are: {:?}",
|
||||
@@ -199,7 +199,7 @@ impl View {
|
||||
let tasks = tasks.clone();
|
||||
Box::new(move |_, ev| {
|
||||
if let Some(mode) = ev.value.and_then(|v| {
|
||||
ResMode::from_str(&*v)
|
||||
ResMode::from_str(&v)
|
||||
.inspect_err(|_| {
|
||||
log::error!(
|
||||
"Invalid value for resolution: '{v}'. Valid values are: {:?}",
|
||||
@@ -237,7 +237,7 @@ impl View {
|
||||
let tasks = tasks.clone();
|
||||
Box::new(move |_, ev| {
|
||||
if let Some(mode) = ev.value.and_then(|v| {
|
||||
OrientationMode::from_str(&*v)
|
||||
OrientationMode::from_str(&v)
|
||||
.inspect_err(|_| {
|
||||
log::error!(
|
||||
"Invalid value for orientation: '{v}'. Valid values are: {:?}",
|
||||
|
||||
@@ -55,7 +55,6 @@ pub struct View {
|
||||
frontend_tasks: FrontendTasks,
|
||||
globals: WguiGlobals,
|
||||
id_list_parent: WidgetID,
|
||||
steam_utils: steam_utils::SteamUtils,
|
||||
cells: HashMap<AppID, Cell>,
|
||||
game_cover_view_common: game_cover::ViewCommon,
|
||||
executor: AsyncExecutor,
|
||||
@@ -75,8 +74,6 @@ impl View {
|
||||
|
||||
let tasks = Tasks::new();
|
||||
|
||||
let steam_utils = SteamUtils::new()?;
|
||||
|
||||
tasks.push(Task::Refresh);
|
||||
|
||||
Ok(Self {
|
||||
@@ -85,7 +82,6 @@ impl View {
|
||||
frontend_tasks: params.frontend_tasks,
|
||||
globals: params.globals.clone(),
|
||||
id_list_parent: list_parent.id,
|
||||
steam_utils,
|
||||
cells: HashMap::new(),
|
||||
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
|
||||
state: Rc::new(RefCell::new(State { view_launcher: None })),
|
||||
@@ -93,7 +89,12 @@ impl View {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
layout: &mut Layout,
|
||||
steam_utils: &mut SteamUtils,
|
||||
executor: &AsyncExecutor,
|
||||
) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let tasks = self.tasks.drain();
|
||||
if tasks.is_empty() {
|
||||
@@ -101,7 +102,7 @@ impl View {
|
||||
}
|
||||
for task in tasks {
|
||||
match task {
|
||||
Task::Refresh => self.refresh(layout, executor)?,
|
||||
Task::Refresh => self.refresh(layout, steam_utils, executor)?,
|
||||
Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?,
|
||||
Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(layout, app_id, cover_art),
|
||||
Task::CloseLauncher => self.state.borrow_mut().view_launcher = None,
|
||||
@@ -162,20 +163,23 @@ fn fill_game_list(
|
||||
}
|
||||
|
||||
impl View {
|
||||
fn game_list(&self) -> anyhow::Result<Games> {
|
||||
let manifests = self
|
||||
.steam_utils
|
||||
.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)?;
|
||||
fn game_list(&self, steam_utils: &mut SteamUtils) -> anyhow::Result<Games> {
|
||||
let manifests = steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)?;
|
||||
|
||||
Ok(Games { manifests })
|
||||
}
|
||||
|
||||
fn refresh(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
|
||||
fn refresh(
|
||||
&mut self,
|
||||
layout: &mut Layout,
|
||||
steam_utils: &mut SteamUtils,
|
||||
executor: &AsyncExecutor,
|
||||
) -> anyhow::Result<()> {
|
||||
layout.remove_children(self.id_list_parent);
|
||||
self.cells.clear();
|
||||
|
||||
let mut text: Option<Translation> = None;
|
||||
match self.game_list() {
|
||||
match self.game_list(steam_utils) {
|
||||
Ok(list) => {
|
||||
if list.manifests.is_empty() {
|
||||
text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND"))
|
||||
|
||||
@@ -3,3 +3,4 @@ pub mod audio_settings;
|
||||
pub mod game_cover;
|
||||
pub mod game_launcher;
|
||||
pub mod game_list;
|
||||
pub mod running_games_list;
|
||||
|
||||
178
dash-frontend/src/views/running_games_list.rs
Normal file
178
dash-frontend/src/views/running_games_list.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::ComponentButton,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
steam_utils::{self, AppID, AppManifest, GameSortMethod, SteamUtils},
|
||||
wgui_simple,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
Refresh,
|
||||
StopGame(AppID, bool /* kill */),
|
||||
}
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: WguiGlobals,
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
pub steam_utils: &'a mut SteamUtils,
|
||||
pub frontend_tasks: FrontendTasks,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
#[allow(dead_code)]
|
||||
state: ParserState,
|
||||
tasks: Tasks<Task>,
|
||||
last_update_ms: u32,
|
||||
id_list_parent: WidgetID,
|
||||
installed_games: Vec<AppManifest>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
}
|
||||
|
||||
fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
|
||||
ParseDocumentParams {
|
||||
globals,
|
||||
path: AssetPath::BuiltIn("gui/view/running_games_list.xml"),
|
||||
extra: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(params: Params) -> anyhow::Result<Self> {
|
||||
let state = wgui::parser::parse_from_assets(&doc_params(params.globals.clone()), params.layout, params.parent_id)?;
|
||||
let btn_refresh = state.fetch_component_as::<ComponentButton>("btn_refresh")?;
|
||||
let id_list_parent = state.get_widget_id("list_parent")?;
|
||||
|
||||
let installed_games = params
|
||||
.steam_utils
|
||||
.list_installed_games(GameSortMethod::None)
|
||||
.unwrap_or(Vec::new());
|
||||
|
||||
let tasks = Tasks::<Task>::new();
|
||||
|
||||
tasks.handle_button(&btn_refresh, Task::Refresh);
|
||||
tasks.push(Task::Refresh);
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
tasks,
|
||||
last_update_ms: 0,
|
||||
id_list_parent,
|
||||
installed_games,
|
||||
frontend_tasks: params.frontend_tasks,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, layout: &mut Layout, time_ms: u32) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let tasks = self.tasks.drain();
|
||||
if tasks.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
match task {
|
||||
Task::Refresh => self.refresh(layout)?,
|
||||
Task::StopGame(app_id, kill) => self.stop_game(app_id, kill),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.last_update_ms + 5000 < time_ms {
|
||||
self.last_update_ms = time_ms;
|
||||
self.tasks.push(Task::Refresh);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_name_from_appid<'a>(app_id: &AppID, manifests: &[AppManifest]) -> String {
|
||||
for manifest in manifests {
|
||||
if manifest.app_id == *app_id {
|
||||
return manifest.name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
format!("Unknown AppID {}", app_id)
|
||||
}
|
||||
|
||||
fn fill_list(&mut self, layout: &mut Layout, games: Vec<steam_utils::RunningGame>) -> anyhow::Result<()> {
|
||||
if games.is_empty() {
|
||||
wgui_simple::create_label(
|
||||
layout,
|
||||
self.id_list_parent,
|
||||
Translation::from_translation_key("GAME_LIST.NO_RUNNING_GAME_FOUND"),
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for game in games {
|
||||
let game_name = View::extract_name_from_appid(&game.app_id, &self.installed_games);
|
||||
|
||||
let t = self.state.parse_template(
|
||||
&doc_params(layout.state.globals.clone()),
|
||||
"RunningGameCell",
|
||||
layout,
|
||||
self.id_list_parent,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
let mut label_name = t.fetch_widget_as::<WidgetLabel>(&layout.state, "label_name")?;
|
||||
|
||||
self.tasks.handle_button(
|
||||
&t.fetch_component_as::<ComponentButton>("btn_stop")?,
|
||||
Task::StopGame(game.app_id.clone(), false),
|
||||
);
|
||||
|
||||
self.tasks.handle_button(
|
||||
&t.fetch_component_as::<ComponentButton>("btn_kill")?,
|
||||
Task::StopGame(game.app_id, true),
|
||||
);
|
||||
|
||||
label_name.set_text_simple(
|
||||
&mut layout.state.globals.get(),
|
||||
Translation::from_raw_text_string(game_name),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||
log::debug!("refreshing running games list");
|
||||
|
||||
layout.remove_children(self.id_list_parent);
|
||||
|
||||
match steam_utils::list_running_games() {
|
||||
Ok(games) => self.fill_list(layout, games)?,
|
||||
Err(e) => {
|
||||
log::error!("failed to list games: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_game(&mut self, app_id: AppID, kill: bool) {
|
||||
if let Err(e) = steam_utils::stop(app_id, kill) {
|
||||
self
|
||||
.frontend_tasks
|
||||
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
|
||||
"Error: {}",
|
||||
e
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user