Compare commits

9 Commits

11 changed files with 871 additions and 514 deletions

1079
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,10 @@ chrono = "0.4.38"
strum = "0.26" strum = "0.26"
strum_macros = "0.26" strum_macros = "0.26"
rand = "0.8.5" rand = "0.8.5"
xdg = "2.5.2"
toml = "0.8.19"
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0.93"
# strum = { version = "0.26", features = ["derive"] } # strum = { version = "0.26", features = ["derive"] }
[profile.dev.package.sqlx-macros] [profile.dev.package.sqlx-macros]

View File

@@ -1,2 +1,12 @@
# Sheet Organizer # Sheet Organizer
A simple tool for organizing and opening digital sheet music on a touch display as part of a digital music stand. A simple tool for organizing and opening digital sheet music on a touch display as part of a digital music stand.
## Dependencies
This tool offers editing pdf using [Xournal++](https://github.com/xournalpp/xournalpp).
## Configuration
You can configure sheet-organizer using an file `config.toml` inside one of your `$XDG_CONFIG_DIRECTORIES` (e.g. `~/.config/sheet-organizer/config.toml`).
```toml
working_directory = "~/my-sheets"
```

23
flake.lock generated
View File

@@ -1,17 +1,12 @@
{ {
"nodes": { "nodes": {
"crane": { "crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": { "locked": {
"lastModified": 1717469187, "lastModified": 1736101677,
"narHash": "sha256-UVvFGiWFGPfVXG7Xr6HPKChx9hhtzkGaGAS/Ph1Khjg=", "narHash": "sha256-iKOPq86AOWCohuzxwFy/MtC8PcSVGnrxBOvxpjpzrAY=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "7e86136dc729cdf237aa59a5a02687bc0d1144b6", "rev": "61ba163d85e5adeddc7b3a69bb174034965965b2",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -25,11 +20,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1710146030, "lastModified": 1731533236,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -40,11 +35,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1716715802, "lastModified": 1736241350,
"narHash": "sha256-usk0vE7VlxPX8jOavrtpOqphdfqEQpf9lgedlY/r66c=", "narHash": "sha256-CHd7yhaDigUuJyDeX0SADbTM9FXfiWaeNyY34FL1wQU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e2dd4e18cc1c7314e24154331bae07df76eb582f", "rev": "8c9fd3e564728e90829ee7dbac6edc972971cd0f",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -3,24 +3,20 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
crane.url = "github:ipetkov/crane";
}; };
outputs = outputs =
{ {
self, self,
nixpkgs, nixpkgs,
crane,
flake-utils, flake-utils,
crane,
... ...
}: }:
flake-utils.lib.eachDefaultSystem ( let
packageOutputs = flake-utils.lib.eachDefaultSystem (
system: system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
@@ -56,7 +52,6 @@
with pkgs; with pkgs;
[ [
gtk4 gtk4
xournalpp # not needed for building
] ]
++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
# Additional darwin specific inputs can be set here # Additional darwin specific inputs can be set here
@@ -86,11 +81,26 @@
# Build the actual crate itself, reusing the dependency # Build the actual crate itself, reusing the dependency
# artifacts from above. # artifacts from above.
myCrate = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; }); myCrate = craneLib.buildPackage (
commonArgs
// {
inherit cargoArtifacts;
}
// {
postInstall = ''
mkdir -p $out/share/applications
cp ${./sheet-organizer.desktop} $out/share/applications/sheet-organizer.desktop
mkdir -p $out/share/icons
cp ${./sheet-organizer.png} $out/share/icons/sheet-organizer.png
'';
}
);
# Also run the crate tests under cargo-tarpaulin so that we can keep # Also run the crate tests under cargo-tarpaulin so that we can keep
# track of code coverage # track of code coverage
myCrateCoverage = craneLib.cargoTarpaulin (commonArgs // { inherit cargoArtifacts; }); myCrateCoverage = craneLib.cargoTarpaulin (commonArgs // { inherit cargoArtifacts; });
in in
{ {
packages.default = myCrate; packages.default = myCrate;
@@ -104,4 +114,6 @@
}; };
} }
); );
in
packageOutputs;
} }

6
sheet-organizer.desktop Normal file
View File

@@ -0,0 +1,6 @@
[Desktop Entry]
Type=Application
Terminal=false
Name=Sheet Organizer
Icon=sheet-organizer
Exec=sheet-organizer

BIN
sheet-organizer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

39
src/config.rs Normal file
View File

