feat: settings refactor and initial mod listing
Some checks failed
CI / Flatpak (push) Failing after 27s
CI / Rustfmt (push) Failing after 54s

This commit is contained in:
Tine Jozelj 2023-12-21 23:25:08 +01:00
parent 807f4b668c
commit 613c9032a9
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
18 changed files with 490 additions and 265 deletions

View file

@ -13,9 +13,9 @@
<default>false</default>
<summary>Window maximized state</summary>
</key>
<key name="games" type="as">
<key name="games" type="a(ss)">
<default>[]</default>
<summary>Games</summary>
<summary>List of games. (Slug, Path)</summary>
</key>
</schema>
</schemalist>

View file

@ -5,7 +5,6 @@ blueprints = custom_target('blueprints',
'ui/windows/main/pages/welcome.blp',
'ui/windows/main/pages/games_and_mods.blp',
'ui/windows/add_new_game/add_new_game.blp',
'ui/components/mods_list.blp',
),
output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],

View file

@ -6,7 +6,6 @@
<file compressed="true">ui/windows/main/pages/welcome.ui</file>
<file compressed="true">ui/windows/main/pages/games_and_mods.ui</file>
<file compressed="true">ui/windows/add_new_game/add_new_game.ui</file>
<file compressed="true">ui/components/mods_list.ui</file>
<file compressed="true">style.css</file>
</gresource>
</gresources>
</gresources>

View file

@ -1,135 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $ModsList : Adw.NavigationPage {
title: _("Main");
Adw.ToolbarView {
[top]
Adw.HeaderBar header_bar {
show-back-button: true;
}
content: Box {
orientation: vertical;
valign: center;
halign: center;
Image {
name: "logo";
icon-name: "re.sonny.Workbench";
pixel-size: 196;
margin-bottom: 30;
styles [
"icon-dropshadow"
]
}
Label {
label: _("Welcome to Mod Manager");
margin-bottom: 30;
styles [
"title-1"
]
}
Box subtitle {
orientation: vertical;
halign: center;
margin-bottom: 30;
Label {
label: "Learn and prototype with\nGNOME technologies";
justify: center;
}
}
Box {
orientation: vertical;
homogeneous: true;
halign: center;
Box {
margin-bottom: 6;
Image {
icon-name: "update-symbolic";
margin-end: 12;
icon-size: normal;
}
Label {
label: _("Edit Style or UI to update the Preview");
}
}
Box {
margin-bottom: 6;
Image {
icon-name: "media-playback-start-symbolic";
margin-end: 12;
icon-size: normal;
}
Label {
label: _("Hit");
}
ShortcutsShortcut {
accelerator: "<Control>Return";
margin-start: 12;
}
Label {
label: _("to format and run Code");
}
}
Box {
margin-bottom: 6;
Image {
icon-name: "media-floppy-symbolic";
margin-end: 12;
icon-size: normal;
}
Label {
label: _("Changes are automatically saved and restored");
}
}
Box {
margin-bottom: 6;
Image {
icon-name: "library-symbolic";
margin-end: 12;
icon-size: normal;
}
Label {
label: _("Browse the Library for demos and examples");
}
}
Box {
margin-bottom: 6;
Image {
icon-name: "user-bookmarks-symbolic";
margin-end: 12;
icon-size: normal;
}
Label {
label: _("Checkout the Bookmarks menu to learn and get help");
}
}
}
};
}
}

View file

