From 7a36a00aaa50eca99f35209fe6421ec4d7dbcfc4 Mon Sep 17 00:00:00 2001 From: CEbbinghaus Date: Sun, 22 Sep 2024 15:39:45 +1000 Subject: [PATCH 1/8] Added API to modify config properties # Conflicts: # backend/src/log.rs --- backend/src/api.rs | 26 +++++++--- backend/src/cfg.rs | 113 ++++++++++++++++++++++++++++++++++++++----- backend/src/ds.rs | 7 +-- backend/src/env.rs | 5 ++ backend/src/log.rs | 15 ++++-- backend/src/main.rs | 33 +++++++++---- backend/src/steam.rs | 1 + backend/src/watch.rs | 7 ++- 8 files changed, 170 insertions(+), 37 deletions(-) diff --git a/backend/src/api.rs b/backend/src/api.rs index 6b303d5..b763c4e 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -1,13 +1,8 @@ use crate::{ - ds::Store, - dto::{CardEvent, Game, MicroSDCard}, - env::PACKAGE_VERSION, - err::Error, - event::Event, - sdcard::{get_card_cid, is_card_inserted}, + cfg::CONFIG, ds::Store, dto::{CardEvent, Game, MicroSDCard}, env::PACKAGE_VERSION, err::Error, event::Event, sdcard::{get_card_cid, is_card_inserted} }; use actix_web::{ - delete, get, http::StatusCode, post, web, Either, HttpResponse, HttpResponseBuilder, Responder, + delete, get, http::StatusCode, post, web::{self, Bytes}, Either, HttpResponse, HttpResponseBuilder, Responder, Result, }; use futures::StreamExt; @@ -23,6 +18,8 @@ pub(crate) fn config(cfg: &mut web::ServiceConfig) { .service(version) .service(listen) .service(save) + .service(get_setting_by_name) + .service(set_setting_by_name) .service(get_current_card) .service(get_current_card_id) .service(get_current_card_and_games) @@ -75,6 +72,21 @@ pub(crate) async fn listen(sender: web::Data>) -> Result, datastore: web::Data>) -> Result { + let result = CONFIG.read().await.get_property(&name)?; + Ok(result) +} + +#[post("/setting/{name}")] +#[instrument] +pub(crate) async fn set_setting_by_name(body: Bytes, name: web::Path, datastore: web::Data>) -> Result { + let value = String::from_utf8(body.to_vec()).map_err(|_| Error::from_str("Unable to decode body as utf8"))?; + CONFIG.write().await.set_property(&name, &value)?; + Ok(HttpResponse::Ok()) +} + #[get("/list")] #[instrument(skip(datastore))] pub(crate) async fn list_cards_with_games(datastore: web::Data>) -> impl Responder { diff --git a/backend/src/cfg.rs b/backend/src/cfg.rs index 6c81568..5143515 100644 --- a/backend/src/cfg.rs +++ b/backend/src/cfg.rs @@ -1,6 +1,7 @@ use anyhow::Result; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; use std::{ fs::{self, File}, io::Write, @@ -8,15 +9,14 @@ use std::{ }; use tracing::Level; -use crate::DATA_DIR; +use crate::{err::Error, CONFIG_PATH}; lazy_static! { - pub static ref CONFIG_PATH: PathBuf = DATA_DIR.join("config.toml"); - pub static ref CONFIG: Config = Config::load().unwrap_or_else(|| { + pub static ref CONFIG: RwLock = RwLock::new(Config::load().unwrap_or_else(|| { let result = Config::new(); result.write().expect("Write to succeed"); result - }); + })); } #[allow(clippy::upper_case_acronyms)] @@ -30,26 +30,50 @@ pub enum LogLevel { ERROR = 4, } +#[derive(Serialize, Deserialize, Default)] +pub struct Startup { + pub skip_validate: bool, + pub skip_clean: bool, +} + +#[derive(Serialize, Deserialize, Default)] +pub struct Frontend { + pub dismissed_docs: bool, +} #[derive(Serialize, Deserialize)] -pub struct Config { +pub struct Backend { pub port: u16, pub scan_interval: u64, pub store_file: PathBuf, pub log_file: PathBuf, #[serde(with = "LogLevel")] pub log_level: Level, + pub startup: Startup, } -impl Config { - pub fn new() -> Self { - Config { +impl Default for Backend { + fn default() -> Self { + Backend { port: 12412, scan_interval: 5000, log_file: "microsdeck.log".into(), store_file: "store".into(), log_level: Level::INFO, + startup: Default::default(), } } +} + +#[derive(Serialize, Deserialize, Default)] +pub struct Config { + pub backend: Backend, + pub frontend: Frontend, +} + +impl Config { + pub fn new() -> Self { + Default::default() + } pub fn write(&self) -> Result<()> { self.write_to_file(&CONFIG_PATH) } @@ -66,11 +90,78 @@ impl Config { Self::load_from_file(&CONFIG_PATH) } pub fn load_from_file(path: &'_ PathBuf) -> Option { - fs::read_to_string(path) - .ok() - .and_then(|val| Self::load_from_str(&val).ok()) + let content = fs::read_to_string(path) + .ok(); + + if let Some(ref content) = content { + let result = Self::load_from_str(content); + + match result { + Ok(val) => return Some(val), + Err(ref err) => eprintln!("Unable to deserialize config: \"{}\"", err), + } + } else { + eprintln!("No content found at config path \"{}\"", path.to_string_lossy()); + } + None } pub fn load_from_str(content: &'_ str) -> Result { Ok(toml::de::from_str::(content)?) } } + + +// TODO: Turn this Impl into a macro that generates the get and set functions +impl Config { + pub fn get_property(&self, name: &'_ str) -> Result { + let parts: Vec<&str> = name.split(":").collect(); + + match parts[..] { + ["backend", "port"] => Ok(self.backend.port.to_string()), + ["backend", "scan_interval"] => Ok(self.backend.scan_interval.to_string()), + ["backend", "store_file"] => Ok(self.backend.store_file.to_string_lossy().to_string()), + ["backend", "log_file"] => Ok(self.backend.log_file.to_string_lossy().to_string()), + ["backend", "log_level"] => Ok(self.backend.log_level.to_string()), + ["backend", "startup", "skip_validate"] => Ok(self.backend.startup.skip_validate.to_string()), + ["backend", "startup", "skip_clean"] => Ok(self.backend.startup.skip_clean.to_string()), + ["frontend", "dismissed_docs"] => Ok(self.frontend.dismissed_docs.to_string()), + _ => Err(Error::from_str("Invalid property Name")), + } + } + + pub fn set_property(&mut self, name: &'_ str, value: &'_ str) -> Result<(), Error> { + let parts: Vec<&str> = name.split(":").collect(); + + let wrong_value_err = Error::from_str(&format!("The value provided \"{value}\" did not match the expected type")); + + match parts[..] { + ["backend", "port"] => { + self.backend.port = value.parse().map_err(|_| wrong_value_err)?; + } + ["backend", "scan_interval"] => { + self.backend.scan_interval = value.parse().map_err(|_| wrong_value_err)?; + } + ["backend", "store_file"] => { + self.backend.store_file = value.into(); + } + ["backend", "log_file"] => { + self.backend.log_file = value.into(); + } + ["backend", "log_level"] => { + self.backend.log_level = value.parse().map_err(|_| wrong_value_err)?; + } + ["backend", "startup", "skip_validate"] => { + self.backend.startup.skip_validate = value.parse().map_err(|_| wrong_value_err)?; + } + ["backend", "startup", "skip_clean"] => { + self.backend.startup.skip_clean = value.parse().map_err(|_| wrong_value_err)?; + } + ["frontend", "dismissed_docs"] => { + self.frontend.dismissed_docs = value.parse().map_err(|_| wrong_value_err)?; + } + _ => return Err(Error::from_str("Invalid property Name")), + } + + self.write().map_err(|err| Error::from_str(&err.to_string())) + } +} \ No newline at end of file diff --git a/backend/src/ds.rs b/backend/src/ds.rs index 476cbd0..1c3a3ca 100644 --- a/backend/src/ds.rs +++ b/backend/src/ds.rs @@ -74,7 +74,7 @@ pub struct StoreData { hashes: HashMap, } -impl StoreData { +impl StoreData { #[instrument(skip(self))] pub fn add_card(&mut self, id: String, card: MicroSDCard) { self.node_ids @@ -389,10 +389,11 @@ impl Store { result } - /// Removes any whitespace from the card uid + /// cleans up any data to make it compliant pub fn clean_up(&self) { let mut data = self.data.write().unwrap(); - + + // Removes any whitespace from the card uid let cleaned_node_ids: HashMap = data .node_ids .iter() diff --git a/backend/src/env.rs b/backend/src/env.rs index ff440b0..52d64b7 100644 --- a/backend/src/env.rs +++ b/backend/src/env.rs @@ -24,6 +24,11 @@ lazy_static! { TEMPDIR.to_string() + "/log" } }); + + pub static ref CONFIG_PATH: PathBuf = match std::env::var("DECKY_CONFIG_PATH") { + Ok(loc) => PathBuf::from(loc), + Err(_) => DATA_DIR.join("config.toml") + }; } pub fn get_file_path_and_create_directory( diff --git a/backend/src/log.rs b/backend/src/log.rs index 50821b1..860e23b 100644 --- a/backend/src/log.rs +++ b/backend/src/log.rs @@ -13,9 +13,14 @@ const IGNORED_MODULES: [&'static str; 6] = [ "mio::poll", ]; -pub fn create_subscriber() { - let log_file_path = get_file_path_and_create_directory(&CONFIG.log_file, &LOG_DIR) - .expect("Log file to be created"); +pub async fn create_subscriber() { + let (log_file, log_level) = { + let config = CONFIG.read().await; + (config.backend.log_file.clone(), config.backend.log_level) + }; + + let log_file_path = + get_file_path_and_create_directory(&log_file, &LOG_DIR).expect("Log file to be created"); let file = std::fs::OpenOptions::new() .create(true) @@ -27,7 +32,7 @@ pub fn create_subscriber() { .json() .with_writer(file) .with_filter(tracing_subscriber::filter::LevelFilter::from_level( - CONFIG.log_level, + log_level, )) .with_filter(filter::filter_fn(|metadata| { metadata @@ -43,7 +48,7 @@ pub fn create_subscriber() { tracing_subscriber::fmt::layer() .pretty() .with_filter(tracing_subscriber::filter::LevelFilter::from_level( - CONFIG.log_level, + log_level, )) .with_filter(filter::filter_fn(|metadata| { metadata diff --git a/backend/src/main.rs b/backend/src/main.rs index aff854f..3364bc1 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -25,14 +25,14 @@ use std::sync::Arc; use tokio::sync::broadcast::{self, Sender}; use tracing::{debug, error, info}; -pub fn init() { - create_subscriber(); +pub async fn init() { + create_subscriber().await; } type MainResult = Result<(), Error>; -async fn run_web_server(datastore: Arc, sender: Sender) -> MainResult { - info!("Starting HTTP server..."); +async fn run_web_server(port: u16, datastore: Arc, sender: Sender) -> MainResult { + info!("Starting HTTP server on port {port}..."); HttpServer::new(move || { let cors = Cors::default() @@ -49,7 +49,7 @@ async fn run_web_server(datastore: Arc, sender: Sender) -> Mai .configure(config) }) .workers(2) - .bind(("0.0.0.0", CONFIG.port))? + .bind(("0.0.0.0", port))? .run() .await .map_err(|err| err.into()) @@ -61,16 +61,26 @@ async fn main() { std::env::set_var("RUST_BACKTRACE", "1"); } - init(); + init().await; info!( version = PACKAGE_VERSION, "{}@{} by {}", PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_AUTHORS ); + let (store_file, skip_clean, skip_validate, port) = { + let config = CONFIG.read().await; + ( + config.backend.store_file.clone(), + config.backend.startup.skip_clean, + config.backend.startup.skip_validate, + config.backend.port, + ) + }; + let store_path = PathBuf::from( &std::env::var("STORE_PATH").map(PathBuf::from).unwrap_or( - get_file_path_and_create_directory(&CONFIG.store_file, &DATA_DIR) + get_file_path_and_create_directory(&store_file, &DATA_DIR) .expect("should retrieve data directory"), ), ); @@ -79,9 +89,12 @@ async fn main() { let store: Arc = Arc::new(Store::read_from_file(store_path.clone()).unwrap_or(Store::new(Some(store_path)))); - store.clean_up(); + if !skip_clean { + store.clean_up(); + } + - if !store.validate() { + if !skip_validate && !store.validate() { error!("Validity of the data is not guaranteed. Cannot run backend..."); exit(1); } @@ -91,7 +104,7 @@ async fn main() { let (txtx, _) = broadcast::channel::(1); - let server_future = run_web_server(store.clone(), txtx.clone()).fuse(); + let server_future = run_web_server(port, store.clone(), txtx.clone()).fuse(); let watch_future = start_watch(store.clone(), txtx.clone()).fuse(); diff --git a/backend/src/steam.rs b/backend/src/steam.rs index d65a87b..5f7434c 100644 --- a/backend/src/steam.rs +++ b/backend/src/steam.rs @@ -10,6 +10,7 @@ pub struct LibraryFolder { pub label: String, } +#[allow(dead_code)] #[serde_alias(CamelCase, PascalCase, LowerCase, SnakeCase)] #[derive(Deserialize)] pub struct AppState { diff --git a/backend/src/watch.rs b/backend/src/watch.rs index 18af9c9..d73470f 100644 --- a/backend/src/watch.rs +++ b/backend/src/watch.rs @@ -137,7 +137,12 @@ fn find_mount_name() -> Result, Error> { } pub async fn start_watch(datastore: Arc, sender: Sender) -> Result<(), Error> { - let mut interval = interval(Duration::from_millis(CONFIG.scan_interval)); + let scan_interval = { + let config = CONFIG.read().await; + config.backend.scan_interval + }; + + let mut interval = interval(Duration::from_millis(scan_interval)); let mut card_inserted = false; From 2f5d831ed06b649880420ea25bab5a1ca9d4d148 Mon Sep 17 00:00:00 2001 From: CEbbinghaus Date: Mon, 9 Sep 2024 01:16:51 +1000 Subject: [PATCH 2/8] Added prior art to comment --- backend/src/cfg.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/cfg.rs b/backend/src/cfg.rs index 5143515..c05b89a 100644 --- a/backend/src/cfg.rs +++ b/backend/src/cfg.rs @@ -112,6 +112,7 @@ impl Config { // TODO: Turn this Impl into a macro that generates the get and set functions +// Possibly using https://github.com/0xDEADFED5/set_field as the base impl Config { pub fn get_property(&self, name: &'_ str) -> Result { let parts: Vec<&str> = name.split(":").collect(); From 1487f074dc1adabf0b037c10bdce659c99550ba5 Mon Sep 17 00:00:00 2001 From: CEbbinghaus Date: Sun, 27 Oct 2024 13:13:55 +1100 Subject: [PATCH 3/8] Updated mise tool version --- .mise.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mise.toml b/.mise.toml index c73ac2d..80d3718 100644 --- a/.mise.toml +++ b/.mise.toml @@ -2,7 +2,7 @@ # specify single or multiple versions pnpm = '9.5.0' node = '22.4.0' -rust = '1.79.0' +rust = "1.80.1" [tasks."build"] @@ -36,4 +36,4 @@ run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -o copy" [tasks."upload"] depends = ["build"] description = 'Upload MicroSDeck to the SteamDeck' -run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -o upload" \ No newline at end of file +run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -o upload" From acf0f1f67082c51ee14ac6ef842eb3a83969ba5c Mon Sep 17 00:00:00 2001 From: CEbbinghaus Date: Tue, 10 Sep 2024 00:56:19 +1000 Subject: [PATCH 4/8] Added frontend Docs prompt and JSON support for config api --- .mise.toml | 4 ++-- backend/src/cfg.rs | 16 +++++++++++++ lib/src/MicoSDeck.ts | 23 +++++++++++++++---- lib/src/backend.ts | 29 +++++++++++++++++++++++- lib/src/components/MicroSDeckContext.tsx | 3 ++- lib/src/types.ts | 5 ++++ src/index.tsx | 22 +++++++++++++++++- 7 files changed, 92 insertions(+), 10 deletions(-) diff --git a/.mise.toml b/.mise.toml index 80d3718..3340585 100644 --- a/.mise.toml +++ b/.mise.toml @@ -18,8 +18,8 @@ outputs = ['build/bin/backend'] [tasks."build:frontend"] description = 'Build the Frontend' run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -o frontend" -sources = ['package.json', 'lib/package.json', '{src,lib}/**/*.{ts,tsx,codegen}'] -outputs = ['dist/index.js'] +# sources = ['package.json', 'lib/package.json', '{src,lib}/**/*.*'] +# outputs = ['dist/index.js'] [tasks."build:collect"] depends = ["build:backend", "build:frontend"] diff --git a/backend/src/cfg.rs b/backend/src/cfg.rs index c05b89a..01a8126 100644 --- a/backend/src/cfg.rs +++ b/backend/src/cfg.rs @@ -118,13 +118,17 @@ impl Config { let parts: Vec<&str> = name.split(":").collect(); match parts[..] { + ["*"] => Ok(serde_json::to_string(&self).unwrap()), + ["backend"] => Ok(serde_json::to_string(&self.backend).unwrap()), ["backend", "port"] => Ok(self.backend.port.to_string()), ["backend", "scan_interval"] => Ok(self.backend.scan_interval.to_string()), ["backend", "store_file"] => Ok(self.backend.store_file.to_string_lossy().to_string()), ["backend", "log_file"] => Ok(self.backend.log_file.to_string_lossy().to_string()), ["backend", "log_level"] => Ok(self.backend.log_level.to_string()), + ["backend", "startup"] => Ok(serde_json::to_string(&self.backend.startup).unwrap()), ["backend", "startup", "skip_validate"] => Ok(self.backend.startup.skip_validate.to_string()), ["backend", "startup", "skip_clean"] => Ok(self.backend.startup.skip_clean.to_string()), + ["frontend"] => Ok(serde_json::to_string(&self.frontend).unwrap()), ["frontend", "dismissed_docs"] => Ok(self.frontend.dismissed_docs.to_string()), _ => Err(Error::from_str("Invalid property Name")), } @@ -136,6 +140,12 @@ impl Config { let wrong_value_err = Error::from_str(&format!("The value provided \"{value}\" did not match the expected type")); match parts[..] { + ["*"] => { + *self = serde_json::from_str(value).map_err(|_| wrong_value_err)?; + } + ["backend"] => { + self.backend = serde_json::from_str(value).map_err(|_| wrong_value_err)?; + } ["backend", "port"] => { self.backend.port = value.parse().map_err(|_| wrong_value_err)?; } @@ -151,12 +161,18 @@ impl Config { ["backend", "log_level"] => { self.backend.log_level = value.parse().map_err(|_| wrong_value_err)?; } + ["backend", "startup"] => { + self.backend.startup = serde_json::from_str(value).map_err(|_| wrong_value_err)?; + } ["backend", "startup", "skip_validate"] => { self.backend.startup.skip_validate = value.parse().map_err(|_| wrong_value_err)?; } ["backend", "startup", "skip_clean"] => { self.backend.startup.skip_clean = value.parse().map_err(|_| wrong_value_err)?; } + ["frontend"] => { + self.frontend = serde_json::from_str(value).map_err(|_| wrong_value_err)?; + } ["frontend", "dismissed_docs"] => { self.frontend.dismissed_docs = value.parse().map_err(|_| wrong_value_err)?; } diff --git a/lib/src/MicoSDeck.ts b/lib/src/MicoSDeck.ts index 3476b8c..3b14175 100644 --- a/lib/src/MicoSDeck.ts +++ b/lib/src/MicoSDeck.ts @@ -1,6 +1,6 @@ -import { Event as BackendEvent, EventType, fetchCardsAndGames, fetchCardsForGame, fetchCreateGame, fetchCurrentCardAndGames, fetchDeleteCard, fetchEventTarget, fetchHealth, fetchLinkCardAndGame, fetchLinkCardAndManyGames, fetchUnlinkCardAndGame, fetchUnlinkCardAndManyGames, fetchUpdateCard, fetchVersion } from "./backend.js"; +import { Event as BackendEvent, EventType, fetchCardsAndGames, fetchCardsForGame, fetchCreateGame, fetchCurrentCardAndGames, fetchDeleteCard, fetchEventTarget, fetchGetSetting, fetchHealth, fetchLinkCardAndGame, fetchLinkCardAndManyGames, fetchUnlinkCardAndGame, fetchUnlinkCardAndManyGames, fetchUpdateCard, fetchVersion } from "./backend.js"; import Logger from "lipe"; -import { CardAndGames, CardsAndGames, Game, MicroSDCard } from "./types.js" +import { CardAndGames, CardsAndGames, FrontendSettings, Game, MicroSDCard } from "./types.js" import semverParse from "semver/functions/parse" import semverEq from "semver/functions/eq.js" @@ -91,6 +91,11 @@ export class MicroSDeck { return this.cardsAndGames; } + private frontendSettings: FrontendSettings | undefined; + public get FrontendSettings() { + return this.frontendSettings; + } + private pollLock: any | undefined; private isDestructed = false; @@ -124,8 +129,12 @@ export class MicroSDeck { } this.backendVersion = await fetchVersion(this.fetchProps); - await this.fetchCurrent(); - await this.fetchCardsAndGames(); + await Promise.all([ + this.fetchCurrent(), + this.fetchCardsAndGames(), + this.fetchFrontendSettings(), + ]); + this.eventBus.dispatchEvent(new Event("update")); this.enabled = true; } @@ -136,13 +145,17 @@ export class MicroSDeck { async fetchCardsAndGames() { this.cardsAndGames = await fetchCardsAndGames(this.fetchProps) || this.cardsAndGames || []; } + async fetchFrontendSettings() { + this.frontendSettings = await fetchGetSetting({ ...this.fetchProps, setting_name: "frontend"}) || this.frontendSettings; + } getProps() { return { enabled: this.enabled, version: this.backendVersion, cardsAndGames: this.cardsAndGames, - currentCardAndGames: this.currentCardAndGames + currentCardAndGames: this.currentCardAndGames, + frontendSettings: this.frontendSettings } } diff --git a/lib/src/backend.ts b/lib/src/backend.ts index 577e167..4ee44b1 100644 --- a/lib/src/backend.ts +++ b/lib/src/backend.ts @@ -123,6 +123,33 @@ export async function fetchVersion({ url, logger }: FetchProps): Promise { + const result = await wrapFetch({ url: `${url}/setting/${setting_name}`, logger }); + return result && JSON.parse(result) || result; +} + +export async function fetchSetSetting({ url, logger, value, setting_name }: FetchProps & { setting_name: SettingNames, value: any }) { + await wrapFetch({ url: `${url}/setting/${setting_name}`, logger }, { + method: "POST", + ...ApplicationJsonHeaders, + body: JSON.stringify(value), + }); +} + export async function fetchDeleteCard({ url, logger, card }: FetchProps & { card: MicroSDCard }) { await wrapFetch({ url: `${url}/card/${card.uid}`, logger }, { method: "DELETE" }); } @@ -193,4 +220,4 @@ export async function fetchUnlinkCardAndManyGames({ url, logger, card_id, game_i ...ApplicationJsonHeaders, body: JSON.stringify({card_id, game_ids}), }); -} \ No newline at end of file +} diff --git a/lib/src/components/MicroSDeckContext.tsx b/lib/src/components/MicroSDeckContext.tsx index 2b654d7..7c88d2f 100644 --- a/lib/src/components/MicroSDeckContext.tsx +++ b/lib/src/components/MicroSDeckContext.tsx @@ -1,6 +1,6 @@ import { createContext, useContext, useEffect, useState } from "react"; import { MicroSDeck } from "../MicoSDeck.js"; -import { CardAndGames, CardsAndGames } from "../types.js"; +import { CardAndGames, CardsAndGames, FrontendSettings } from "../types.js"; const MicroSDeckContext = createContext(null as any); export const useMicroSDeckContext = () => useContext(MicroSDeckContext) || {}; @@ -12,6 +12,7 @@ interface ProviderProps { interface PublicMicroSDeck { currentCardAndGames: CardAndGames | undefined; cardsAndGames: CardsAndGames; + frontendSettings: FrontendSettings | undefined; } interface MicroSDeckContext extends PublicMicroSDeck { diff --git a/lib/src/types.ts b/lib/src/types.ts index a33c7aa..9b2b224 100644 --- a/lib/src/types.ts +++ b/lib/src/types.ts @@ -21,3 +21,8 @@ export type Game = { export type CardAndGames = [MicroSDCard, Game[]]; export type CardsAndGames = CardAndGames[]; + + +export type FrontendSettings = { + dismissed_docs: boolean +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index b9065dc..6ec58aa 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,6 +11,7 @@ import { } from "@decky/ui"; import { routerHook } from '@decky/api'; import { FaEllipsisH, FaSdCard, FaStar } from "react-icons/fa"; +import { GiHamburgerMenu } from "react-icons/gi"; import PatchAppScreen from "./patch/PatchAppScreen"; import { API_URL, DOCUMENTATION_PATH, UNNAMED_CARD_NAME } from "./const"; import { Logger } from "./Logging"; @@ -21,6 +22,7 @@ import { CardActionsContextMenu } from "./components/CardActions"; import { backend } from "../lib/src"; import { version as libVersion } from "../lib/src"; import { version } from "../package.json"; +import { fetchSetSetting } from "../lib/src/backend"; if (!IsMatchingSemver(libVersion, version)) { throw new Error("How the hell did we get here???"); @@ -52,7 +54,7 @@ function EditCardButton(props: EditCardButtonProps) { } function Content() { - const { currentCardAndGames, cardsAndGames, microSDeck } = useMicroSDeckContext(); + const { currentCardAndGames, cardsAndGames, microSDeck, frontendSettings } = useMicroSDeckContext(); const [currentCard] = currentCardAndGames || [undefined]; @@ -83,9 +85,27 @@ function Content() { return (); } + let docs_card = (<>); + + if (frontendSettings && frontendSettings.dismissed_docs === false) { + docs_card = ( + +
+ Open the documentation to learn how to use this plugin, For this use the context button +
+ { fetchSetSetting({ url: API_URL, logger: Logger, setting_name: "frontend:dismissed_docs", value: true }); }} + onOKActionDescription="Dismiss Docs Reminder">Dismiss +
+ ); + } + return ( <> { Navigation.CloseSideMenus(); Navigation.Navigate(DOCUMENTATION_PATH); }}> +
+ {docs_card}
Edit MicroSD Cards
From 6c5fe2152cc6e6f97bba52de9d238d38822fa514 Mon Sep 17 00:00:00 2001 From: CEbbinghaus Date: Tue, 10 Sep 2024 23:55:43 +1000 Subject: [PATCH 5/8] Fixed refresh & updated visuals --- lib/src/components/MicroSDeckContext.tsx | 8 +++-- src/index.tsx | 37 +++++++++++++++++------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/src/components/MicroSDeckContext.tsx b/lib/src/components/MicroSDeckContext.tsx index 7c88d2f..a8f5688 100644 --- a/lib/src/components/MicroSDeckContext.tsx +++ b/lib/src/components/MicroSDeckContext.tsx @@ -13,6 +13,7 @@ interface PublicMicroSDeck { currentCardAndGames: CardAndGames | undefined; cardsAndGames: CardsAndGames; frontendSettings: FrontendSettings | undefined; + refresh: () => void; } interface MicroSDeckContext extends PublicMicroSDeck { @@ -20,14 +21,17 @@ interface MicroSDeckContext extends PublicMicroSDeck { } export function MicroSDeckContextProvider({ children, microSDeck }: React.PropsWithChildren) { + var refresh = microSDeck.fetch.bind(microSDeck); const [publicState, setPublicState] = useState({ - ...microSDeck.getProps() + ...microSDeck.getProps(), + refresh }); useEffect(() => { function onUpdate() { setPublicState({ - ...microSDeck.getProps() + ...microSDeck.getProps(), + refresh }); } diff --git a/src/index.tsx b/src/index.tsx index 6ec58aa..f339010 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ import { definePlugin, DialogButton, + DialogCheckbox, Focusable, Navigation, PanelSection, @@ -15,7 +16,7 @@ import { GiHamburgerMenu } from "react-icons/gi"; import PatchAppScreen from "./patch/PatchAppScreen"; import { API_URL, DOCUMENTATION_PATH, UNNAMED_CARD_NAME } from "./const"; import { Logger } from "./Logging"; -import React from "react"; +import React, { useState } from "react"; import Docs from "./pages/Docs"; import { MicroSDeck, MicroSDeckContextProvider, useMicroSDeckContext, CardAndGames, MicroSDCard, IsMatchingSemver } from "../lib/src"; import { CardActionsContextMenu } from "./components/CardActions"; @@ -54,7 +55,9 @@ function EditCardButton(props: EditCardButtonProps) { } function Content() { - const { currentCardAndGames, cardsAndGames, microSDeck, frontendSettings } = useMicroSDeckContext(); + const { currentCardAndGames, cardsAndGames, microSDeck, frontendSettings, refresh } = useMicroSDeckContext(); + + const [dismiss_docs, setDismissDocs] = useState(frontendSettings?.dismissed_docs || false); const [currentCard] = currentCardAndGames || [undefined]; @@ -89,22 +92,34 @@ function Content() { if (frontendSettings && frontendSettings.dismissed_docs === false) { docs_card = ( - -
- Open the documentation to learn how to use this plugin, For this use the context button +
+
+
+

Check out the new Docs!

+ Open them using +
+ +
+
+ + { + if (dismiss_docs) { + refresh(); + fetchSetSetting({ url: API_URL, logger: Logger, setting_name: "frontend:dismissed_docs", value: dismiss_docs }); + } + Navigation.Navigate(DOCUMENTATION_PATH); + }} + onOKActionDescription="Dismiss Docs Reminder">Open Docs
- { fetchSetSetting({ url: API_URL, logger: Logger, setting_name: "frontend:dismissed_docs", value: true }); }} - onOKActionDescription="Dismiss Docs Reminder">Dismiss - +
); } return ( <> { Navigation.CloseSideMenus(); Navigation.Navigate(DOCUMENTATION_PATH); }}> -
{docs_card}
Edit MicroSD Cards From ccf86133d28acad71c17758e0c335b1289bfe4bd Mon Sep 17 00:00:00 2001 From: CEbbinghaus Date: Wed, 11 Sep 2024 00:26:50 +1000 Subject: [PATCH 6/8] Updated Styling --- src/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index f339010..7580a85 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -97,8 +97,10 @@ function Content() {

Check out the new Docs!

Open them using -
- +
+
+ +
@@ -118,7 +120,7 @@ function Content() { } return ( - <> +
{ Navigation.CloseSideMenus(); Navigation.Navigate(DOCUMENTATION_PATH); }}> {docs_card}
@@ -147,7 +149,7 @@ function Content() { )} - +
); }; From 2b78ced9aa53726ab60cddfb6ee9421351701207 Mon Sep 17 00:00:00 2001 From: CEbbinghaus Date: Sun, 15 Sep 2024 20:01:34 +1000 Subject: [PATCH 7/8] Removed unessecary label --- src/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 7580a85..90fe8fc 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -123,9 +123,6 @@ function Content() {
{ Navigation.CloseSideMenus(); Navigation.Navigate(DOCUMENTATION_PATH); }}> {docs_card} -
- Edit MicroSD Cards -
{isLoaded ? ( From 2ef4cadf6766e33e71b455ff1e642851f71ef365 Mon Sep 17 00:00:00 2001 From: CEbbinghaus Date: Sun, 22 Sep 2024 15:03:56 +1000 Subject: [PATCH 8/8] Added Traceing to settings api --- backend/src/api.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/api.rs b/backend/src/api.rs index b763c4e..1452ee1 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -74,14 +74,18 @@ pub(crate) async fn listen(sender: web::Data>) -> Result, datastore: web::Data>) -> Result { +pub(crate) async fn get_setting_by_name(name: web::Path) -> Result { + trace!("HTTP GET /setting/{name}"); + let result = CONFIG.read().await.get_property(&name)?; Ok(result) } #[post("/setting/{name}")] #[instrument] -pub(crate) async fn set_setting_by_name(body: Bytes, name: web::Path, datastore: web::Data>) -> Result { +pub(crate) async fn set_setting_by_name(body: Bytes, name: web::Path) -> Result { + trace!("HTTP POST /setting/{name}"); + let value = String::from_utf8(body.to_vec()).map_err(|_| Error::from_str("Unable to decode body as utf8"))?; CONFIG.write().await.set_property(&name, &value)?; Ok(HttpResponse::Ok())