@@ -0,0 +1,39 @@
use anyhow::{anyhow, Context, Result};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use xdg::BaseDirectories;
#[derive(Debug, Deserialize)]
pub struct Config {
pub working_directory: Option<PathBuf>,
}
impl Config {
pub fn default() -> Config {
Config {
working_directory: None,
}
}
}
pub fn load_config(app_name: &str, file_name: &str) -> Result<Config> {
// Create an XDG base directories instance
let xdg_dirs =
BaseDirectories::with_prefix(app_name).context("Failed to initialize XDG directories")?;
let config_path = xdg_dirs
.place_config_file(file_name)
.context("Failed to determine configuration file path")?;
if !config_path.exists() {
return Err(anyhow!("No configuration file at {:?}", config_path));
}
let contents = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read configuration file at {:?}", config_path))?;
let config: Config = toml::from_str(&contents)
.with_context(|| format!("Failed to parse TOML configuration at {:?}", config_path))?;
Ok(config)
}

View File

@@ -1,3 +1,4 @@
mod config;
mod database; mod database;
mod sheet; mod sheet;
mod sheet_dao; mod sheet_dao;
@@ -7,9 +8,10 @@ mod ui;
use std::{path::PathBuf, process}; use std::{path::PathBuf, process};
use clap::Parser; use clap::Parser;
use config::Config;
use database::Database; use database::Database;
use env_logger::Env; use env_logger::Env;
use log::error; use log::{error, warn};
use relm4::RelmApp; use relm4::RelmApp;
use crate::ui::app::{AppInitData, AppModel}; use crate::ui::app::{AppInitData, AppModel};
@@ -17,28 +19,49 @@ use crate::ui::app::{AppInitData, AppModel};
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about)] #[command(author, version, about)]
struct Cli { struct Cli {
directory: PathBuf, working_directory: Option<PathBuf>,
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init(); env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init();
let mut config = match config::load_config("sheet-organizer", "config.toml") {
Ok(config) => config,
Err(err) => {
warn!("Could not get configuration: {:#}", err);
Config::default()
}
};
let cli = Cli::parse(); let cli = Cli::parse();
if !cli.directory.is_dir() { // Overwrite config by cli options if specified
error!("Sheet folder path is no dir or does not exist"); if cli.working_directory.is_some() {
config.working_directory = cli.working_directory;
}
let working_directory = config.working_directory.unwrap_or_else(|| {
error!("No working directory specified, neither in config nor in cli. Exiting...");
process::exit(1);
});
if !working_directory.is_dir() {
error!(
"Working directory '{}' does not exist",
working_directory.to_string_lossy()
);
process::exit(1); process::exit(1);
} }
let database = Database::setup(cli.directory.join("database.sqlite")) let database = Database::setup(working_directory.join("database.sqlite"))
.await .await
.unwrap(); .unwrap();
let sheets = sheet_validation::load_and_validate_sheets(&database, &cli.directory).await; let sheets = sheet_validation::load_and_validate_sheets(&database, &working_directory).await;
let app_init_data = AppInitData { let app_init_data = AppInitData {
sheets, sheets,
database, database,
directory: cli.directory, directory: working_directory,
}; };
let app = RelmApp::new("de.frajul.sheet-organizer"); let app = RelmApp::new("de.frajul.sheet-organizer");
// Pass empty command line args to allow my own parsing // Pass empty command line args to allow my own parsing

View File

@@ -81,6 +81,19 @@ pub async fn get_composer_by_id(database: &Database, id: i64) -> sqlx::Result<Co
.await .await
} }
pub async fn remove_duplicate_sheets(database: &Database) -> sqlx::Result<()> {
for kind in SheetKindDiscriminants::iter() {
let table = kind.get_database_table_name();
sqlx::query(&format!(
"DELETE FROM {} WHERE id NOT IN (SELECT MIN(id) FROM {} GROUP BY file_hash)",
table, table
))
.execute(&database.connection)
.await?;
}
Ok(())
}
pub async fn fetch_all_sheets(database: &Database) -> sqlx::Result<Vec<Sheet>> { pub async fn fetch_all_sheets(database: &Database) -> sqlx::Result<Vec<Sheet>> {
let mut sheets: Vec<Sheet> = Vec::new(); let mut sheets: Vec<Sheet> = Vec::new();

View File

@@ -13,6 +13,8 @@ pub async fn load_and_validate_sheets(
database: &Database, database: &Database,
directory: impl AsRef<Path>, directory: impl AsRef<Path>,
) -> Vec<Sheet> { ) -> Vec<Sheet> {
sheet_dao::remove_duplicate_sheets(database).await.unwrap();
let sheets = sheet_dao::fetch_all_sheets(database).await.unwrap(); let sheets = sheet_dao::fetch_all_sheets(database).await.unwrap();
debug!("Validating sheets from database..."); debug!("Validating sheets from database...");