@ -12,7 +12,11 @@ template $GamesAndMods: Adw.Bin {
Adw.HeaderBar {
[end]
Button add_new_game {
label: "Add game";
icon-name: "list-add-symbolic";
}
Button remove_all_games {
icon-name: "list-remove-symbolic";
}
}
@ -33,13 +37,24 @@ template $GamesAndMods: Adw.Bin {
}
}
content: Box {
Label {
content: Adw.Clamp {
orientation: vertical;
Label title {
label: "Mods";
styles ["title-1"]
}
Button remove_all_games {
label: "Remove all games";
Adw.Carousel {
}
Adw.CarouselIndicatorDots {}
ScrolledWindow {
vexpand: true;
ListBox mods_list {
}
}
};
}
};

View file

@ -7,21 +7,17 @@ template $Welcome: Adw.Bin {
Adw.HeaderBar {
}
content: Box {
orientation: vertical;
halign: center;
valign: center;
Label {
label: "No games added yet!";
justify: center;
wrap: true;
styles ["title-1"]
margin-bottom: 30;
}
Button add_new_game {
label: "Add new game";
styles ["suggested-action", "pill"]
content: Adw.Clamp {
Adw.StatusPage {
title: "No games added yet.";
description: "Click the button below to add a new game.";
icon-name: "start-here-symbolic";
Button add_new_game {
label: "Add new game";
styles ["suggested-action", "pill"]
}
}
};
}

View file

@ -1,5 +1,14 @@
use crate::settings::Game;
pub const API_URL: &str = "https://api.curseforge.com";
pub fn get_game_id(game: Game) -> i32 {
match game {
Game::TheSims4(_) => 78062,
_ => panic!("Game not supported"),
}
}
pub fn get_curse_forge_client() -> Result<reqwest::blocking::Client, reqwest::Error> {
let mut api_key_header =
reqwest::header::HeaderValue::from_str(crate::config::API_KEY).unwrap();

View file

@ -3,11 +3,16 @@ use serde::Deserialize;
use crate::api::*;
#[derive(Deserialize, Debug)]
struct Response {
struct GetGamesResponse {
data: Vec<Game>,
pagination: Pagination,
}
#[derive(Deserialize, Debug)]
struct GetGameResponse {
data: Game,
}
#[derive(Deserialize, Debug)]
struct Game {
id: u32,
@ -40,7 +45,18 @@ pub fn get_games() -> Vec<String> {
.get(&format!("{}/v1/games", base::API_URL))
.send()
.unwrap();
let json: Response = response.json().unwrap();
let json: GetGamesResponse = response.json().unwrap();
return json.data.iter().map(|game| game.name.clone()).collect();
}
pub fn get_game(id: &i32) -> Game {
let client = base::get_curse_forge_client().unwrap();
let response = client
.get(&format!("{}/v1/games/{}", base::API_URL, id))
.send()
.unwrap();
let json: GetGameResponse = response.json().unwrap();
return json.data;
}

View file

@ -3,3 +3,6 @@ pub use base::*;
pub mod games;
pub use games::*;
pub mod mods;
pub use mods::*;

275
src/api/mods.rs Normal file
View file

@ -0,0 +1,275 @@
use serde::Deserialize;
use super::{get_curse_forge_client, API_URL};
/*
{
"data": [
{
"id": 0,
"gameId": 0,
"name": "string",
"slug": "string",
"links": {
"websiteUrl": "string",
"wikiUrl": "string",
"issuesUrl": "string",
"sourceUrl": "string"
},
"summary": "string",
"status": 1,
"downloadCount": 0,
"isFeatured": true,
"primaryCategoryId": 0,
"categories": [
{
"id": 0,
"gameId": 0,
"name": "string",
"slug": "string",
"url": "string",
"iconUrl": "string",
"dateModified": "2019-08-24T14:15:22Z",
"isClass": true,
"classId": 0,
"parentCategoryId": 0,
"displayIndex": 0
}
],
"classId": 0,
"authors": [
{
"id": 0,
"name": "string",
"url": "string"
}
],
"logo": {
"id": 0,
"modId": 0,
"title": "string",
"description": "string",
"thumbnailUrl": "string",
"url": "string"
},
"screenshots": [
{
"id": 0,
"modId": 0,
"title": "string",
"description": "string",
"thumbnailUrl": "string",
"url": "string"
}
],
"mainFileId": 0,
"latestFiles": [
{
"id": 0,
"gameId": 0,
"modId": 0,
"isAvailable": true,
"displayName": "string",
"fileName": "string",
"releaseType": 1,
"fileStatus": 1,
"hashes": [
{
"value": "string",
"algo": 1
}
],
"fileDate": "2019-08-24T14:15:22Z",
"fileLength": 0,
"downloadCount": 0,
"fileSizeOnDisk": 0,
"downloadUrl": "string",
"gameVersions": [
"string"
],
"sortableGameVersions": [
{
"gameVersionName": "string",
"gameVersionPadded": "string",
"gameVersion": "string",
"gameVersionReleaseDate": "2019-08-24T14:15:22Z",
"gameVersionTypeId": 0
}
],
"dependencies": [
{
"modId": 0,
"relationType": 1
}
],
"exposeAsAlternative": true,
"parentProjectFileId": 0,
"alternateFileId": 0,
"isServerPack": true,
"serverPackFileId": 0,
"isEarlyAccessContent": true,
"earlyAccessEndDate": "2019-08-24T14:15:22Z",
"fileFingerprint": 0,
"modules": [
{
"name": "string",
"fingerprint": 0
}
]
}
],
"latestFilesIndexes": [
{
"gameVersion": "string",
"fileId": 0,
"filename": "string",
"releaseType": 1,
"gameVersionTypeId": 0,
"modLoader": 0
}
],
"latestEarlyAccessFilesIndexes": [
{
"gameVersion": "string",
"fileId": 0,
"filename": "string",
"releaseType": 1,
"gameVersionTypeId": 0,
"modLoader": 0
}
],
"dateCreated": "2019-08-24T14:15:22Z",
"dateModified": "2019-08-24T14:15:22Z",
"dateReleased": "2019-08-24T14:15:22Z",
"allowModDistribution": true,
"gamePopularityRank": 0,
"isAvailable": true,
"thumbsUpCount": 0
}
],
"pagination": {
"index": 0,
"pageSize": 0,
"resultCount": 0,
"totalCount": 0
}
}
*/
#[derive(Debug, Deserialize)]
struct Hash {
value: String,
algo: i32,
}
#[derive(Debug, Deserialize)]
struct SortableGameVersion {
gameVersionName: String,
gameVersionPadded: String,
gameVersion: String,
gameVersionReleaseDate: String,
gameVersionTypeId: i32,
}
#[derive(Debug, Deserialize)]
struct Dependency {
modId: i32,
relationType: i32,
}
#[derive(Debug, Deserialize)]
struct Module {
name: String,
fingerprint: i32,
}
#[derive(Debug, Deserialize)]
pub struct Mod {
pub name: String,
//pub modId: i32,
//pub gameSlug: String,
//pub gameId: i32,
pub summary: String,
//pub defaultFileId: i32,
//pub downloadCount: i32,
//pub latestFiles: Vec<LatestFile>,
//pub latestFilesIndexes: Vec<LatestFileIndex>,
//pub latestEarlyAccessFilesIndexes: Vec<LatestEarlyAccessFileIndex>,
//pub dateCreated: String,
//pub dateModified: String,
//pub dateReleased: String,
//pub allowModDistribution: bool,
//pub gamePopularityRank: i32,
//pub isAvailable: bool,
//pub thumbsUpCount: i32,
}
#[derive(Debug, Deserialize)]
struct LatestFile {
fileId: i32,
displayName: String,
fileName: String,
fileDate: String,
fileLength: i32,
releaseType: i32,
fileStatus: i32,
downloadUrl: String,
gameVersions: Vec<String>,
sortableGameVersions: Vec<SortableGameVersion>,
dependencies: Vec<Dependency>,
exposeAsAlternative: bool,
parentProjectFileId: i32,
alternateFileId: i32,
isServerPack: bool,
serverPackFileId: i32,
isEarlyAccessContent: bool,
earlyAccessEndDate: String,
fileFingerprint: i32,
modules: Vec<Module>,
}
#[derive(Debug, Deserialize)]
struct LatestFileIndex {
gameVersion: String,
fileId: i32,
filename: String,
releaseType: i32,
gameVersionTypeId: i32,
modLoader: i32,
}
#[derive(Debug, Deserialize)]
struct LatestEarlyAccessFileIndex {
gameVersion: String,
fileId: i32,
filename: String,
releaseType: i32,
gameVersionTypeId: i32,
modLoader: i32,
}
#[derive(Debug, Deserialize)]
struct GetModsResponse {
data: Vec<Mod>,
pagination: Pagination,
}
#[derive(Debug, Deserialize)]
struct Pagination {
index: i32,
pageSize: i32,
resultCount: i32,
totalCount: i32,
}
pub fn get_mods(game_id: &i32) -> Vec<Mod> {
let client = get_curse_forge_client().unwrap();
let response = client
.get(&format!("{}/v1/mods/search", API_URL))
.query(&[("gameId", game_id)])
.send()
.unwrap();
let json: GetModsResponse = response.json().unwrap();
return json.data;
}

View file

@ -25,16 +25,10 @@ use tracing::{debug, info};
use crate::config::VERSION;
use crate::config::{APP_ID, PKGDATADIR, PROFILE};
use crate::settings::ModManagerSettings;
use crate::windows::main::ModManagerWindowMain;
use crate::windows::main::Welcome;
mod imp {
use games_and_mods::GamesAndMods;
use crate::windows::games_and_mods;
use super::*;
#[derive(Debug, Default)]

View file

@ -1,2 +0,0 @@
pub mod mods_list;
pub use mods_list::*;

View file

@ -1,64 +0,0 @@
/* welcome.rs
*
* Copyright 2023 Tine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
use adw::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{glib::clone, prelude::*};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(resource = "/dev/mnts/ModManager/ui/components/mods_list.ui")]
pub struct ModsList {}
#[glib::object_subclass]
impl ObjectSubclass for ModsList {
const NAME: &'static str = "ModsList";
type Type = super::ModsList;
type ParentType = adw::NavigationPage;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ModsList {
fn constructed(&self) {}
}
impl WidgetImpl for ModsList {}
impl NavigationPageImpl for ModsList {}
}
glib::wrapper! {
pub struct ModsList(ObjectSubclass<imp::ModsList>)
@extends gtk::Widget, gtk::Buildable, adw::NavigationPage,
@implements gio::ActionGroup, gio::ActionMap;
}
impl ModsList {
pub fn new() -> Self {
glib::Object::builder().build()
}
}

View file

@ -17,7 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
mod api;
mod application;
mod components;
mod config;
mod settings;
mod windows;

View file

@ -1,15 +1,115 @@
use gio::glib;
use gsettings_macro::gen_settings;
use gtk::gio;
use std::collections::HashMap;
use tracing::{debug, info};
use crate::config::APP_ID;
#[gen_settings(file = "./data/dev.mnts.ModManager.gschema.xml.in")]
pub struct ModManagerSettings;
#[gen_settings_define(
key_name = "games",
arg_type = "Vec<(String, String)>",
ret_type = "Vec<(String, String)>"
)]
pub struct Settings;
impl Default for ModManagerSettings {
impl Default for Settings {
fn default() -> Self {
Self::new(APP_ID)
}
}
#[derive(Debug, Clone)]
pub enum Game {
TheSims4(String),
}
impl Game {
pub fn from_slug(slug: String, path: String) -> Self {
match slug.as_str() {
"sims4" => Game::TheSims4(path),
_ => panic!("Game not supported"),
}
}
pub fn from_name(name: String, path: String) -> Self {
match name.as_str() {
"The Sims 4" => Game::TheSims4(path),
_ => panic!("Game not supported"),
}
}
pub fn to_path(&self) -> String {
match self {
Game::TheSims4(path) => path.to_string(),
}
}
pub fn to_slug(&self) -> String {
match self {
Game::TheSims4(_) => "sims4".to_string(),
}
}
pub fn to_name(&self) -> String {
match self {
Game::TheSims4(_) => "The Sims 4".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct ModManagerSettings {
settings: Settings,
}
impl ModManagerSettings {
pub fn default() -> Self {
let settings = Settings::default();
Self { settings }
}
pub fn get_supported_games(&self) -> Vec<Game> {
info!("Getting supported games");
return [Game::TheSims4("".to_string())].to_vec();
}
pub fn get_managed_games(&self) -> Vec<Game> {
info!("Getting managed games");
let mut games_vec: Vec<Game> = Vec::new();
for (slug, path) in self.settings.games() {
info!("Found game {:?} at path {:?}", slug, path);
games_vec.push(Game::from_slug(slug, path));
}
return games_vec;
}
pub fn add_managed_game(&self, game: Game) {
info!("Adding game {} {}", game.to_slug(), game.to_name());
let mut games_vec: Vec<(String, String)> = Vec::new();
for (slug, path) in self.settings.games() {
games_vec.push((slug, path));
}
games_vec.push((game.to_slug(), game.to_path()));
self.settings.set_games(games_vec);
}
pub fn remove_managed_games(&self) {
info!("Removing all managed games");
self.settings.set_games([].to_vec());
}
pub fn on_managed_games_changed(&self, callback: impl Fn() + 'static) {
self.settings.connect_games_changed(move |_| {
callback();
});
}
}

View file

@ -25,7 +25,7 @@ use gtk::prelude::*;
use gtk::{gio, glib};
use crate::api::*;
use crate::settings::ModManagerSettings;
use crate::settings::{Game, ModManagerSettings};
mod imp {
use super::*;
@ -83,9 +83,12 @@ impl ModManagerWindowAddNewGame {
}
pub fn setup(&self) {
let games = games::get_games();
let games_strs: Vec<&str> = games.iter().map(|s| s.as_str()).collect();
let games_list = &gtk::StringList::new(&games_strs);
let settings = ModManagerSettings::default();
let supported_games = settings.get_supported_games();
let games_strs: Vec<String> = supported_games.iter().map(|s| s.to_name()).collect();
let games_list =
&gtk::StringList::new(&games_strs.iter().map(|s| s.as_str()).collect::<Vec<&str>>());
let obj = self.imp();
@ -94,18 +97,13 @@ impl ModManagerWindowAddNewGame {
let instance = self;
obj.complete_button
.connect_clicked(clone!(@strong instance, @strong games_list => move |_| {
.connect_clicked(clone!(@strong instance, @strong games_list, @strong settings => move |_| {
//let selected_game = games_dropdown.selected_item();
let settings = ModManagerSettings::default();
let game_selected = games_list.string(instance.imp().games_dropdown.selected()).unwrap().to_string();
println!("complete button clicked, selected game: {:?}", &game_selected);
let mut selected = settings.games().to_vec();
selected.push(game_selected);
settings.set_strv("games", selected);
settings.add_managed_game(Game::from_name(game_selected, "".to_string()));
//if let Some(selected_game) = selected_game {
instance.hide()

View file

@ -21,15 +21,14 @@ use gtk::{gio, glib};
use tracing::{debug, info};
use crate::config::PROFILE;
use crate::settings::ModManagerSettings;
use crate::windows::GamesAndMods;
use super::Welcome;
use crate::settings::ModManagerSettings;
use gtk::glib::clone;
mod imp {
use gtk::glib::clone;
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate)]
@ -62,10 +61,10 @@ mod imp {
}
let settings = ModManagerSettings::default();
settings.connect_games_changed(clone!(@strong obj, @strong settings => move |_| {
settings.on_managed_games_changed(clone!(@strong obj, @strong settings => move || {
info!("Games changed, deciding on initial page.");
let page: gtk::Widget = if settings.games().len() > 0 {
let page: gtk::Widget = if settings.get_managed_games().len() > 0 {
GamesAndMods::new().upcast()
} else {
Welcome::new().upcast()
@ -74,7 +73,7 @@ mod imp {
obj.set_property("content", page);
}));
let page: gtk::Widget = if settings.games().len() > 0 {
let page: gtk::Widget = if settings.get_managed_games().len() > 0 {
GamesAndMods::new().upcast()
} else {
Welcome::new().upcast()

View file

@ -25,7 +25,11 @@ mod imp {
use gtk::glib::clone;
use tracing::info;
use crate::{api::games, settings::ModManagerSettings, windows::ModManagerWindowAddNewGame};
use crate::{
api::{games, get_game_id, get_mods},
settings::{Game, ModManagerSettings},
windows::ModManagerWindowAddNewGame,
};
use super::*;
@ -40,6 +44,9 @@ mod imp {
#[template_child]
pub games_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub mods_list: TemplateChild<gtk::ListBox>,
}
#[glib::object_subclass]
@ -64,7 +71,7 @@ mod imp {
self.remove_all_games.connect_clicked(|_| {
let settings = ModManagerSettings::default();
settings.set_games(&[]);
settings.remove_managed_games();
println!("Remove all games button clicked");
});
@ -74,23 +81,40 @@ mod imp {
let settings = ModManagerSettings::default();
for game in settings.games() {
info!("Adding game {} to list", game);
for game in settings.get_managed_games() {
info!("Adding game {:?} to list", game);
let row = adw::ActionRow::new();
row.set_title(&game);
row.set_title(&game.to_name());
obj.imp().games_list.append(&row);
}
settings.connect_games_changed(clone!(@weak obj, @strong settings => move |_| {
settings.on_managed_games_changed(clone!(@weak obj, @strong settings => move || {
info!("Games changed, modifying list");
obj.imp().games_list.remove_all();
for game in settings.games() {
info!("Adding game {} to list", game);
for game in settings.get_managed_games() {
info!("Adding game {:?} to list", game);
let row = adw::ActionRow::new();
row.set_title(&game);
row.set_title(&game.to_name());
obj.imp().games_list.append(&row);
}
}));
let mods = get_mods(&get_game_id(Game::TheSims4("".to_string())))
.iter()
.for_each(|mod_| {
let card = gtk::Box::new(gtk::Orientation::Vertical, 10);
card.set_css_classes(&["card"]);
let title = gtk::Label::new(Some(&mod_.name));
title.set_css_classes(&["heading"]);
let description = gtk::Label::new(Some(&mod_.summary));
description.set_css_classes(&["body"]);
card.append(&title);
card.append(&description);
obj.imp().mods_list.append(&card);
});
}
}
impl WidgetImpl for GamesAndMods {}