Skip to content

Commit

Permalink
Save profiles to disk without having device or profile IDs in context…
Browse files Browse the repository at this point in the history
… fields
  • Loading branch information
ninjadev64 committed Aug 16, 2024
1 parent 8e32628 commit a284266
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 16 deletions.
5 changes: 4 additions & 1 deletion src-tauri/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,12 @@ impl std::fmt::Display for ActionContext {
}

impl std::str::FromStr for ActionContext {
type Err = std::num::ParseIntError;
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let segments: Vec<&str> = s.split('.').collect();
if segments.len() < 5 {
return Err(anyhow::anyhow!("not enough segments"));
}
let device = segments[0].to_owned();
let profile = segments[1].to_owned();
let controller = segments[2].to_owned();
Expand Down
38 changes: 32 additions & 6 deletions src-tauri/src/store/mod.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,54 @@
pub mod profiles;
mod simplified_context;

use std::fs;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

pub trait FromAndIntoDiskValue
where
Self: Sized,
{
#[allow(clippy::wrong_self_convention)]
fn into_value(&self) -> Result<serde_json::Value, serde_json::Error>;
fn from_value(_: serde_json::Value, _: &Path) -> Result<Self, serde_json::Error>;
}

pub trait NotProfile {}

impl<T> FromAndIntoDiskValue for T
where
T: Serialize + for<'a> Deserialize<'a> + NotProfile,
{
fn into_value(&self) -> Result<serde_json::Value, serde_json::Error> {
serde_json::to_value(self)
}
fn from_value(value: serde_json::Value, _: &Path) -> Result<T, serde_json::Error> {
serde_json::from_value(value)
}
}

/// Allows for easy persistence of values using JSON files.
pub struct Store<T>
where
T: Serialize + for<'a> Deserialize<'a>,
T: FromAndIntoDiskValue,
{
pub value: T,
path: PathBuf,
}

impl<T> Store<T>
where
T: Serialize + for<'a> Deserialize<'a>,
T: FromAndIntoDiskValue,
{
/// Create a new Store given an ID and storage directory.
pub fn new(id: &str, config_dir: &PathBuf, default: T) -> Result<Self, anyhow::Error> {
pub fn new(id: &str, config_dir: &Path, default: T) -> Result<Self, anyhow::Error> {
let path = config_dir.join(format!("{}.json", id));

if path.exists() {
let file_contents = fs::read(&path)?;
let existing_value: T = serde_json::from_slice(&file_contents)?;
let existing_value: T = T::from_value(serde_json::from_slice(&file_contents)?, &path)?;

Ok(Self { path, value: existing_value })
} else {
Expand All @@ -35,7 +59,7 @@ where
/// Save the relevant Store as a file.
pub fn save(&self) -> Result<(), anyhow::Error> {
fs::create_dir_all(self.path.parent().unwrap())?;
fs::write(&self.path, serde_json::to_string_pretty(&self.value)?)?;
fs::write(&self.path, serde_json::to_string_pretty(&T::into_value(&self.value)?)?)?;
Ok(())
}
}
Expand All @@ -62,6 +86,8 @@ impl Default for Settings {
}
}

impl NotProfile for Settings {}

pub fn get_settings(app_handle: &tauri::AppHandle) -> Result<Store<Settings>, anyhow::Error> {
Store::new("settings", &app_handle.path_resolver().app_config_dir().unwrap(), Settings::default())
}
28 changes: 19 additions & 9 deletions src-tauri/src/store/profiles.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use super::Store;
use super::{
simplified_context::{DiskActionInstance, DiskProfile},
Store,
};
use crate::shared::{Action, ActionInstance, ActionState, Profile};

use std::collections::HashMap;
Expand Down Expand Up @@ -92,6 +95,8 @@ pub struct DeviceConfig {
pub selected_profile: String,
}

impl super::NotProfile for DeviceConfig {}

pub struct DeviceStores {
stores: HashMap<String, Store<DeviceConfig>>,
}
Expand Down Expand Up @@ -139,24 +144,25 @@ impl DeviceStores {
}

#[derive(Deserialize)]
#[allow(dead_code)]
struct ProfileV1 {
id: String,
keys: Vec<Vec<ActionInstance>>,
sliders: Vec<Vec<ActionInstance>>,
}

impl From<ProfileV1> for Profile {
impl From<ProfileV1> for DiskProfile {
fn from(val: ProfileV1) -> Self {
let mut keys = vec![];
for slot in val.keys {
if slot.len() == 1 {
keys.push(Some(slot[0].clone()));
keys.push(Some(slot[0].clone().into()));
} else if !slot.is_empty() {
let mut children = slot.clone();
for child in &mut children {
child.context.index += 1;
}
keys.push(Some(ActionInstance {
keys.push(Some(DiskActionInstance {
action: Action {
name: "Multi Action".to_owned(),
uuid: "com.amansprojects.opendeck.multiaction".to_owned(),
Expand All @@ -174,14 +180,14 @@ impl From<ProfileV1> for Profile {
..Default::default()
}],
},
context: slot[0].context.clone(),
context: slot[0].context.clone().into(),
states: vec![ActionState {
image: "opendeck/multi-action.png".to_owned(),
..Default::default()
}],
current_state: 0,
settings: serde_json::Value::Object(serde_json::Map::new()),
children: Some(children),
children: Some(children.into_iter().map(|v| v.into()).collect()),
}));
} else {
keys.push(None);
Expand All @@ -190,12 +196,12 @@ impl From<ProfileV1> for Profile {
let mut sliders = vec![];
for slot in val.sliders {
if !slot.is_empty() {
sliders.push(Some(slot[0].clone()));
sliders.push(Some(slot[0].clone().into()));
} else {
sliders.push(None);
}
}
Self { id: val.id, keys, sliders }
Self { keys, sliders }
}
}

Expand All @@ -205,12 +211,16 @@ impl From<ProfileV1> for Profile {
enum ProfileVersions {
V1(ProfileV1),
V2(Profile),
V3(DiskProfile),
}

fn migrate_profile(path: PathBuf) -> Result<(), anyhow::Error> {
let profile = serde_json::from_slice(&fs::read(&path)?)?;
if let ProfileVersions::V1(v1) = profile {
let migrated: Profile = v1.into();
let migrated: DiskProfile = v1.into();
fs::write(path, serde_json::to_string_pretty(&migrated)?)?;
} else if let ProfileVersions::V2(v2) = profile {
let migrated: DiskProfile = (&v2).into();
fs::write(path, serde_json::to_string_pretty(&migrated)?)?;
}
Ok(())
Expand Down
139 changes: 139 additions & 0 deletions src-tauri/src/store/simplified_context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! Duplicates of many structs to facilitate saving profiles to disk without having device or profile IDs in context fields.

use crate::shared::{Action, ActionContext, ActionInstance, ActionState, Profile};

use std::path::Path;

use serde::{Deserialize, Serialize};

#[derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
pub struct DiskActionContext {
pub controller: String,
pub position: u8,
pub index: u16,
}

impl std::fmt::Display for DiskActionContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.controller, self.position, self.index)
}
}

impl std::str::FromStr for DiskActionContext {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let segments: Vec<&str> = s.split('.').collect();
let mut offset: usize = 0;
if segments.len() == 5 {
offset = 2;
}
let controller = segments[offset].to_owned();
let position = u8::from_str(segments[1 + offset])?;
let index = u16::from_str(segments[2 + offset])?;
Ok(Self { controller, position, index })
}
}

impl From<ActionContext> for DiskActionContext {
fn from(value: ActionContext) -> Self {
Self {
controller: value.controller,
position: value.position,
index: value.index,
}
}
}

impl DiskActionContext {
fn into_action_context(self, path: &Path) -> ActionContext {
let config_dir = crate::APP_HANDLE.get().unwrap().path_resolver().app_config_dir().unwrap();
let mut iter = path.strip_prefix(config_dir).unwrap().iter();
let device = iter.nth(1).unwrap().to_string_lossy().into_owned();
let mut profile = iter.map(|x| x.to_string_lossy()).collect::<Vec<_>>().join("/");
profile = profile[..profile.len() - 5].to_owned();
ActionContext {
device,
profile,
controller: self.controller,
position: self.position,
index: self.index,
}
}
}

#[derive(Serialize, Deserialize)]
pub struct DiskActionInstance {
pub action: Action,
pub context: DiskActionContext,
pub states: Vec<ActionState>,
pub current_state: u16,
pub settings: serde_json::Value,
pub children: Option<Vec<DiskActionInstance>>,
}

impl From<ActionInstance> for DiskActionInstance {
fn from(value: ActionInstance) -> Self {
Self {
context: value.context.into(),
action: value.action,
states: value.states,
current_state: value.current_state,
settings: value.settings,
children: value.children.map(|c| c.into_iter().map(|v| v.into()).collect()),
}
}
}

impl DiskActionInstance {
fn into_action_instance(self, path: &Path) -> ActionInstance {
ActionInstance {
context: self.context.into_action_context(path),
action: self.action,
states: self.states,
current_state: self.current_state,
settings: self.settings,
children: self.children.map(|c| c.into_iter().map(|v| v.into_action_instance(path)).collect()),
}
}
}

#[derive(Serialize, Deserialize)]
pub struct DiskProfile {
pub keys: Vec<Option<DiskActionInstance>>,
pub sliders: Vec<Option<DiskActionInstance>>,
}

impl From<&Profile> for DiskProfile {
fn from(value: &Profile) -> Self {
Self {
keys: value.keys.clone().into_iter().map(|x| x.map(|v| v.into())).collect(),
sliders: value.sliders.clone().into_iter().map(|x| x.map(|v| v.into())).collect(),
}
}
}

impl DiskProfile {
fn into_profile(self, path: &Path) -> Profile {
let config_dir = crate::APP_HANDLE.get().unwrap().path_resolver().app_config_dir().unwrap();
let mut iter = path.strip_prefix(config_dir).unwrap().iter();
let _ = iter.nth(1);
let mut id = iter.map(|x| x.to_string_lossy()).collect::<Vec<_>>().join("/");
id = id[..id.len() - 5].to_owned();
Profile {
id,
keys: self.keys.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(),
sliders: self.sliders.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(),
}
}
}

impl super::FromAndIntoDiskValue for Profile {
fn into_value(&self) -> Result<serde_json::Value, serde_json::Error> {
let disk: DiskProfile = self.into();
serde_json::to_value(disk)
}
fn from_value(value: serde_json::Value, path: &Path) -> Result<Profile, serde_json::Error> {
let disk: DiskProfile = serde_json::from_value(value)?;
Ok(disk.into_profile(path))
}
}

0 comments on commit a284266

Please sign in to comment.