initial commit

This commit is contained in:
Sebastian Dine
2022-12-02 14:37:07 +00:00
commit 28579efc80
74 changed files with 8978 additions and 0 deletions
@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
[package]
name = "card-collection-manager-2"
version = "1.0.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.57"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.1", features = ["api-all"] }
# b64 image encoding
base64 = "0.13"
image = "0.24"
# rest calls
reqwest = { version = "0.11", features = ["blocking", "json"] }
# iterator for enums
strum = "0.24"
strum_macros = "0.24"
# data type checker, e.g. is_digit
nom = "7.1.1"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = [ "custom-protocol" ]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ]
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
@@ -0,0 +1 @@
{"dataStorage":"/home/dev/cards","defaultGame":"Magic"}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666665"
version="1.1"
id="svg5"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="32x32.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="false"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="4.2179991"
inkscape:cx="22.285448"
inkscape:cy="66.619265"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#0000ff;stroke:#000000;stroke-width:0.680409"
id="rect184-5"
width="3.2237122"
height="4.821435"
x="-0.41053185"
y="2.2624261"
inkscape:transform-center-x="-0.99973824"
inkscape:transform-center-y="-1.3768086"
transform="matrix(0.93880917,-0.34443772,0.35090811,0.9364099,0,0)" />
<rect
style="fill:#ff0000;stroke:#000000;stroke-width:0.680409"
id="rect184-5-6"
width="3.2237122"
height="4.821435"
x="-8.2546825"
y="-0.7155177"
inkscape:transform-center-x="0.99973788"
inkscape:transform-center-y="-1.3768083"
transform="matrix(-0.93880917,-0.34443772,-0.35090811,0.9364099,0,0)" />
<ellipse
style="fill:#ff0000;stroke:#000000;stroke-width:0.529006"
id="path494"
cx="1.1477517"
cy="4.4643512"
rx="0.47794324"
ry="0.67525065"
transform="matrix(0.93935269,-0.34295266,0.34940282,0.93697261,0,0)" />
<ellipse
style="fill:#0000ff;stroke:#000000;stroke-width:0.529006"
id="path494-0"
cx="-6.792244"
cy="1.4968817"
rx="0.47794324"
ry="0.67525065"
transform="matrix(-0.93935269,-0.34295266,-0.34940282,0.93697261,0,0)" />
<rect
style="fill:#803300;stroke:#000000;stroke-width:0.680401"
id="rect184"
width="3.517828"
height="5.2026267"
x="2.495986"
y="1.8122036" />
<ellipse
style="fill:#ffff00;stroke:#000000;stroke-width:0.528999"
id="path440"
cx="4.2621508"
cy="4.2843785"
rx="0.47854421"
ry="0.63851511" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="128"
height="128"
viewBox="0 0 33.866666 33.866666"
version="1.1"
id="svg5"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="false"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="4.2179991"
inkscape:cx="22.285448"
inkscape:cy="66.619265"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#0000ff;stroke:#000000;stroke-width:0.6804"
id="rect184-5"
width="13.962224"
height="21.0504"
x="-2.3585024"
y="7.8501358"
inkscape:transform-center-x="-4.3244808"
inkscape:transform-center-y="-6.0189139"
transform="rotate(-20.344379)" />
<rect
style="fill:#ff0000;stroke:#000000;stroke-width:0.6804"
id="rect184-5-6"
width="13.962224"
height="21.0504"
x="-33.98983"
y="-4.2830024"
inkscape:transform-center-x="4.3244806"
inkscape:transform-center-y="-6.0189138"
transform="matrix(-0.93761993,-0.347662,-0.347662,0.93761993,0,0)" />
<ellipse
style="fill:#ff0000;stroke:#000000;stroke-width:0.529"
id="path494"
cx="4.3872981"
cy="17.464888"
rx="2.069998"
ry="2.948179"
transform="rotate(-20.253039)" />
<ellipse
style="fill:#0000ff;stroke:#000000;stroke-width:0.529"
id="path494-0"
cx="-27.657389"
cy="5.3736062"
rx="2.069998"
ry="2.948179"
transform="matrix(-0.93817298,-0.34616682,-0.34616682,0.93817298,0,0)" />
<rect
style="fill:#803300;stroke:#000000;stroke-width:0.6804"
id="rect184"
width="15.216769"
height="22.744036"
x="9.5475092"
y="6.2229657" />
<ellipse
style="fill:#ffff00;stroke:#000000;stroke-width:0.529"
id="path440"
cx="17.187256"
cy="17.030437"
rx="2.069998"
ry="2.7913611" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,139 @@
use serde::{Serialize, Deserialize};
use crate::util::enums::{Language, Condition};
use crate::util::collection::MapEntryWithId;
use crate::util::fs::parse_index_from_filename;
use super::set_services::Set;
use crate::templates;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Card {
pub id: u32,
pub amount: u8,
pub name: String,
pub set: Set,
pub note: String,
pub images: Vec<String>,
pub language: Language,
pub condition: Condition,
pub foil: bool,
pub signed: bool,
pub altered: bool
}
/// implementation of the Card struct that is used by templates
/// to enable generic handling.
impl MapEntryWithId for Card {
fn set_id(&mut self, id: u32) {
self.id = id;
}
fn get_id(&self) -> u32 {
self.id
}
}
/// Parse the provided JSON object into an instance of `Card`, store it into
/// the Magic collection hashmap and return the id of the new entry.
///
/// # Argument
/// obj - Magic card instance as JSON object.
///
/// # Returns
/// New id of the entry withing the collection
///
pub fn add_card<'a>(obj: &str) -> Result<u32, &'a str> {
templates::card_service_templates::add_entry_to_collection::<Card>("magic", obj)
}
/// Update the entry within the Magic collection hashmap with the same id as the
/// povided `Card` instance as JSON object. The old entry will be completely overwritten
/// with the data from the provided object.
///
/// # Argument
/// obj - Pokemon card instance as JSON object.
///
pub fn update_card<'a>(obj: &str) -> Result<(), &'a str> {
templates::card_service_templates::update_entry_in_collection::<Card>("magic", obj)
}
/// Delete the entry within the Magic collection hashmap with the provided id.
///
/// # Argument
/// id - Id of the card within the collection that should be deleted.
///
pub fn delete_card<'a>(id: &u32) -> Result<(), &'a str> {
let card: Card = templates::card_service_templates::get_entry_by_id::<Card>("magic", id).unwrap();
for image in card.images.iter() {
delete_image(image);
}
templates::card_service_templates::delete_entry_by_id::<Card>("magic", id)
}
/// Get the Magic collection hashmap as JSON encoded string.
///
pub fn get_collection_json<'a>() -> Result<String, &'a str> {
templates::card_service_templates::get_collection_json::<Card>("magic")
}
/// Copy the image specfied by the image location parameter as an image that is
/// related to the provided Card object to the collection's image directory.
/// The flag `new_entry` indicates if the provided Card object is an existing object
/// within the collection or a new one. If it is a new one, this function will use
/// the next free id wihtin the collection for the name of the image that is copied.
///
/// # Arguments
/// obj - JSON encoded Card instance of the card entry that the image should belong to.
/// img_location - Ansolute path to the image that should be copied.
/// new_entry - Indicator whether the related card entry is already stored in the collection
/// or if it is a new entry that is not stored in the collection.
///
/// # Returns
/// New name of copied image within the card collection directory
///
pub fn copy_image<'a>(obj: &str, img_location: &str, new_entry: bool) -> Result<String, &'a str> {
let card: Card = serde_json::from_str(obj).expect("Unable to deserialize card object.");
// get next image index
let mut index: u8 = 0;
if card.images.len() > 0 {
// support of file names of card collection manager v1. In this case, we start with index 0.
let last_element = card.images.get(card.images.len() - 1).unwrap();
if !(last_element.contains("IMG_FRONT") || last_element.contains("IMG_BACK")) {
index = parse_index_from_filename(last_element.as_str()) + 1;
}
}
// image name, if its a new entry, the template function will request the next id automatically
let img_target_name: String = match new_entry {
true => format!("{}+{}+{}", card.set.name, &card.name, index),
false => format!("{}+{}+{}+{}", card.id, card.set.name, &card.name, index)
};
// todo- fallback support old card image names from card collection manager 1
templates::card_service_templates::copy_image::<Card>(img_location, &img_target_name, "magic", new_entry)
}
/// Delete the image with the specified name from the card collection directory.
///
/// # Arguments
/// image - Name of image that should be deleted.
///
pub fn delete_image<'a>(image: &str) -> Result<(), &'a str> {
templates::card_service_templates::delete_image("magic", image)
}
/// Get the image with the specified name from the card collection directory
/// as base-64 encoded string.
///
/// # Arguments
/// image - Name of image that should be returned.
///
/// # Returns
/// Image as base-64 encoded string
///
pub fn get_image_b64<'a>(image: &str) -> Result<String, &'a str> {
templates::card_service_templates::get_entry_image_b64("magic", image)
}
@@ -0,0 +1,2 @@
pub mod card_services;
pub mod set_services;
@@ -0,0 +1,97 @@
use crate::templates;
use serde::{Deserialize, Serialize};
// Magic set information
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Set {
/// set id accoring to API at `https://api.magicthegathering.io`
pub id: String,
/// actual set name
pub name: String,
/// release date in format YYYY/MM/DD
#[serde(rename = "releaseDate")]
pub release_date: String,
}
/// Get all MTG sets as vector of `Set` instances.
/// Implicitly, this function also stores the resulting data in JSON format
/// at `<storage_location>/magic/sets.json`.
///
/// This function call the REST-API at `https://api.magicthegathering.io` to retrieve the data.
///
pub fn update_sets<'a>() -> Result<Vec<Set>, &'a str> {
#[derive(Serialize, Deserialize, Debug, Clone)]
struct TmpSet {
pub code: String,
pub name: String,
pub released_at: String,
pub digital: bool,
}
#[derive(Serialize, Deserialize, Debug)]
struct Response {
data: Vec<TmpSet>,
}
let resp = reqwest::blocking::get("https://api.scryfall.com/sets")
.unwrap()
.json::<Response>()
.unwrap();
// filter out 'onlyOnline', map to final struct and sort by release date
let mut prepared_sets = resp
.data
.iter()
.filter(|&set| !set.digital)
.map(|set| Set {
id: set.code.clone(),
name: set.name.clone(),
release_date: set.released_at.clone().replace("-", "/"),
})
.collect::<Vec<_>>();
prepared_sets.sort_by_key(|set| set.release_date.clone());
store_sets(&prepared_sets).unwrap();
Ok(prepared_sets)
}
/// Get all available MTG sets from either the locally stored file at `<storage_location>/magic/sets.json`
/// or a fresh fetch from the corresponding API, in JSON format as a string.
/// If the file does not exist, it will automatically fetch the data from the
/// REST-API, store the result in the `sets.json` file and return the data as JSON.
///
/// # Arguments
/// `from_local` - If `true`, the function will try to access the local `set.json` file and only
/// fetch the API, if it cannot find this file. If `false`, it will fetch the API
/// for set data, store it in the local `set.json` file and then provide its content
/// as a JSON string.
///
pub fn get_sets_json<'a>(from_local: bool) -> Result<String, &'a str> {
let game = "magic";
// in case `from_local` is false, we perform a fresh data fetch from the API before we
// return data.
if !from_local {
update_sets().unwrap();
}
// in any case we will check if `set.json` already exisits. If it is not the case,
// we will perform an API fetch before (see `Err` branch).
match templates::set_service_templates::get_sets_json(game) {
Ok(sets) => Ok(sets),
Err(_) => {
update_sets().unwrap();
Ok(templates::set_service_templates::get_sets_json(game).unwrap())
}
}
}
/// Store the provided set data in JSON format at `<storage_location>/magic/sets.json`.
///
/// # Argument
/// `sets` - Vector of Set instances that should be stored as JSON.
///
pub fn store_sets<'a>(sets: &Vec<Set>) -> Result<(), &'a str> {
templates::set_service_templates::store_sets::<Set>("magic", sets)
}
@@ -0,0 +1,133 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod pokemon;
mod magic;
mod util;
mod templates;
use tauri::{CustomMenuItem, Menu, Submenu};
use util::enums::Game;
#[tauri::command]
fn get_sets<'a>(game: Game) -> Result<String, &'a str> {
match game {
Game::Magic => magic::set_services::get_sets_json(true),
Game::Pokemon => pokemon::set_services::get_sets_json(true)
}
}
#[tauri::command]
fn update_sets<'a>(game: Game) -> Result<String, &'a str> {
match game {
Game::Magic => magic::set_services::get_sets_json(false),
Game::Pokemon => pokemon::set_services::get_sets_json(false)
}
}
#[tauri::command]
fn get_collection<'a>(game: Game) -> Result<String, &'a str> {
match game {
Game::Magic => magic::card_services::get_collection_json(),
Game::Pokemon => pokemon::card_services::get_collection_json()
}
}
#[tauri::command]
fn add_card<'a>(obj: &str, game: Game) -> Result<u32, &'a str> {
match game {
Game::Magic => magic::card_services::add_card(obj),
Game::Pokemon => pokemon::card_services::add_card(obj)
}
}
#[tauri::command]
fn update_card<'a>(obj: &str, game: Game) -> Result<(), &'a str> {
match game {
Game::Magic => magic::card_services::update_card(obj),
Game::Pokemon => pokemon::card_services::update_card(obj)
}
}
#[tauri::command]
fn delete_card<'a>(id: u32, game: Game) -> Result<(), &'a str> {
match game {
Game::Magic => magic::card_services::delete_card(&id),
Game::Pokemon => pokemon::card_services::delete_card(&id)
}
}
#[tauri::command]
fn copy_image<'a>(obj: &str, img_location: &str, new_entry: bool, game: Game) -> Result<String, &'a str> {
match game {
Game::Magic => magic::card_services::copy_image(obj, img_location, new_entry),
Game::Pokemon => pokemon::card_services::copy_image(obj, img_location, new_entry)
}
}
#[tauri::command]
fn get_image_b64<'a>(image: &str, game: Game) -> Result<String, &'a str> {
match game {
Game::Magic => magic::card_services::get_image_b64(image),
Game::Pokemon => pokemon::card_services::get_image_b64(image)
}
}
#[tauri::command]
fn delete_image<'a>(image: &str, game: Game) -> Result<(), &'a str> {
match game {
Game::Magic => magic::card_services::delete_image(image),
Game::Pokemon => pokemon::card_services::delete_image(image)
}
}
fn main() {
// configure menu
// file menu
let settings = CustomMenuItem::new("settings".to_string(), "Settings");
let quit = CustomMenuItem::new("quit".to_string(), "Quit Application");
let file_menu = Submenu::new("File", Menu::new().add_item(settings).add_item(quit));
// game menu
let game_pokemon = CustomMenuItem::new("switch_game/pokemon".to_string(), "Pokemon");
let game_magic = CustomMenuItem::new("switch_game/magic".to_string(), "Magic");
let game_menu = Submenu::new("Game", Menu::new().add_item(game_pokemon).add_item(game_magic));
// update menu
let update_sets_pokemon = CustomMenuItem::new("update/sets/pokemon".to_string(), "Update Pokemon");
let update_sets_magic = CustomMenuItem::new("update/sets/magic".to_string(), "Update Magic");
let update_menu = Submenu::new("Sets", Menu::new().add_item(update_sets_pokemon).add_item(update_sets_magic));
let menu = Menu::new().add_submenu(file_menu).add_submenu(game_menu).add_submenu(update_menu);
tauri::Builder::default()
.menu(menu)
.on_menu_event(|event| match event.menu_item_id() {
"quit" => {
std::process::exit(0);
}
_ => {}
})
.invoke_handler(tauri::generate_handler![
util::config::get_configuration_json,
util::config::store_configuration,
util::enums::get_condition_variants_json,
util::enums::get_language_variants_json,
util::enums::get_game_variants_json,
add_card,
get_sets,
update_sets,
get_collection,
copy_image,
get_image_b64,
delete_image,
delete_card,
update_card
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
@@ -0,0 +1,137 @@
use serde::{Serialize, Deserialize};
use crate::util::enums::{Language, Condition};
use crate::util::collection::MapEntryWithId;
use crate::util::fs::parse_index_from_filename;
use super::set_services::Set;
use crate::templates;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Card {
pub id: u32,
pub amount: u8,
pub name: String,
pub set: Set,
#[serde(rename = "setNo")]
pub set_no: String,
pub note: String,
pub images: Vec<String>,
pub language: Language,
pub condition: Condition,
#[serde(rename = "firstEdition")]
pub first_edition: bool,
pub holo: bool,
pub signed: bool,
pub altered: bool
}
/// implementation of the Card struct that is used by templates
/// to enable generic handling.
impl MapEntryWithId for Card {
fn set_id(&mut self, id: u32) {
self.id = id;
}
fn get_id(&self) -> u32 {
self.id
}
}
/// Parse the provided JSON object into an instance of `Card`, store it into
/// the Pokemon collection hashmap and return the id of the new entry.
///
/// # Argument
/// obj - Pokemon card instance as JSON object.
///
/// # Returns
/// New id of the entry withing the collection
///
pub fn add_card<'a>(obj: &str) -> Result<u32, &'a str> {
templates::card_service_templates::add_entry_to_collection::<Card>("pokemon", obj)
}
/// Update the entry within the Pokemon collection hashmap with the same id as the
/// povided `Card` instance as JSON object. The old entry will be completely overwritten
/// with the data from the provided object.
///
/// # Argument
/// obj - Pokemon card instance as JSON object.
///
pub fn update_card<'a>(obj: &str) -> Result<(), &'a str> {
templates::card_service_templates::update_entry_in_collection::<Card>("pokemon", obj)
}
/// Delete the entry within the Pokemon collection hashmap with the provided id.
///
/// # Argument
/// id - Id of the card within the collection that should be deleted.
///
pub fn delete_card<'a>(id: &u32) -> Result<(), &'a str> {
let card: Card = templates::card_service_templates::get_entry_by_id::<Card>("pokemon", id).unwrap();
for image in card.images.iter() {
delete_image(image);
}
templates::card_service_templates::delete_entry_by_id::<Card>("pokemon", id)
}
/// Get the Pokemon collection hashmap as JSON encoded string.
///
pub fn get_collection_json<'a>() -> Result<String, &'a str> {
templates::card_service_templates::get_collection_json::<Card>("pokemon")
}
/// Copy the image specfied by the image location parameter as an image that is
/// related to the provided Card object to the collection's image directory.
/// The flag `new_entry` indicates if the provided Card object is an existing object
/// within the collection or a new one. If it is a new one, this function will use
/// the next free id wihtin the collection for the name of the image that is copied.
///
/// # Arguments
/// obj - JSON encoded Card instance of the card entry that the image should belong to.
/// img_location - Ansolute path to the image that should be copied.
/// new_entry - Indicator whether the related card entry is already stored in the collection
/// or if it is a new entry that is not stored in the collection.
///
/// # Returns
/// New name of copied image within the card collection directory
///
pub fn copy_image<'a>(obj: &str, img_location: &str, new_entry: bool) -> Result<String, &'a str> {
let card: Card = serde_json::from_str(obj).expect("Unable to deserialize card object.");
// get next image index
let mut index: u8 = 0;
if card.images.len() > 0 {
index = parse_index_from_filename(card.images[card.images.len() - 1].as_str()) + 1;
}
// image name, if its a new entry, the template function will request the next id automatically
let img_target_name: String = match new_entry {
true => format!("{}+{}+{}", card.set.name, &card.name, index),
false => format!("{}+{}+{}+{}", card.id, card.set.name, &card.name, index)
};
templates::card_service_templates::copy_image::<Card>(img_location, &img_target_name, "pokemon", new_entry)
}
/// Delete the image with the specified name from the card collection directory.
///
/// # Arguments
/// image - Name of image that should be deleted.
///
pub fn delete_image<'a>(image: &str) -> Result<(), &'a str> {
templates::card_service_templates::delete_image("pokemon", image)
}
/// Get the image with the specified name from the card collection directory
/// as base-64 encoded string.
///
/// # Arguments
/// image - Name of image that should be returned.
///
/// # Returns
/// Image as base-64 encoded string
///
pub fn get_image_b64<'a>(image: &str) -> Result<String, &'a str> {
templates::card_service_templates::get_entry_image_b64("pokemon", image)
}
@@ -0,0 +1,2 @@
pub mod card_services;
pub mod set_services;
@@ -0,0 +1,73 @@
use serde::{Serialize, Deserialize};
use crate::templates;
// Pokemon TCG set information
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Set {
/// set id accoring to API at `https://api.pokemontcg.io`
pub id: String,
/// actual set name
pub name: String,
/// release date in format YYYY/MM/DD
#[serde(rename = "releaseDate")]
pub release_date: String
}
/// Get all Pokemon TCG sets as vector of `Set` instances.
/// Implicitly, this function also stores the resulting data in JSON format
/// at `<storage_location>/pokemon/sets.json`.
///
/// This function call the REST-API at `https://api.pokemontcg.io` to retrieve the data.
///
pub fn update_sets<'a>() -> Result<Vec<Set>, &'a str> {
#[derive(Serialize, Deserialize, Debug)]
struct Response {
data: Vec<Set>
}
let resp = reqwest::blocking::get("https://api.pokemontcg.io/v2/sets").unwrap().json::<Response>().unwrap();
store_sets(&resp.data).unwrap();
Ok(resp.data)
}
/// Get all Pokemon TCG from either the locally stored file at `<storage_location>/pokemon/sets.json`
/// or a fresh fetch from the corresponding API, in JSON format as a string.
/// If the file does not exist, it will automatically fetch the data from the
/// REST-API, store the result in the `sets.json` file and return the data as JSON.
///
/// # Arguments
/// `from_local` - If `true`, the function will try to access the local `set.json` file and only
/// fetch the API, if it cannot find this file. If `false`, it will fetch the API
/// for set data, store it in the local `set.json` file and then provide its content
/// as a JSON string.
///
pub fn get_sets_json<'a>(from_local: bool) -> Result<String, &'a str> {
let game = "pokemon";
// in case `from_local` is false, we perform a fresh data fetch from the API before we
// return data.
if !from_local {
update_sets().unwrap();
}
// in any case we will check if `set.json` already exisits. If it is not the case,
// we will perform an API fetch before (see `Err` branch).
match templates::set_service_templates::get_sets_json(game) {
Ok(sets) => Ok(sets),
Err(_) => {
update_sets().unwrap();
Ok(templates::set_service_templates::get_sets_json(game).unwrap())
}
}
}
/// Store the provided set data in JSON format at `<storage_location>/pokemon/sets.json`.
///
/// # Argument
/// `sets` - Vector of Set instances that should be stored as JSON.
///
pub fn store_sets<'a>(sets: &Vec<Set>) -> Result<(), &'a str> {
templates::set_service_templates::store_sets::<Set>("pokemon", sets)
}
@@ -0,0 +1,230 @@
use std::io::{Read, Write};
use std::collections::HashMap;
use std::path::Path;
use std::ffi::OsStr;
use std::fs::{File, create_dir, copy, remove_file};
use serde::{Serialize};
use serde::de::DeserializeOwned;
use image::{DynamicImage, ImageOutputFormat};
use std::io::Cursor;
use crate::util::config::{Configuration, load_configuration};
use crate::util::collection::{MapEntryWithId, add_map_entry, get_next_id};
use crate::util::fs::format_text_for_fs;
/// Add the povided object as a new entry to the collection of the provided game, store the new update
/// to the collection file of the game and return the new id of the entry in the collection.
///
/// # Arguments
/// `game` - Game name to specifiy to which collection the entry should be added
/// `obj` - New entry as JSON string
///
/// # Returns
/// New id of the entry withing the collection
///
pub fn add_entry_to_collection<'a, T: Serialize + DeserializeOwned + MapEntryWithId>(game: &str, obj: &str) -> Result<u32, &'a str> {
let entry: T = serde_json::from_str(obj).expect("Unable to deserialize entry object.");
let mut collection: HashMap<u32, T> = load_collection::<T>(game).unwrap();
let id: u32 = add_map_entry::<T>(&mut collection, entry);
store_collection::<T>(game, &collection).expect("Unable to store updated collection.");
Ok(id)
}
/// Update an existing entry in the collection of the provided game, with the provided object.
/// The id within the provided object specficies, which existing entry should be updated.
///
/// # Arguments
/// `game` - Game name to specifiy in which collection the entry should be updated
/// `obj` - entry as JSON string
///
pub fn update_entry_in_collection<'a, T: Serialize + DeserializeOwned + MapEntryWithId>(game: &str, obj: &str) -> Result<(), &'a str> {
let entry: T = serde_json::from_str(obj).expect("Unable to deserialize entry object.");
let mut collection: HashMap<u32, T> = load_collection::<T>(game).unwrap();
let id: u32 = entry.get_id();
collection.insert(id, entry).expect("Unable to update collection");
store_collection::<T>(game, &collection).expect("Unable to store updated collection.");
Ok(())
}
/// Return the entry with the specfied `id` from the collection corresponding to the specified `game`.
///
/// # Arguments
/// `game` - Game name to specifiy from which collection the entry should be retrieved
/// `id` - Id of the entry that should be retrieved
///
/// # Returns
/// Entry record with the specified id from the specified collection
///
pub fn get_entry_by_id<'a, T: Serialize + DeserializeOwned + Clone>(game: &str, id: &u32) -> Result<T, &'a str> {
let collection: HashMap<u32, T> = load_collection::<T>(game).unwrap();
match collection.get(id) {
Some(entry) => Ok(entry.clone()),
None => Err("Unable to get entry")
}
}
/// Delete the entry with the specfied `id` from the collection corresponding to the specified `game`.
///
/// # Arguments
/// `game` - Game name to specifiy from which collection the entry should be deleted
/// `id` - Id of the entry that should be deleted
///
pub fn delete_entry_by_id<'a, T: Serialize + DeserializeOwned + Clone>(game: &str, id: &u32) -> Result<(), &'a str> {
let mut collection: HashMap<u32, T> = load_collection::<T>(game).unwrap();
collection.remove(id).unwrap();
store_collection::<T>(game, &collection)
}
/// Get the collection as a JSON map corresponding to the provided game from the local collection.json file that
/// belongs to this game. If this file does not exist, it will be generated.
///
/// # Arguments
/// `game` - Game name for which the collection should be returned
///
/// # Returns
/// string with a map of the collection data
///
pub fn get_collection_json<'a, T: Serialize + DeserializeOwned>(game: &'a str) -> Result<String, &'a str> {
let collection = load_collection::<T>(game).unwrap();
match serde_json::to_string(&collection) {
Ok(json) => Ok(json),
Err(_) => Err("Unable to serialize collection to JSON.")
}
}
/// Load the collection related to the provided game from the corresponding collection file and
/// return it as a hash map.
///
/// # Arguments
/// `game` - Game name to specify which collection should be loaded
///
/// # Returns
/// The collection corresponding to the game name as a hash map
///
fn load_collection<'a, T: Serialize + DeserializeOwned>(game: &str) -> Result<HashMap<u32, T>, &'a str> {
let config: Configuration = load_configuration().unwrap();
let collection_file = format!("{}/{}/collection.json", &config.data_storage, game);
let collection_file_path = Path::new(&collection_file);
if collection_file_path.exists() {
let mut data = String::new();
let mut f = File::open(&collection_file_path).expect("Unable to open file stream.");
f.read_to_string(&mut data).expect("Unable to read file to string.");
let collection: HashMap<u32, T> = serde_json::from_str(&data).expect("Unable to deserialize collection.");
Ok(collection)
}
else {
let collection: HashMap<u32, T> = HashMap::new();
store_collection::<T>(game, &collection).unwrap();
Ok(collection)
}
}
/// Store the provided collection to the corresponding file specified by the provided game name.
///
/// # Arguments
/// `game` - Game name to specify to which game the provided collection belongs
/// `collection` - Collection that should be stored to the collection file corresponding to the provided game
///
fn store_collection<'a, T: Serialize>(game: &str, collection: &HashMap<u32, T>) -> Result<(), &'a str> {
let config: Configuration = load_configuration().unwrap();
let game_dir = format!("{}/{}", &config.data_storage, game);
let collection_file = format!("{}/{}/collection.json", &config.data_storage, game);
let game_dir_path = Path::new(&game_dir);
let collection_file_path = Path::new(&collection_file);
// check if game subdir exists
if !game_dir_path.exists() {
create_dir(game_dir_path).unwrap();
}
let collection_json = serde_json::to_string(collection).expect("Unable to serialize collection to JSON.");
let mut collection_file = File::create(&collection_file_path).expect("Unable to create 'collection.json' file.");
collection_file.write_all(&collection_json.as_bytes()).expect("Unable to write JSON-serialized collection to 'collection.json'.");
Ok(())
}
/// Copy the image from the location specified via `img_location`. The path of the copied file depends on the specified `game`,
/// the string provided via `img_target_name` and the flag `new_entry`. `game` basically translates into the corresponding
/// game sub-directory in the apps storage directory. `img_target_name` is an arbitrary string that is used as the name of the
/// copied file. If the flag `new_entry` is `true`, this function will fetch the next id of the game's collection and use it
/// as a prefix of the copied file name. Otherwise, it will simply use `img_target_name` as the filename.
///
/// # Arguments
/// `img_location` - Absolute path to the image that should be copied
/// `img_target_name` - Name that should be used for the copy
/// `game` - Game name to specify to which game the image belongs
/// `new_entry` - Flag to indicate if the image belogns to a new entry (`true`) or an existing one (`false`).
/// In case of a new entry, the next id of the game's collection will be used as a prefix for
/// the new image name.
///
/// # Returns
/// The name of the new image file.
///
pub fn copy_image<'a, T: Serialize + DeserializeOwned>(img_location: &str, img_target_name: &str, game: &str, new_entry: bool) -> Result<String, &'a str> {
let config = load_configuration().expect("Unable to load configuration");
let file_extension = Path::new(&img_location).extension().and_then(OsStr::to_str).unwrap();
let mut new_filename: String = format_text_for_fs(&format!("{}.{}",img_target_name, file_extension));
if new_entry {
let collection: HashMap<u32, T> = load_collection::<T>(game).unwrap();
let id: u32 = get_next_id::<T>(&collection);
new_filename = format!("{}+{}", id, &new_filename);
}
// check if image dir exists
let image_dir = format!("{}/{}/images", &config.data_storage, game);
let image_dir_path = Path::new(&image_dir);
if !image_dir_path.exists() {
create_dir(image_dir_path).unwrap();
}
let copy_target = format!("{}/{}", &image_dir, &new_filename);
copy(img_location, &copy_target).expect("Unable to copy file.");
Ok(new_filename)
}
/// Returns the image, specified by the provided `game` and `image` name as a base-64 encoded string.
///
/// # Arguments
/// `game` - Game name to specify to which game the image belongs
/// `image` - Name of the image to returns
///
/// # Returns
/// Specified image as base-64 encoded string.
///
pub fn get_entry_image_b64<'a>(game: &str, image: &str) -> Result<String, &'a str> {
let config = load_configuration().expect("Unable to load configuration");
let file_extension = Path::new(image).extension().and_then(OsStr::to_str).unwrap();
let image_location = format!("{}/{}/images/{}", &config.data_storage, game, image);
let img: DynamicImage = image::open(&image_location).unwrap();
let mut img_data: Vec<u8> = Vec::new();
let format = match file_extension {
"png" => ImageOutputFormat::Png,
"jpg" => ImageOutputFormat::Jpeg(255),
"jpeg" => ImageOutputFormat::Jpeg(255),
_ => ImageOutputFormat::Png,
};
img.write_to(&mut Cursor::new(&mut img_data), format).unwrap();
let img_data_b64 = format!("data:image/{};base64,{}", file_extension, base64::encode(img_data));
Ok(img_data_b64)
}
/// Delete the image, specified by the provided `game` and `image` name.
///
/// # Arguments
/// `game` - Game name to specify to which game the image belongs
/// `image` - Name of the image to delete
///
pub fn delete_image<'a>(game: &str, image: &str) -> Result<(), &'a str> {
let config = load_configuration().expect("Unable to load configuration");
let image_location = format!("{}/{}/images/{}", &config.data_storage, game, image);
let image_location_path = Path::new(&image_location);
remove_file(image_location_path).expect("Unable to delete image.");
Ok(())
}
@@ -0,0 +1,2 @@
pub mod set_service_templates;
pub mod card_service_templates;
@@ -0,0 +1,56 @@
use std::io::{Read, Write};
use std::path::Path;
use std::fs::{File, create_dir};
use serde::{Serialize};
use crate::util::config::{Configuration, load_configuration};
/// Get all sets as a JSON list corresponding to the provided game from the local sets.json file that
/// belongs to this game. If this file does not exist, the function will return an error.
///
/// # Arguments
/// `game` - Game name for which the sets should be returned
///
/// # Returns
/// string with a list of set objects if the corresponding file exists
///
pub fn get_sets_json<'a>(game: &'a str) -> Result<String, &'a str> {
let config: Configuration = load_configuration().unwrap();
let set_file = format!("{}/{}/sets.json", &config.data_storage, game);
let set_file_path = Path::new(&set_file);
if set_file_path.exists() {
let mut data = String::new();
let mut f = File::open(&set_file_path).expect("Unable to open file stream.");
f.read_to_string(&mut data).expect("Unable to read file to string.");
Ok(data)
}
else {
Err("File does not exist.")
}
}
/// Store the povided vector of sets to the set file corresponding to the provided game.
///
/// # Arguments
/// `game` - Game name to specify to which game the provided sets belong
/// `sets` - Vector of sets that should be stored in the set file corresponding to the provided game
///
pub fn store_sets<'a, T: Serialize>(game: &'a str, sets: &Vec<T>) -> Result<(), &'a str> {
let config: Configuration = load_configuration().unwrap();
let game_dir = format!("{}/{}", &config.data_storage, game);
let set_file = format!("{}/{}/sets.json", &config.data_storage, game);
let game_dir_path = Path::new(&game_dir);
let set_file_path = Path::new(&set_file);
// check if game subdir exists
if !game_dir_path.exists() {
create_dir(game_dir_path).unwrap();
}
let set_json = serde_json::to_string(sets).expect("Unable to serialize set data to JSON.");
let mut set_file = File::create(&set_file_path).expect("Unable to create 'sets.json' file.");
set_file.write_all(&set_json.as_bytes()).expect("Unable to write JSON-serialized sets to 'set.json'.");
Ok(())
}
@@ -0,0 +1,34 @@
use std::collections::HashMap;
/// Trait that a type needs to implement in order to use function `add_map_entry`
/// for a hash map that refers to the type.
///
pub trait MapEntryWithId {
fn set_id(&mut self, id: u32);
fn get_id(&self) -> u32;
}
pub fn get_next_id<T>(map: &HashMap<u32, T>) -> u32 {
match map.keys().max() {
None => 0,
Some(i) => i + 1
}
}
/// Adds the provided entry to the provided map, set its field `id` to the new id
/// of the map and returns that id.
///
/// # Arguments
/// `map` - HashMap to which the provided entry should be added
/// `entry` - Entry that should be added to the provided map with a new id of the map
///
/// # Returns
/// Id of the new entry
///
pub fn add_map_entry<T: MapEntryWithId>(map: &mut HashMap<u32, T>, mut entry: T) -> u32 {
let id: u32 = get_next_id::<T>(&map);
entry.set_id(id);
map.insert(id, entry);
id
}
@@ -0,0 +1,56 @@
use std::path::Path;
use std::fs::File;
use std::io::{Read, Write};
use std::env::current_dir;
use serde::{Serialize, Deserialize};
use super::enums::Game;
/// General application configuration
#[derive(Serialize, Deserialize, Debug)]
pub struct Configuration {
/// Absolute path to the location, where all collection data should be stored.
#[serde(rename = "dataStorage")]
pub data_storage: String,
/// Default game to start the app with
#[serde(rename = "defaultGame")]
pub default_game: Game
}
/// Get the application_s configuration from the config file at `config.json`
/// as instance of struct `Configuration`. If the file does not exist, it will
/// be generated automatically.
///
pub fn load_configuration<'a>() -> Result<Configuration, &'a str> {
if Path::new("config.json").exists() {
let mut data = String::new();
let mut f = File::open("config.json").expect("Unable to open file stream.");
f.read_to_string(&mut data).expect("Unable to read config file to string.");
Ok(serde_json::from_str(&data).expect("Unable to deserialize configuration."))
}
else {
let config = Configuration { data_storage: current_dir().unwrap().to_str().unwrap().to_string(), default_game: Game::Magic };
store_configuration(&serde_json::to_string(&config).expect("Unable to serialize configuration.")).unwrap();
load_configuration()
}
}
/// Overwrite the current configuration file with the JSON data provided by this function.
///
/// # Argument
/// * `obj` - A string that contains the new app configuration as a JSON object.
///
#[tauri::command]
pub fn store_configuration<'a>(obj: &'a str) -> Result<(), &'a str> {
let mut file = File::create("config.json").expect("Unable to create file.");
file.write_all(obj.as_bytes()).expect("Unable to write file.");
Ok(())
}
/// Get the app configuration from file `config.json` in JSON format as a string.
#[tauri::command]
pub fn get_configuration_json<'a>() -> Result<String, &'a str> {
let configuration = load_configuration().expect("Unable to load configuration");
Ok(serde_json::to_string(&configuration).expect("Unable to serialize configuration."))
}
@@ -0,0 +1,59 @@
use serde::{Serialize, Deserialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
#[derive(Serialize, Deserialize, Debug, Clone, EnumIter)]
pub enum Language {
English,
German,
French,
Spanish,
Italian,
Chinese,
Japanese,
Russian
}
#[derive(Serialize, Deserialize, Debug, Clone, EnumIter)]
pub enum Condition {
Mint,
NearMint,
Excellent,
Good,
LightPlayed,
Played,
Poor
}
#[derive(Serialize, Deserialize, Debug, Clone, EnumIter)]
pub enum Game {
Magic,
Pokemon
}
fn get_enum_variants<T: IntoEnumIterator>() -> Vec<T> {
let mut variants: Vec<T> = Vec::new();
for variant in T::iter() {
variants.push(variant);
}
variants
}
/// Get all possible languages that are supported by the app.
#[tauri::command]
pub fn get_language_variants_json() -> Result<String, String> {
Ok(serde_json::to_string(&get_enum_variants::<Language>()).unwrap())
}
/// Get all possible card conditions that are supported by the app.
#[tauri::command]
pub fn get_condition_variants_json() -> Result<String, String> {
Ok(serde_json::to_string(&get_enum_variants::<Condition>()).unwrap())
}
/// Get all possible games that are supported by the app.
#[tauri::command]
pub fn get_game_variants_json() -> Result<String, String> {
Ok(serde_json::to_string(&get_enum_variants::<Game>()).unwrap())
}
@@ -0,0 +1,44 @@
use nom::character::is_digit;
use std::path::Path;
use std::ffi::OsStr;
/// Replace problematic characters in a text in order to
/// avoid any conflicts when using the text to create a
/// file.
pub fn format_text_for_fs(text: &String) -> String {
text
.replace("'", "")
.replace("`", "")
.replace(",","")
.replace(" ", "")
.replace(":", "-")
.replace("&", "And")
.replace("|", "Or")
.replace("é", "e")
}
/// Parse an index from the end of a filename. This function supports indices with
/// 1 and 2 digit indices.
///
/// Example:
/// image1.png -> 1
/// image22.jpeg -> 22
///
pub fn parse_index_from_filename(filename: &str) -> u8 {
// get offset according to extension
let file_extension = Path::new(filename).extension().and_then(OsStr::to_str).unwrap();
let offset = file_extension.len() + 1;
let mut index_len = 1;
// try if index is 2 digits long
let two_digits_test: String = filename.chars().skip(filename.len() - offset - 2).take(1).collect();
if is_digit(two_digits_test.as_bytes()[0]) {
index_len = 2;
}
// extract index
let index: String = filename.chars().skip(filename.len() - offset - index_len).take(index_len).collect();
index.parse::<u8>().unwrap()
}
@@ -0,0 +1,4 @@
pub mod enums;
pub mod config;
pub mod collection;
pub mod fs;
@@ -0,0 +1,67 @@
{
"build": {
"beforeDevCommand": "yarn dev",
"beforeBuildCommand": "yarn build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
},
"package": {
"productName": "card-collection-manager-2",
"version": "1.0.0"
},
"tauri": {
"allowlist": {
"all": true
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.sdine.dev",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"resizable": true,
"title": "Card Collection Manager 2",
"minWidth": 1300,
"minHeight": 900,
"width": 1300,
"height": 900
}
]
}
}