Compare commits
No commits in common. "master" and "sheet-refactor" have entirely different histories.
master
...
sheet-refa
1478
Cargo.lock
generated
1478
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
@ -7,33 +7,28 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core library
|
# Core library
|
||||||
relm4 = { version = "0.8.1" }
|
relm4 = "0.6.2"
|
||||||
# relm4-macros = "0.6.2"
|
# relm4-macros = "0.6.2"
|
||||||
# Optional: reusable components
|
# Optional: reusable components
|
||||||
relm4-components = "0.8.1"
|
relm4-components = "0.6.2"
|
||||||
# Optional: icons
|
# Optional: icons
|
||||||
relm4-icons = "0.8.2"
|
relm4-icons = { version = "0.6.0", features = ["plus"] }
|
||||||
|
|
||||||
walkdir = "2" # For traversing directories recursively
|
walkdir = "2" # For traversing directories recursively
|
||||||
opener = "0.7.1" # For opening files with the systems default application
|
opener = "0.6.1" # For opening files with the systems default application
|
||||||
|
|
||||||
log = "0.4.21"
|
log = "0.4.20"
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.10.1"
|
||||||
clap = { version = "4.5.4", features = ["derive"] }
|
clap = { version = "4.4.6", features = ["derive"] }
|
||||||
|
|
||||||
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite", "migrate", "macros" ] }
|
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite", "migrate", "macros" ] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
blake3 = "1.5.1"
|
blake3 = "1.5.0"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.33"
|
||||||
|
|
||||||
strum = "0.26"
|
strum = "0.26"
|
||||||
strum_macros = "0.26"
|
strum_macros = "0.26"
|
||||||
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]
|
||||||
|
10
Readme.md
10
Readme.md
@ -1,12 +1,2 @@
|
|||||||
# 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"
|
|
||||||
```
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
CREATE TABLE IF NOT EXISTS sheets (id INTEGER PRIMARY KEY AUTOINCREMENT,
|
CREATE TABLE IF NOT EXISTS sheets (id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
last_opened INTEGER, name TEXT, composer_id INTEGER, first_page INTEGER, book_id INTEGER, path TEXT, file_size INTEGER, file_hash TEXT);
|
last_opened INTEGER, name TEXT, composer_id INTEGER, path TEXT, file_size INTEGER, file_hash TEXT);
|
||||||
CREATE TABLE IF NOT EXISTS orphans (id INTEGER PRIMARY KEY AUTOINCREMENT,
|
CREATE TABLE IF NOT EXISTS orphans (id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
last_opened INTEGER, path TEXT, file_size INTEGER, file_hash TEXT);
|
last_opened INTEGER, path TEXT, file_size INTEGER, file_hash TEXT);
|
||||||
CREATE TABLE IF NOT EXISTS books (id INTEGER PRIMARY KEY AUTOINCREMENT,
|
CREATE TABLE IF NOT EXISTS books (id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
last_opened INTEGER, name TEXT, composer_id INTEGER, sheet_ids TEXT, path TEXT, file_size INTEGER, file_hash TEXT);
|
last_opened INTEGER, name TEXT, composer_id INTEGER, sheet_ids TEXT, path TEXT, file_size INTEGER, file_hash TEXT);
|
||||||
|
CREATE TABLE IF NOT EXISTS booksheets (id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
last_opened INTEGER, name TEXT, book_id INTEGER, first_page INTEGER, last_page INTEGER);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS composers (id INTEGER primary key autoincrement, name TEXT);
|
CREATE TABLE IF NOT EXISTS composers (id INTEGER primary key autoincrement, name TEXT);
|
||||||
|
78
flake.lock
generated
78
flake.lock
generated
@ -1,45 +1,45 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"crane": {
|
"naersk": {
|
||||||
"locked": {
|
|
||||||
"lastModified": 1736101677,
|
|
||||||
"narHash": "sha256-iKOPq86AOWCohuzxwFy/MtC8PcSVGnrxBOvxpjpzrAY=",
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"rev": "61ba163d85e5adeddc7b3a69bb174034965965b2",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": "systems"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1731533236,
|
"lastModified": 1698420672,
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
|
||||||
"owner": "numtide",
|
"owner": "nix-community",
|
||||||
"repo": "flake-utils",
|
"repo": "naersk",
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "numtide",
|
"owner": "nix-community",
|
||||||
"repo": "flake-utils",
|
"ref": "master",
|
||||||
|
"repo": "naersk",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1736241350,
|
"lastModified": 1704842529,
|
||||||
"narHash": "sha256-CHd7yhaDigUuJyDeX0SADbTM9FXfiWaeNyY34FL1wQU=",
|
"narHash": "sha256-OTeQA+F8d/Evad33JMfuXC89VMetQbsU4qcaePchGr4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8c9fd3e564728e90829ee7dbac6edc972971cd0f",
|
"rev": "eabe8d3eface69f5bb16c18f8662a702f50c20d5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1704842529,
|
||||||
|
"narHash": "sha256-OTeQA+F8d/Evad33JMfuXC89VMetQbsU4qcaePchGr4=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "eabe8d3eface69f5bb16c18f8662a702f50c20d5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -51,9 +51,9 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"crane": "crane",
|
"naersk": "naersk",
|
||||||
"flake-utils": "flake-utils",
|
"nixpkgs": "nixpkgs_2",
|
||||||
"nixpkgs": "nixpkgs"
|
"utils": "utils"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systems": {
|
"systems": {
|
||||||
@ -70,6 +70,24 @@
|
|||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1701680307,
|
||||||
|
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
131
flake.nix
131
flake.nix
@ -1,119 +1,38 @@
|
|||||||
{
|
{
|
||||||
description = "My own sheet-organizer using rust and relm4 (and nix)";
|
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
|
naersk.url = "github:nix-community/naersk/master";
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
utils.url = "github:numtide/flake-utils";
|
||||||
crane.url = "github:ipetkov/crane";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs = { self, nixpkgs, utils, naersk }:
|
||||||
{
|
utils.lib.eachDefaultSystem (system:
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
flake-utils,
|
|
||||||
crane,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
let
|
||||||
packageOutputs = flake-utils.lib.eachDefaultSystem (
|
pkgs = import nixpkgs { inherit system; };
|
||||||
system:
|
naersk-lib = pkgs.callPackage naersk { };
|
||||||
let
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
|
|
||||||
craneLib = crane.mkLib pkgs;
|
# Needed at compile time (on build system)
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
dbMigrationsFilter = path: _type: builtins.match ".*sql$" path != null;
|
|
||||||
dbMigrationsOrCargoFilter =
|
|
||||||
path: type: (dbMigrationsFilter path type) || (craneLib.filterCargoSources path type);
|
|
||||||
|
|
||||||
dbMigrations = pkgs.lib.cleanSourceWith {
|
|
||||||
src = craneLib.path ./db-migrations; # The original, unfiltered source
|
|
||||||
filter = dbMigrationsFilter;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Common arguments can be set here to avoid repeating them later
|
|
||||||
# Note: changes here will rebuild all dependency crates
|
|
||||||
commonArgs = rec {
|
|
||||||
strictDeps = true; # When this is not set, all dependency crates will be compiled again
|
|
||||||
src = pkgs.lib.cleanSourceWith {
|
|
||||||
src = craneLib.path ./.; # The original, unfiltered source
|
|
||||||
filter = dbMigrationsOrCargoFilter;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Add icons.toml to $src when compiling dependencies (needed by relm4-icons)
|
|
||||||
extraDummyScript = ''
|
|
||||||
cp --no-preserve=mode,ownership ${./icons.toml} $out/icons.toml
|
|
||||||
'';
|
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [ pkg-config ];
|
|
||||||
|
|
||||||
buildInputs =
|
|
||||||
with pkgs;
|
|
||||||
[
|
|
||||||
gtk4
|
gtk4
|
||||||
]
|
pkg-config
|
||||||
++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
|
# wrapGAppsHook
|
||||||
# Additional darwin specific inputs can be set here
|
|
||||||
pkgs.libiconv
|
|
||||||
];
|
];
|
||||||
|
# Needed at runtime (on run system)
|
||||||
|
buildInputs = with pkgs; [ ];
|
||||||
|
in rec {
|
||||||
|
defaultPackage = naersk-lib.buildPackage {
|
||||||
|
src = ./.;
|
||||||
|
inherit buildInputs;
|
||||||
|
inherit nativeBuildInputs;
|
||||||
};
|
};
|
||||||
|
devShell = with pkgs;
|
||||||
|
mkShell {
|
||||||
|
buildInputs =
|
||||||
|
[ cargo rustc rustfmt pre-commit rustPackages.clippy ];
|
||||||
|
# Without inheriting nativeBuildinputs, cargo build will fail but that is good since we want to use only nix build
|
||||||
|
# inherit nativeBuildInputs;
|
||||||
|
|
||||||
# Build *just* the cargo dependencies, so we can reuse
|
RUST_SRC_PATH = rustPlatform.rustLibSrc;
|
||||||
# all of that work (e.g. via cachix) when running in CI
|
|
||||||
cargoArtifacts = craneLib.buildDepsOnly (commonArgs);
|
|
||||||
|
|
||||||
# Run clippy (and deny all warnings) on the crate source,
|
|
||||||
# reusing the dependency artifacts (e.g. from build scripts or
|
|
||||||
# proc-macros) from above.
|
|
||||||
#
|
|
||||||
# Note that this is done as a separate derivation so it
|
|
||||||
# does not impact building just the crate by itself.
|
|
||||||
myCrateClippy = craneLib.cargoClippy (
|
|
||||||
commonArgs
|
|
||||||
// {
|
|
||||||
# Again we apply some extra arguments only to this derivation
|
|
||||||
# and not every where else. In this case we add some clippy flags
|
|
||||||
inherit cargoArtifacts;
|
|
||||||
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
# Build the actual crate itself, reusing the dependency
|
|
||||||
# artifacts from above.
|
|
||||||
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
|
|
||||||
# track of code coverage
|
|
||||||
myCrateCoverage = craneLib.cargoTarpaulin (commonArgs // { inherit cargoArtifacts; });
|
|
||||||
|
|
||||||
in
|
|
||||||
{
|
|
||||||
packages.default = myCrate;
|
|
||||||
checks = {
|
|
||||||
inherit
|
|
||||||
# Build the crate as part of `nix flake check` for convenience
|
|
||||||
myCrate
|
|
||||||
myCrateClippy
|
|
||||||
myCrateCoverage
|
|
||||||
;
|
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
in
|
|
||||||
packageOutputs;
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
# Recommended: Specify your app ID *OR* your base resource path for more robust icon loading
|
|
||||||
base_resource_path = "/org/gtkrs/"
|
|
||||||
|
|
||||||
# List of icon names you found (shipped with this crate)
|
|
||||||
# Note: the file ending `-symbolic.svg` isn't part of the icon name.
|
|
||||||
icons = ["refresh", "edit", "arrow-sort-regular", "playlist-shuffle", "user-trash", "open-filled", "document-settings-filled"]
|
|
||||||
|
|
||||||
# Optional: Specify a folder containing your own SVG icons
|
|
||||||
# icon_folder = "my_svg_icons"
|
|
@ -1,6 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Terminal=false
|
|
||||||
Name=Sheet Organizer
|
|
||||||
Icon=sheet-organizer
|
|
||||||
Exec=sheet-organizer
|
|
Binary file not shown.
Before Width: | Height: | Size: 293 KiB |
@ -1,39 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
133
src/main.rs
133
src/main.rs
@ -1,70 +1,131 @@
|
|||||||
mod config;
|
|
||||||
mod database;
|
mod database;
|
||||||
mod sheet;
|
mod sheet;
|
||||||
mod sheet_dao;
|
mod sheet_dao;
|
||||||
mod sheet_validation;
|
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
use std::{path::PathBuf, process};
|
use std::{
|
||||||
|
path::{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, warn};
|
use log::{debug, error};
|
||||||
use relm4::RelmApp;
|
use relm4::RelmApp;
|
||||||
|
use sheet::{Pdf, Sheet};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::ui::app::{AppInitData, AppModel};
|
use crate::ui::app::{AppInitData, AppModel};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about)]
|
#[command(author, version, about)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
working_directory: Option<PathBuf>,
|
directory: 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();
|
||||||
// Overwrite config by cli options if specified
|
if !cli.directory.is_dir() {
|
||||||
if cli.working_directory.is_some() {
|
error!("Sheet folder path is no dir or does not exist");
|
||||||
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(working_directory.join("database.sqlite"))
|
let database = Database::setup(cli.directory.join("database.sqlite"))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let sheets = sheet_dao::fetch_all_sheets(&database).await.unwrap();
|
||||||
|
|
||||||
let sheets = sheet_validation::load_and_validate_sheets(&database, &working_directory).await;
|
debug!("Validating sheets from database...");
|
||||||
|
let mut validation_result = validate_sheet_files(sheets, &cli.directory);
|
||||||
|
debug!("{}", validation_result.get_stats()); // TODO: handle invalidated files
|
||||||
|
for updated in validation_result.updated_sheets.iter() {
|
||||||
|
sheet_dao::update_sheet_path(&database, updated)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sheets = validation_result.validated_sheets;
|
||||||
|
sheets.append(&mut validation_result.updated_sheets);
|
||||||
|
|
||||||
|
debug!("Inserting unassigned files into orphan table...");
|
||||||
|
for unassigned in validation_result.unassigned_files {
|
||||||
|
let orphan = sheet_dao::insert_file_as_orphan(&database, unassigned)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
sheets.push(orphan);
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_init_data = AppInitData { sheets, database };
|
||||||
|
|
||||||
let app_init_data = AppInitData {
|
|
||||||
sheets,
|
|
||||||
database,
|
|
||||||
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
|
||||||
app.with_args(Vec::new())
|
app.with_args(Vec::new())
|
||||||
.run_async::<AppModel>(app_init_data);
|
.run_async::<AppModel>(app_init_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FileValidationResult {
|
||||||
|
validated_sheets: Vec<Sheet>,
|
||||||
|
invalidated_sheets: Vec<Sheet>,
|
||||||
|
updated_sheets: Vec<Sheet>,
|
||||||
|
|
||||||
|
unassigned_files: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileValidationResult {
|
||||||
|
fn get_stats(&self) -> String {
|
||||||
|
format!("Validated sheets: {}\nInvalidated sheets: {}\nUpdated sheets: {}\nUnassigned files: {}",
|
||||||
|
self.validated_sheets.len(), self.invalidated_sheets.len(), self.updated_sheets.len(),
|
||||||
|
self.unassigned_files.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_sheet_files(sheets: Vec<Sheet>, dir: impl AsRef<Path>) -> FileValidationResult {
|
||||||
|
let (validated_sheets, mut invalidated_sheets): (Vec<_>, Vec<_>) = sheets
|
||||||
|
.into_iter()
|
||||||
|
.partition(|sheet| sheet.validate_own_path().unwrap_or(false));
|
||||||
|
|
||||||
|
let mut updated_sheets = Vec::new();
|
||||||
|
let mut unassigned_files = Vec::new();
|
||||||
|
|
||||||
|
// TODO: improve performance?
|
||||||
|
for pdf_file in find_all_pdfs_in_directory_recursive(dir) {
|
||||||
|
if let Some((i, _)) = invalidated_sheets
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, sheet)| sheet.validate_path(&pdf_file).unwrap_or(false))
|
||||||
|
{
|
||||||
|
let mut sheet = invalidated_sheets.remove(i);
|
||||||
|
let new_pdf = Pdf::try_from(pdf_file).unwrap();
|
||||||
|
sheet.update_pdf_file(new_pdf);
|
||||||
|
updated_sheets.push(sheet);
|
||||||
|
} else if !validated_sheets
|
||||||
|
.iter()
|
||||||
|
.any(|sheet| sheet.pdf_path_equal(&pdf_file))
|
||||||
|
{
|
||||||
|
unassigned_files.push(pdf_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileValidationResult {
|
||||||
|
validated_sheets,
|
||||||
|
invalidated_sheets,
|
||||||
|
updated_sheets,
|
||||||
|
unassigned_files,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_all_pdfs_in_directory_recursive(dir: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
|
||||||
|
WalkDir::new(dir)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.filter(|file| file.file_type().is_file())
|
||||||
|
.map(|file| file.into_path())
|
||||||
|
.filter(|path| {
|
||||||
|
path.extension()
|
||||||
|
.map(|s| s.to_string_lossy().to_ascii_lowercase() == "pdf")
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
104
src/sheet.rs
104
src/sheet.rs
@ -1,14 +1,16 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
ffi::OsStr,
|
ffi::{OsStr, OsString},
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
use log::debug;
|
use sqlx::database;
|
||||||
use strum_macros::{EnumDiscriminants, EnumIter};
|
use strum_macros::{EnumDiscriminants, EnumIter};
|
||||||
|
|
||||||
|
use crate::{database::Database, sheet_dao};
|
||||||
|
|
||||||
pub trait PdfSheet {
|
pub trait PdfSheet {
|
||||||
fn get_pdf(&self) -> &Pdf;
|
fn get_pdf(&self) -> &Pdf;
|
||||||
}
|
}
|
||||||
@ -18,24 +20,31 @@ pub struct Sheet {
|
|||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub last_opened: I64DateTime,
|
pub last_opened: I64DateTime,
|
||||||
pub kind: SheetKind,
|
pub kind: SheetKind,
|
||||||
pub pdf: Pdf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants)]
|
#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants)]
|
||||||
#[strum_discriminants(derive(EnumIter))]
|
#[strum_discriminants(derive(EnumIter))]
|
||||||
pub enum SheetKind {
|
pub enum SheetKind {
|
||||||
Sheet {
|
Sheet {
|
||||||
|
pdf: Pdf,
|
||||||
name: String,
|
name: String,
|
||||||
composer_id: i64,
|
composer_id: i64,
|
||||||
first_page: i64,
|
|
||||||
book_id: Option<i64>,
|
|
||||||
},
|
},
|
||||||
Orphan,
|
Orphan {
|
||||||
|
pdf: Pdf,
|
||||||
|
},
|
||||||
Book {
|
Book {
|
||||||
|
pdf: Pdf,
|
||||||
name: String,
|
name: String,
|
||||||
composer_id: i64,
|
composer_id: i64,
|
||||||
sheet_ids: Vec<i64>,
|
sheet_ids: Vec<i64>,
|
||||||
},
|
},
|
||||||
|
BookSheet {
|
||||||
|
name: String,
|
||||||
|
book_id: i64,
|
||||||
|
first_page: i64,
|
||||||
|
last_page: i64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for Sheet {
|
impl PartialOrd for Sheet {
|
||||||
@ -51,34 +60,62 @@ impl Ord for Sheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Sheet {
|
impl Sheet {
|
||||||
pub fn construct_xopp_file_path(&self) -> PathBuf {
|
pub async fn open_file(&self, database: &Database) {
|
||||||
let mut xopp_path = self.pdf.path.with_extension("").into_os_string();
|
let path = match &self.kind {
|
||||||
xopp_path.push(".xopp");
|
SheetKind::Sheet { pdf, .. } => pdf.path.clone(),
|
||||||
PathBuf::from(xopp_path)
|
SheetKind::Orphan { pdf } => pdf.path.clone(),
|
||||||
|
SheetKind::Book { pdf, .. } => pdf.path.clone(),
|
||||||
|
SheetKind::BookSheet { book_id, .. } => sheet_dao::find_path_of_book(database, book_id)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
opener::open(path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn construct_annotated_file_path(&self) -> PathBuf {
|
pub fn update_pdf_file(&mut self, new_pdf: Pdf) -> std::io::Result<()> {
|
||||||
let mut annotated_path = self.pdf.path.with_extension("").into_os_string();
|
match &mut self.kind {
|
||||||
annotated_path.push("_annotated.pdf");
|
SheetKind::Sheet { pdf, .. } => *pdf = new_pdf,
|
||||||
PathBuf::from(annotated_path)
|
SheetKind::Orphan { pdf, .. } => *pdf = new_pdf,
|
||||||
|
SheetKind::Book { pdf, .. } => *pdf = new_pdf,
|
||||||
|
SheetKind::BookSheet { .. } => {} // TODO: find better solution!
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_file_or_annotated_version_if_exists(&self) {
|
pub fn pdf_path_equal(&self, path: impl AsRef<Path>) -> bool {
|
||||||
let annotated_version = self.construct_annotated_file_path();
|
match &self.kind {
|
||||||
if annotated_version.exists() {
|
SheetKind::Sheet { pdf, .. } => pdf.path == path.as_ref(),
|
||||||
// TODO: open on first_page
|
SheetKind::Orphan { pdf, .. } => pdf.path == path.as_ref(),
|
||||||
opener::open(annotated_version).unwrap();
|
SheetKind::Book { pdf, .. } => pdf.path == path.as_ref(),
|
||||||
} else {
|
SheetKind::BookSheet { .. } => false, // TODO: find better solution!
|
||||||
// TODO: open on first_page
|
|
||||||
opener::open(&self.pdf.path).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_part_of_book(&self) -> bool {
|
pub fn validate_own_path(&self) -> std::io::Result<bool> {
|
||||||
if let SheetKind::Sheet { book_id, .. } = &self.kind {
|
Ok(match &self.kind {
|
||||||
return book_id.is_some();
|
SheetKind::Sheet { pdf, .. } => pdf.validate_path(&pdf.path)?,
|
||||||
|
SheetKind::Orphan { pdf, .. } => pdf.validate_path(&pdf.path)?,
|
||||||
|
SheetKind::Book { pdf, .. } => pdf.validate_path(&pdf.path)?,
|
||||||
|
SheetKind::BookSheet { book_id, .. } => true, // TODO: better solution?
|
||||||
|
})
|
||||||
}
|
}
|
||||||
false
|
|
||||||
|
pub fn try_get_path(&self) -> Option<&Path> {
|
||||||
|
match &self.kind {
|
||||||
|
SheetKind::Sheet { pdf, .. } => Some(&pdf.path),
|
||||||
|
SheetKind::Orphan { pdf, .. } => Some(&pdf.path),
|
||||||
|
SheetKind::Book { pdf, .. } => Some(&pdf.path),
|
||||||
|
SheetKind::BookSheet { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_path(&self, path: impl AsRef<Path>) -> std::io::Result<bool> {
|
||||||
|
Ok(match &self.kind {
|
||||||
|
SheetKind::Sheet { pdf, .. } => pdf.validate_path(path)?,
|
||||||
|
SheetKind::Orphan { pdf, .. } => pdf.validate_path(path)?,
|
||||||
|
SheetKind::Book { pdf, .. } => pdf.validate_path(path)?,
|
||||||
|
SheetKind::BookSheet { book_id, .. } => true, // TODO: better solution?
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,14 +125,15 @@ impl SheetKindDiscriminants {
|
|||||||
SheetKindDiscriminants::Sheet => "sheets",
|
SheetKindDiscriminants::Sheet => "sheets",
|
||||||
SheetKindDiscriminants::Orphan => "orphans",
|
SheetKindDiscriminants::Orphan => "orphans",
|
||||||
SheetKindDiscriminants::Book => "books",
|
SheetKindDiscriminants::Book => "books",
|
||||||
|
SheetKindDiscriminants::BookSheet => "booksheets",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
pub struct Composer {
|
pub struct Composer {
|
||||||
pub id: i64,
|
id: i64,
|
||||||
pub name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
@ -106,7 +144,9 @@ impl TryFrom<i64> for I64DateTime {
|
|||||||
|
|
||||||
fn try_from(value: i64) -> Result<Self, Self::Error> {
|
fn try_from(value: i64) -> Result<Self, Self::Error> {
|
||||||
Ok(I64DateTime(
|
Ok(I64DateTime(
|
||||||
DateTime::<Utc>::from_timestamp(value, 0).ok_or("Failed converting i64 to DateTime")?,
|
NaiveDateTime::from_timestamp_opt(value, 0)
|
||||||
|
.ok_or("Failed converting i64 to DateTime")?
|
||||||
|
.and_utc(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,10 +169,6 @@ impl Pdf {
|
|||||||
self.path.file_name().unwrap().to_str().unwrap()
|
self.path.file_name().unwrap().to_str().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_own_path(&self) -> std::io::Result<bool> {
|
|
||||||
self.validate_path(&self.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_path(&self, path: impl AsRef<Path>) -> std::io::Result<bool> {
|
pub fn validate_path(&self, path: impl AsRef<Path>) -> std::io::Result<bool> {
|
||||||
// First compare file size since it is faster than hashing
|
// First compare file size since it is faster than hashing
|
||||||
let file_size = fs::metadata(path.as_ref())?.len();
|
let file_size = fs::metadata(path.as_ref())?.len();
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use strum::{EnumMessage, IntoEnumIterator};
|
||||||
|
use strum_macros::{EnumDiscriminants, EnumIter, EnumString};
|
||||||
|
|
||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
use sqlx::{sqlite::SqliteRow, Row, SqlitePool};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::Database,
|
database::Database,
|
||||||
sheet::{Composer, I64DateTime, Pdf, Sheet, SheetKind, SheetKindDiscriminants},
|
sheet::{I64DateTime, Pdf, Sheet, SheetKind, SheetKindDiscriminants},
|
||||||
};
|
};
|
||||||
use sqlx::{sqlite::SqliteRow, Row};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use strum::IntoEnumIterator;
|
|
||||||
|
|
||||||
pub async fn insert_file_as_orphan(
|
pub async fn insert_file_as_orphan(
|
||||||
database: &Database,
|
database: &Database,
|
||||||
file: impl AsRef<Path>,
|
file: impl AsRef<Path>,
|
||||||
) -> sqlx::Result<Sheet> {
|
) -> sqlx::Result<Sheet> {
|
||||||
let pdf = Pdf::try_from(file.as_ref().to_path_buf()).unwrap();
|
let pdf = Pdf::try_from(file.as_ref().to_path_buf()).unwrap();
|
||||||
let last_opened = chrono::offset::Utc::now();
|
let last_opened = DateTime::<Utc>::default();
|
||||||
|
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
"
|
"
|
||||||
@ -31,30 +35,31 @@ pub async fn insert_file_as_orphan(
|
|||||||
|
|
||||||
Ok(Sheet {
|
Ok(Sheet {
|
||||||
id,
|
id,
|
||||||
pdf,
|
|
||||||
last_opened: I64DateTime(last_opened),
|
last_opened: I64DateTime(last_opened),
|
||||||
kind: SheetKind::Orphan,
|
kind: SheetKind::Orphan { pdf },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub async fn find_path_of_book(database: &Database, book_id: &i64) -> sqlx::Result<PathBuf> {
|
pub async fn find_path_of_book(database: &Database, book_id: &i64) -> sqlx::Result<PathBuf> {
|
||||||
// sqlx::query("SELECT path FROM books WHERE id = $1")
|
sqlx::query("SELECT path FROM books WHERE id = $1")
|
||||||
// .bind(book_id)
|
.bind(book_id)
|
||||||
// .map(|row: SqliteRow| PathBuf::try_from(row.try_get::<String, _>("path").unwrap()).unwrap())
|
.map(|row: SqliteRow| PathBuf::try_from(row.try_get::<String, _>("path").unwrap()).unwrap())
|
||||||
// .fetch_one(&database.connection)
|
.fetch_one(&database.connection)
|
||||||
// .await
|
.await
|
||||||
// }
|
}
|
||||||
|
|
||||||
pub async fn update_sheet_path(database: &Database, sheet: &Sheet) -> sqlx::Result<()> {
|
pub async fn update_sheet_path(database: &Database, sheet: &Sheet) -> sqlx::Result<()> {
|
||||||
// TODO: when updating book or sheet of book, update all
|
if let Some(path) = sheet.try_get_path() {
|
||||||
let sheet_kind = SheetKindDiscriminants::from(&sheet.kind);
|
let sheet_kind = SheetKindDiscriminants::from(&sheet.kind);
|
||||||
let table = sheet_kind.get_database_table_name();
|
let table = sheet_kind.get_database_table_name();
|
||||||
sqlx::query(&format!("UPDATE {} SET path = $1 WHERE id = $2", table))
|
return sqlx::query(&format!("UPDATE {} SET path = $1 WHERE id = $2", table))
|
||||||
.bind(sheet.pdf.path.to_str().unwrap().to_string())
|
.bind(path.to_str().unwrap().to_string())
|
||||||
.bind(sheet.id)
|
.bind(sheet.id)
|
||||||
.execute(&database.connection)
|
.execute(&database.connection)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ());
|
||||||
|
}
|
||||||
|
Ok(()) // TODO: error on else?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_sheet_last_opened(database: &Database, sheet: &Sheet) -> sqlx::Result<()> {
|
pub async fn update_sheet_last_opened(database: &Database, sheet: &Sheet) -> sqlx::Result<()> {
|
||||||
@ -71,29 +76,6 @@ pub async fn update_sheet_last_opened(database: &Database, sheet: &Sheet) -> sql
|
|||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_composer_by_id(database: &Database, id: i64) -> sqlx::Result<Composer> {
|
|
||||||
sqlx::query(&format!("SELECT * FROM {} WHERE id = {}", "composers", id))
|
|
||||||
.map(|row: SqliteRow| Composer {
|
|
||||||
id,
|
|
||||||
name: row.try_get("name").unwrap(),
|
|
||||||
})
|
|
||||||
.fetch_one(&database.connection)
|
|
||||||
.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();
|
||||||
|
|
||||||
@ -105,7 +87,6 @@ pub async fn fetch_all_sheets(database: &Database) -> sqlx::Result<Vec<Sheet>> {
|
|||||||
id: row.try_get("id").unwrap(),
|
id: row.try_get("id").unwrap(),
|
||||||
last_opened: I64DateTime::try_from(row.try_get::<i64, _>("last_opened").unwrap())
|
last_opened: I64DateTime::try_from(row.try_get::<i64, _>("last_opened").unwrap())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
pdf: parse_pdf_from_row(&row).unwrap(),
|
|
||||||
kind: parse_kind_from_row(kind, row).unwrap(),
|
kind: parse_kind_from_row(kind, row).unwrap(),
|
||||||
})
|
})
|
||||||
.fetch_all(&database.connection)
|
.fetch_all(&database.connection)
|
||||||
@ -122,22 +103,30 @@ fn parse_kind_from_row(kind: SheetKindDiscriminants, row: SqliteRow) -> sqlx::Re
|
|||||||
SheetKindDiscriminants::Sheet => SheetKind::Sheet {
|
SheetKindDiscriminants::Sheet => SheetKind::Sheet {
|
||||||
name: row.try_get("name")?,
|
name: row.try_get("name")?,
|
||||||
composer_id: row.try_get("composer_id")?,
|
composer_id: row.try_get("composer_id")?,
|
||||||
first_page: row.try_get("first_page")?,
|
pdf: parse_pdf_from_row(&row)?,
|
||||||
book_id: row.try_get("book_id").ok(),
|
},
|
||||||
|
SheetKindDiscriminants::Orphan => SheetKind::Orphan {
|
||||||
|
pdf: parse_pdf_from_row(&row)?,
|
||||||
},
|
},
|
||||||
SheetKindDiscriminants::Orphan => SheetKind::Orphan,
|
|
||||||
SheetKindDiscriminants::Book => SheetKind::Book {
|
SheetKindDiscriminants::Book => SheetKind::Book {
|
||||||
name: row.try_get("name")?,
|
name: row.try_get("name")?,
|
||||||
composer_id: row.try_get("composer_id")?,
|
composer_id: row.try_get("composer_id")?,
|
||||||
|
pdf: parse_pdf_from_row(&row)?,
|
||||||
sheet_ids: sheet_ids_from_string(row.try_get("sheet_ids").unwrap()),
|
sheet_ids: sheet_ids_from_string(row.try_get("sheet_ids").unwrap()),
|
||||||
},
|
},
|
||||||
|
SheetKindDiscriminants::BookSheet => SheetKind::BookSheet {
|
||||||
|
name: row.try_get("name")?,
|
||||||
|
book_id: row.try_get("book_id")?,
|
||||||
|
first_page: row.try_get("first_page")?,
|
||||||
|
last_page: row.try_get("last_page")?,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sheet_ids_from_string(s: String) -> Vec<i64> {
|
fn sheet_ids_from_string(s: String) -> Vec<i64> {
|
||||||
s.trim()
|
s.trim()
|
||||||
.split(',')
|
.split(",")
|
||||||
.map(|s| s.parse::<i64>().unwrap())
|
.map(|s| i64::from_str_radix(s, 10).unwrap())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,115 +0,0 @@
|
|||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use log::debug;
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::Database,
|
|
||||||
sheet::{Pdf, Sheet},
|
|
||||||
sheet_dao,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn load_and_validate_sheets(
|
|
||||||
database: &Database,
|
|
||||||
directory: impl AsRef<Path>,
|
|
||||||
) -> Vec<Sheet> {
|
|
||||||
sheet_dao::remove_duplicate_sheets(database).await.unwrap();
|
|
||||||
|
|
||||||
let sheets = sheet_dao::fetch_all_sheets(database).await.unwrap();
|
|
||||||
|
|
||||||
debug!("Validating sheets from database...");
|
|
||||||
let mut validation_result = validate_sheet_files(sheets, directory);
|
|
||||||
debug!("{}", validation_result.get_stats()); // TODO: handle invalidated files
|
|
||||||
for updated in validation_result.updated_sheets.iter() {
|
|
||||||
sheet_dao::update_sheet_path(database, updated)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut sheets = validation_result.validated_sheets;
|
|
||||||
sheets.append(&mut validation_result.updated_sheets);
|
|
||||||
|
|
||||||
debug!("Inserting unassigned files into orphan table...");
|
|
||||||
for unassigned in validation_result.unassigned_files {
|
|
||||||
let orphan = sheet_dao::insert_file_as_orphan(database, unassigned)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
sheets.push(orphan);
|
|
||||||
}
|
|
||||||
|
|
||||||
sheets
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FileValidationResult {
|
|
||||||
validated_sheets: Vec<Sheet>,
|
|
||||||
invalidated_sheets: Vec<Sheet>,
|
|
||||||
updated_sheets: Vec<Sheet>,
|
|
||||||
|
|
||||||
unassigned_files: Vec<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileValidationResult {
|
|
||||||
fn get_stats(&self) -> String {
|
|
||||||
format!("Validated sheets: {}\nInvalidated sheets: {}\nUpdated sheets: {}\nUnassigned files: {}",
|
|
||||||
self.validated_sheets.len(), self.invalidated_sheets.len(), self.updated_sheets.len(),
|
|
||||||
self.unassigned_files.len())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_sheet_files(sheets: Vec<Sheet>, dir: impl AsRef<Path>) -> FileValidationResult {
|
|
||||||
let (validated_sheets, mut invalidated_sheets): (Vec<_>, Vec<_>) = sheets
|
|
||||||
.into_iter()
|
|
||||||
.partition(|sheet| sheet.pdf.validate_own_path().unwrap_or(false));
|
|
||||||
|
|
||||||
let mut updated_sheets = Vec::new();
|
|
||||||
let mut unassigned_files = Vec::new();
|
|
||||||
|
|
||||||
// TODO: improve performance?
|
|
||||||
for pdf_file in find_all_pdfs_in_directory_recursive(dir) {
|
|
||||||
// Make sure annotated files are not handled (they are then only opened if existent)
|
|
||||||
if pdf_file
|
|
||||||
.file_name()
|
|
||||||
.unwrap()
|
|
||||||
.to_string_lossy()
|
|
||||||
.ends_with("_annotated.pdf")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((i, _)) = invalidated_sheets
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, sheet)| sheet.pdf.validate_path(&pdf_file).unwrap_or(false))
|
|
||||||
{
|
|
||||||
let mut sheet = invalidated_sheets.remove(i);
|
|
||||||
let new_pdf = Pdf::try_from(pdf_file).unwrap();
|
|
||||||
sheet.pdf = new_pdf;
|
|
||||||
updated_sheets.push(sheet);
|
|
||||||
} else if !validated_sheets
|
|
||||||
.iter()
|
|
||||||
.any(|sheet| sheet.pdf.path == pdf_file)
|
|
||||||
{
|
|
||||||
unassigned_files.push(pdf_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileValidationResult {
|
|
||||||
validated_sheets,
|
|
||||||
invalidated_sheets,
|
|
||||||
updated_sheets,
|
|
||||||
unassigned_files,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_all_pdfs_in_directory_recursive(dir: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
|
|
||||||
WalkDir::new(dir)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|e| e.ok())
|
|
||||||
.filter(|file| file.file_type().is_file())
|
|
||||||
.map(|file| file.into_path())
|
|
||||||
.filter(|path| {
|
|
||||||
path.extension()
|
|
||||||
.map(|s| s.to_string_lossy().to_ascii_lowercase() == "pdf")
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
}
|
|
161
src/ui/app.rs
161
src/ui/app.rs
@ -1,60 +1,38 @@
|
|||||||
use std::{path::PathBuf, process::Command, sync::Arc};
|
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use relm4::{
|
use relm4::{
|
||||||
component::{AsyncComponent, AsyncComponentParts, AsyncController},
|
component::{AsyncComponent, AsyncComponentParts},
|
||||||
gtk::{gdk, Adjustment},
|
|
||||||
prelude::*,
|
prelude::*,
|
||||||
AsyncComponentSender,
|
AsyncComponentSender,
|
||||||
};
|
};
|
||||||
use relm4_icons::icon_names;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{self, Database},
|
database::Database,
|
||||||
sheet::{I64DateTime, Sheet},
|
sheet::{I64DateTime, Sheet},
|
||||||
sheet_dao, sheet_validation,
|
sheet_dao,
|
||||||
ui::mcdu::McduOutput,
|
ui::mcdu::McduOutput,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
mcdu::McduModel,
|
mcdu::McduModel,
|
||||||
sheet_edit_dialog::{SheetEditDialogInit, SheetEditDialogModel},
|
sheet_listing::{SheetListingInput, SheetListingModel},
|
||||||
sheet_listing::{SheetListingInput, SheetListingModel, SheetListingOutput},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct AppModel {
|
pub struct AppModel {
|
||||||
database: Arc<Database>,
|
database: Database,
|
||||||
directory: Arc<PathBuf>,
|
|
||||||
mcdu: Controller<McduModel>,
|
mcdu: Controller<McduModel>,
|
||||||
sheets_listing: Controller<SheetListingModel>,
|
sheets_listing: Controller<SheetListingModel>,
|
||||||
click_mode: ClickMode,
|
|
||||||
scroll_adjustment: Adjustment,
|
|
||||||
sheet_edit_dialog: Option<AsyncController<SheetEditDialogModel>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ClickMode {
|
|
||||||
Open,
|
|
||||||
Edit,
|
|
||||||
Annotate,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AppInput {
|
pub enum AppInput {
|
||||||
SearchStarted(String),
|
SearchStarted(String),
|
||||||
SheetPressed(Sheet),
|
SheetPressed(Sheet),
|
||||||
Refresh,
|
|
||||||
Sort,
|
|
||||||
Shuffle,
|
|
||||||
SetClickMode(ClickMode),
|
|
||||||
SheetListingContentsChanged,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppInitData {
|
pub struct AppInitData {
|
||||||
pub sheets: Vec<Sheet>,
|
pub sheets: Vec<Sheet>,
|
||||||
pub database: Database,
|
pub database: Database,
|
||||||
pub directory: PathBuf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::component(pub, async)]
|
#[relm4::component(pub, async)]
|
||||||
@ -62,7 +40,7 @@ impl AsyncComponent for AppModel {
|
|||||||
type Input = AppInput;
|
type Input = AppInput;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
type Init = AppInitData;
|
type Init = AppInitData;
|
||||||
type CommandOutput = Vec<Sheet>;
|
type CommandOutput = ();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
#[root]
|
#[root]
|
||||||
@ -76,49 +54,10 @@ impl AsyncComponent for AppModel {
|
|||||||
gtk::Box {
|
gtk::Box {
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
set_hexpand: true,
|
set_hexpand: true,
|
||||||
gtk::Box {
|
|
||||||
set_orientation: gtk::Orientation::Horizontal,
|
|
||||||
set_margin_all: 10,
|
|
||||||
set_spacing: 3,
|
|
||||||
gtk::Button {
|
|
||||||
set_icon_name: icon_names::REFRESH,
|
|
||||||
set_margin_end: 10,
|
|
||||||
connect_clicked[sender] => move |_| sender.input(AppInput::Refresh),
|
|
||||||
},
|
|
||||||
#[name = "button_sort"]
|
|
||||||
gtk::ToggleButton {
|
|
||||||
set_icon_name: icon_names::ARROW_SORT_REGULAR,
|
|
||||||
set_active: true,
|
|
||||||
connect_clicked[sender] => move |_| sender.input(AppInput::Sort),
|
|
||||||
},
|
|
||||||
gtk::ToggleButton {
|
|
||||||
set_icon_name: icon_names::PLAYLIST_SHUFFLE,
|
|
||||||
set_group: Some(&button_sort),
|
|
||||||
set_margin_end: 10,
|
|
||||||
connect_clicked[sender] => move |_| sender.input(AppInput::Shuffle),
|
|
||||||
},
|
|
||||||
#[name = "button_open"]
|
|
||||||
gtk::ToggleButton {
|
|
||||||
set_icon_name: icon_names::OPEN_FILLED,
|
|
||||||
set_active: true,
|
|
||||||
connect_clicked[sender] => move |button| if button.is_active() { sender.input(AppInput::SetClickMode(ClickMode::Open)) },
|
|
||||||
},
|
|
||||||
gtk::ToggleButton {
|
|
||||||
set_icon_name: icon_names::DOCUMENT_SETTINGS_FILLED,
|
|
||||||
set_group: Some(&button_open),
|
|
||||||
connect_clicked[sender] => move |button| if button.is_active() { sender.input(AppInput::SetClickMode(ClickMode::Edit)) },
|
|
||||||
},
|
|
||||||
gtk::ToggleButton {
|
|
||||||
set_icon_name: icon_names::EDIT,
|
|
||||||
set_group: Some(&button_open),
|
|
||||||
connect_clicked[sender] => move |button| if button.is_active() { sender.input(AppInput::SetClickMode(ClickMode::Annotate)) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gtk::ScrolledWindow {
|
gtk::ScrolledWindow {
|
||||||
model.sheets_listing.widget(),
|
model.sheets_listing.widget(),
|
||||||
set_vexpand: true,
|
set_vexpand: true,
|
||||||
set_hexpand: true,
|
set_hexpand: true,
|
||||||
set_vadjustment: Some(&model.scroll_adjustment),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
model.mcdu.widget() {
|
model.mcdu.widget() {
|
||||||
@ -134,12 +73,6 @@ impl AsyncComponent for AppModel {
|
|||||||
sender: AsyncComponentSender<Self>,
|
sender: AsyncComponentSender<Self>,
|
||||||
) -> AsyncComponentParts<Self> {
|
) -> AsyncComponentParts<Self> {
|
||||||
relm4_icons::initialize_icons();
|
relm4_icons::initialize_icons();
|
||||||
gtk::init().unwrap();
|
|
||||||
let display = gdk::Display::default().unwrap();
|
|
||||||
let theme = gtk::IconTheme::for_display(&display);
|
|
||||||
|
|
||||||
theme.add_resource_path("/org/gtkrs/icons/");
|
|
||||||
// theme.add_resource_path("/org/gtkrs/icons/scalable/actions/");
|
|
||||||
|
|
||||||
let mcdu = McduModel::builder()
|
let mcdu = McduModel::builder()
|
||||||
.launch(())
|
.launch(())
|
||||||
@ -150,22 +83,16 @@ impl AsyncComponent for AppModel {
|
|||||||
let mut sheets = init_data.sheets;
|
let mut sheets = init_data.sheets;
|
||||||
sheets.sort_by(|a, b| a.cmp(b).reverse());
|
sheets.sort_by(|a, b| a.cmp(b).reverse());
|
||||||
|
|
||||||
let sheets_listing = SheetListingModel::builder().launch(sheets).forward(
|
let sheets_listing = SheetListingModel::builder()
|
||||||
sender.input_sender(),
|
.launch(sheets)
|
||||||
|response| match response {
|
.forward(sender.input_sender(), |response| {
|
||||||
SheetListingOutput::SheetModelSelected(sheet) => AppInput::SheetPressed(sheet),
|
AppInput::SheetPressed(response.sheet)
|
||||||
SheetListingOutput::ContentsChanged => AppInput::SheetListingContentsChanged,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let model = AppModel {
|
let model = AppModel {
|
||||||
database: Arc::new(init_data.database),
|
database: init_data.database,
|
||||||
directory: Arc::new(init_data.directory),
|
|
||||||
mcdu,
|
mcdu,
|
||||||
sheets_listing,
|
sheets_listing,
|
||||||
click_mode: ClickMode::Open,
|
|
||||||
scroll_adjustment: Adjustment::builder().build(),
|
|
||||||
sheet_edit_dialog: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
@ -176,8 +103,8 @@ impl AsyncComponent for AppModel {
|
|||||||
async fn update(
|
async fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
message: Self::Input,
|
message: Self::Input,
|
||||||
sender: AsyncComponentSender<Self>,
|
_sender: AsyncComponentSender<Self>,
|
||||||
root: &Self::Root,
|
_root: &Self::Root,
|
||||||
) {
|
) {
|
||||||
match message {
|
match message {
|
||||||
AppInput::SearchStarted(query) => {
|
AppInput::SearchStarted(query) => {
|
||||||
@ -185,62 +112,14 @@ impl AsyncComponent for AppModel {
|
|||||||
.emit(SheetListingInput::Query(query.clone()));
|
.emit(SheetListingInput::Query(query.clone()));
|
||||||
}
|
}
|
||||||
AppInput::SheetPressed(sheet) => {
|
AppInput::SheetPressed(sheet) => {
|
||||||
match self.click_mode {
|
sheet.open_file(&self.database).await;
|
||||||
ClickMode::Open => open_sheet(&sheet, &self.database).await,
|
// TODO: updating does not work
|
||||||
ClickMode::Edit => {
|
let mut sheet = sheet;
|
||||||
self.sheet_edit_dialog = Some(
|
|
||||||
SheetEditDialogModel::builder()
|
|
||||||
.transient_for(root)
|
|
||||||
.launch(SheetEditDialogInit {
|
|
||||||
sheet,
|
|
||||||
database: Arc::clone(&self.database),
|
|
||||||
})
|
|
||||||
.forward(sender.input_sender(), |_| todo!()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ClickMode::Annotate => annotate_sheet(&sheet).await,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
AppInput::Refresh => {
|
|
||||||
let db = Arc::clone(&self.database);
|
|
||||||
let dir = Arc::clone(&self.directory);
|
|
||||||
sender.oneshot_command(async move {
|
|
||||||
sheet_validation::load_and_validate_sheets(&db, dir.as_ref()).await
|
|
||||||
});
|
|
||||||
}
|
|
||||||
AppInput::Sort => self.sheets_listing.emit(SheetListingInput::Sort),
|
|
||||||
AppInput::Shuffle => self.sheets_listing.emit(SheetListingInput::Shuffle),
|
|
||||||
AppInput::SetClickMode(click_mode) => self.click_mode = click_mode,
|
|
||||||
AppInput::SheetListingContentsChanged => self.scroll_adjustment.set_value(0.0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_cmd(
|
|
||||||
&mut self,
|
|
||||||
message: Self::CommandOutput,
|
|
||||||
_sender: AsyncComponentSender<Self>,
|
|
||||||
_: &Self::Root,
|
|
||||||
) {
|
|
||||||
let mut sheets = message;
|
|
||||||
sheets.sort_by(|a, b| a.cmp(b).reverse());
|
|
||||||
|
|
||||||
self.sheets_listing
|
|
||||||
.emit(SheetListingInput::ReloadSheets(sheets));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn open_sheet(sheet: &Sheet, database: &Database) {
|
|
||||||
sheet.open_file_or_annotated_version_if_exists();
|
|
||||||
let mut sheet = sheet.to_owned();
|
|
||||||
sheet.last_opened = I64DateTime(Utc::now());
|
sheet.last_opened = I64DateTime(Utc::now());
|
||||||
sheet_dao::update_sheet_last_opened(database, &sheet)
|
sheet_dao::update_sheet_last_opened(&self.database, &sheet)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
async fn annotate_sheet(sheet: &Sheet) {
|
}
|
||||||
Command::new("xournalpp")
|
|
||||||
.arg(&sheet.pdf.path)
|
|
||||||
.spawn()
|
|
||||||
.expect("failed to execute process");
|
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ impl SimpleComponent for McduModel {
|
|||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
_init: Self::Init,
|
_init: Self::Init,
|
||||||
root: Self::Root,
|
root: &Self::Root,
|
||||||
sender: ComponentSender<Self>,
|
sender: ComponentSender<Self>,
|
||||||
) -> ComponentParts<Self> {
|
) -> ComponentParts<Self> {
|
||||||
let model = McduModel::new();
|
let model = McduModel::new();
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod mcdu;
|
pub mod mcdu;
|
||||||
pub mod sheet_edit_dialog;
|
|
||||||
pub mod sheet_listing;
|
pub mod sheet_listing;
|
||||||
pub mod sheet_model;
|
pub mod sheet_model;
|
||||||
|
@ -1,256 +0,0 @@
|
|||||||
use gtk::prelude::*;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use relm4::{
|
|
||||||
component::{AsyncComponent, AsyncComponentParts, Connector},
|
|
||||||
gtk::{
|
|
||||||
gio::ListStore,
|
|
||||||
glib::{self, GString, Type, Value},
|
|
||||||
EntryBuffer, EntryCompletion,
|
|
||||||
},
|
|
||||||
prelude::*,
|
|
||||||
AsyncComponentSender,
|
|
||||||
};
|
|
||||||
use relm4_components::alert::{Alert, AlertMsg, AlertSettings};
|
|
||||||
|
|
||||||
use crate::{database::Database, sheet::Sheet, sheet_dao};
|
|
||||||
|
|
||||||
pub struct SheetEditDialogModel {
|
|
||||||
database: Arc<Database>,
|
|
||||||
hidden: bool,
|
|
||||||
sheet: Option<Sheet>,
|
|
||||||
name_entry_buffer: EntryBuffer,
|
|
||||||
composer_entry_buffer: EntryBuffer,
|
|
||||||
composer_entry_completion: EntryCompletion,
|
|
||||||
is_book: bool,
|
|
||||||
book_sheets: Vec<(String, String, i64)>,
|
|
||||||
alert_empty_fields: Connector<Alert>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SheetEditDialogInit {
|
|
||||||
pub database: Arc<Database>,
|
|
||||||
pub sheet: Sheet,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum SheetEditDialogInput {
|
|
||||||
Accept,
|
|
||||||
Cancel,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum SheetEditDialogOutput {
|
|
||||||
SheetEdited(Sheet),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[relm4::component(pub, async)]
|
|
||||||
impl AsyncComponent for SheetEditDialogModel {
|
|
||||||
type Init = SheetEditDialogInit;
|
|
||||||
type Input = SheetEditDialogInput;
|
|
||||||
type Output = SheetEditDialogOutput;
|
|
||||||
type CommandOutput = ();
|
|
||||||
|
|
||||||
view! {
|
|
||||||
gtk::Window {
|
|
||||||
#[watch]
|
|
||||||
set_visible: !model.hidden,
|
|
||||||
set_modal: true,
|
|
||||||
set_title: Some("Edit sheet"),
|
|
||||||
set_default_width: 10,
|
|
||||||
set_default_height: 10,
|
|
||||||
gtk::Box {
|
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
|
||||||
set_margin_all : 15,
|
|
||||||
set_spacing: 5,
|
|
||||||
gtk::Box {
|
|
||||||
set_spacing: 10,
|
|
||||||
gtk::Label {
|
|
||||||
set_text: "Sheet name"
|
|
||||||
},
|
|
||||||
gtk::Entry {
|
|
||||||
set_buffer: &model.name_entry_buffer,
|
|
||||||
set_width_chars: 40,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gtk::Box {
|
|
||||||
set_spacing: 10,
|
|
||||||
gtk::Label {
|
|
||||||
set_text: "Sheet composer"
|
|
||||||
},
|
|
||||||
gtk::Entry {
|
|
||||||
set_buffer: &model.composer_entry_buffer,
|
|
||||||
set_completion: Some(&model.composer_entry_completion),
|
|
||||||
set_hexpand: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gtk::Box {
|
|
||||||
set_spacing: 10,
|
|
||||||
gtk::Label {
|
|
||||||
set_text: "Book"
|
|
||||||
},
|
|
||||||
gtk::CheckButton {
|
|
||||||
#[watch]
|
|
||||||
set_active: model.is_book,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gtk::Box {
|
|
||||||
set_orientation: gtk::Orientation::Horizontal,
|
|
||||||
set_margin_top: 10,
|
|
||||||
set_spacing: 10,
|
|
||||||
set_homogeneous: true,
|
|
||||||
set_halign: gtk::Align::Center,
|
|
||||||
set_hexpand: true,
|
|
||||||
|
|
||||||
gtk::Button {
|
|
||||||
set_label: "Cancel",
|
|
||||||
connect_clicked[sender] => move |_| sender.input(SheetEditDialogInput::Cancel)
|
|
||||||
},
|
|
||||||
gtk::Button {
|
|
||||||
set_label : "Confirm",
|
|
||||||
connect_clicked[sender] => move |_| sender.input(SheetEditDialogInput::Accept)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn init(
|
|
||||||
params: Self::Init,
|
|
||||||
root: Self::Root,
|
|
||||||
sender: AsyncComponentSender<Self>,
|
|
||||||
) -> AsyncComponentParts<Self> {
|
|
||||||
let sheet = params.sheet;
|
|
||||||
let mut sheet_name = String::new();
|
|
||||||
let mut sheet_composer = String::new();
|
|
||||||
let mut is_book = false;
|
|
||||||
|
|
||||||
match &sheet.kind {
|
|
||||||
crate::sheet::SheetKind::Sheet {
|
|
||||||
name, composer_id, ..
|
|
||||||
} => {
|
|
||||||
sheet_name = name.to_string();
|
|
||||||
if let Ok(composer) =
|
|
||||||
sheet_dao::get_composer_by_id(¶ms.database, *composer_id).await
|
|
||||||
{
|
|
||||||
sheet_composer = composer.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::sheet::SheetKind::Orphan => {
|
|
||||||
sheet_name = sheet.pdf.get_name().to_string();
|
|
||||||
}
|
|
||||||
crate::sheet::SheetKind::Book {
|
|
||||||
name,
|
|
||||||
composer_id,
|
|
||||||
sheet_ids: _,
|
|
||||||
} => {
|
|
||||||
is_book = true;
|
|
||||||
sheet_name = name.to_string();
|
|
||||||
if let Ok(composer) =
|
|
||||||
sheet_dao::get_composer_by_id(¶ms.database, *composer_id).await
|
|
||||||
{
|
|
||||||
sheet_composer = composer.name;
|
|
||||||
}
|
|
||||||
// TODO: load sheets of book
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let composer_entry_completion = EntryCompletion::new();
|
|
||||||
let data = [
|
|
||||||
"France".to_string(),
|
|
||||||
"Italy".to_string(),
|
|
||||||
"Sweden".to_string(),
|
|
||||||
"Switzerland".to_string(),
|
|
||||||
];
|
|
||||||
let store = gtk::ListStore::new(&[glib::Type::STRING]);
|
|
||||||
for d in data.iter() {
|
|
||||||
store.set(&store.append(), &[(0, &d)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
composer_entry_completion.set_model(Some(&store));
|
|
||||||
// Use the first (and only) column available to set the autocompletion text
|
|
||||||
composer_entry_completion.set_text_column(0);
|
|
||||||
// how many keystrokes to wait before attempting to autocomplete?
|
|
||||||
composer_entry_completion.set_minimum_key_length(1);
|
|
||||||
// whether the completions should be presented in a popup window
|
|
||||||
composer_entry_completion.set_popup_completion(true);
|
|
||||||
|
|
||||||
let model = SheetEditDialogModel {
|
|
||||||
database: params.database,
|
|
||||||
hidden: false,
|
|
||||||
sheet: Some(sheet),
|
|
||||||
name_entry_buffer: EntryBuffer::new(Some(sheet_name)),
|
|
||||||
composer_entry_buffer: EntryBuffer::new(Some(sheet_composer)),
|
|
||||||
composer_entry_completion,
|
|
||||||
is_book,
|
|
||||||
book_sheets: Vec::new(),
|
|
||||||
alert_empty_fields: Alert::builder().transient_for(&root).launch(AlertSettings {
|
|
||||||
text: String::from("Missing input"),
|
|
||||||
secondary_text: Some(String::from("Please make sure all fields are filled")),
|
|
||||||
is_modal: true,
|
|
||||||
destructive_accept: false,
|
|
||||||
confirm_label: Some(String::from("Ok")),
|
|
||||||
cancel_label: None,
|
|
||||||
option_label: None,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
let widgets = view_output!();
|
|
||||||
|
|
||||||
AsyncComponentParts { model, widgets }
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: init_loading_widgets
|
|
||||||
|
|
||||||
async fn update(
|
|
||||||
&mut self,
|
|
||||||
msg: Self::Input,
|
|
||||||
_sender: AsyncComponentSender<Self>,
|
|
||||||
_root: &Self::Root,
|
|
||||||
) {
|
|
||||||
match msg {
|
|
||||||
SheetEditDialogInput::Accept => {
|
|
||||||
if let Some(_sheet) = &self.sheet {
|
|
||||||
let sheet_name_string = self.name_entry_buffer.text();
|
|
||||||
let sheet_name = sheet_name_string.trim();
|
|
||||||
|
|
||||||
let sheet_composer_string = self.composer_entry_buffer.text();
|
|
||||||
let sheet_composer = sheet_composer_string.trim();
|
|
||||||
|
|
||||||
if sheet_name.is_empty() || sheet_composer.is_empty() {
|
|
||||||
self.alert_empty_fields.emit(AlertMsg::Show);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// match sheet.kind.borrow_mut() {
|
|
||||||
// crate::sheet::SheetKind::Sheet {
|
|
||||||
// name,
|
|
||||||
// composer_id,
|
|
||||||
// first_page,
|
|
||||||
// book_id,
|
|
||||||
// } => {
|
|
||||||
// todo!("Do something!!!");
|
|
||||||
// // name = "hello world";
|
|
||||||
// // name = &mut self.sheet_name.clone();
|
|
||||||
// // composer_id = 0;
|
|
||||||
// }
|
|
||||||
// crate::sheet::SheetKind::Orphan => {
|
|
||||||
// todo!("Create Sheet");
|
|
||||||
// }
|
|
||||||
// crate::sheet::SheetKind::Book {
|
|
||||||
// name,
|
|
||||||
// composer_id,
|
|
||||||
// sheet_ids,
|
|
||||||
// } => todo!(),
|
|
||||||
// };
|
|
||||||
// sender
|
|
||||||
// .output(SheetEditDialogOutput::SheetEdited(sheet))
|
|
||||||
// .unwrap();
|
|
||||||
}
|
|
||||||
self.hidden = true;
|
|
||||||
}
|
|
||||||
SheetEditDialogInput::Cancel => {
|
|
||||||
self.hidden = true;
|
|
||||||
self.sheet = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,8 +8,6 @@ use crate::sheet::Sheet;
|
|||||||
|
|
||||||
use super::sheet_model::{OnQueryUpdate, SheetModel};
|
use super::sheet_model::{OnQueryUpdate, SheetModel};
|
||||||
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
|
|
||||||
pub struct SheetListingModel {
|
pub struct SheetListingModel {
|
||||||
sheets: FactoryVecDeque<SheetModel>,
|
sheets: FactoryVecDeque<SheetModel>,
|
||||||
}
|
}
|
||||||
@ -18,23 +16,18 @@ pub struct SheetListingModel {
|
|||||||
pub enum SheetListingInput {
|
pub enum SheetListingInput {
|
||||||
Query(String),
|
Query(String),
|
||||||
ListBoxRowClicked(i32),
|
ListBoxRowClicked(i32),
|
||||||
Sort,
|
|
||||||
Shuffle,
|
|
||||||
ReloadSheets(Vec<Sheet>),
|
|
||||||
None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SheetListingOutput {
|
pub struct SheetModelSelected {
|
||||||
SheetModelSelected(Sheet),
|
pub sheet: Sheet,
|
||||||
ContentsChanged,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::component(pub)]
|
#[relm4::component(pub)]
|
||||||
impl SimpleComponent for SheetListingModel {
|
impl SimpleComponent for SheetListingModel {
|
||||||
type Init = Vec<Sheet>;
|
type Init = Vec<Sheet>;
|
||||||
type Input = SheetListingInput;
|
type Input = SheetListingInput;
|
||||||
type Output = SheetListingOutput;
|
type Output = SheetModelSelected;
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
#[root]
|
#[root]
|
||||||
@ -55,12 +48,10 @@ impl SimpleComponent for SheetListingModel {
|
|||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
init: Self::Init,
|
init: Self::Init,
|
||||||
root: Self::Root,
|
root: &Self::Root,
|
||||||
sender: ComponentSender<Self>,
|
sender: ComponentSender<Self>,
|
||||||
) -> ComponentParts<Self> {
|
) -> ComponentParts<Self> {
|
||||||
let mut sheets = FactoryVecDeque::builder()
|
let mut sheets = FactoryVecDeque::new(gtk::ListBox::default(), sender.input_sender());
|
||||||
.launch(gtk::ListBox::default())
|
|
||||||
.forward(sender.input_sender(), |_| SheetListingInput::None);
|
|
||||||
for sheet_model_type in init {
|
for sheet_model_type in init {
|
||||||
sheets.guard().push_back(sheet_model_type);
|
sheets.guard().push_back(sheet_model_type);
|
||||||
}
|
}
|
||||||
@ -78,107 +69,11 @@ impl SimpleComponent for SheetListingModel {
|
|||||||
SheetListingInput::ListBoxRowClicked(index) => {
|
SheetListingInput::ListBoxRowClicked(index) => {
|
||||||
let sheet_model = self.sheets.get(index as usize).unwrap();
|
let sheet_model = self.sheets.get(index as usize).unwrap();
|
||||||
sender
|
sender
|
||||||
.output(SheetListingOutput::SheetModelSelected(
|
.output(SheetModelSelected {
|
||||||
sheet_model.sheet.clone(),
|
sheet: sheet_model.sheet.clone(),
|
||||||
))
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
SheetListingInput::Sort => {
|
|
||||||
sort_sheets(&mut self.sheets);
|
|
||||||
sender.output(SheetListingOutput::ContentsChanged).unwrap();
|
|
||||||
}
|
|
||||||
SheetListingInput::Shuffle => {
|
|
||||||
shuffle_sheets(&mut self.sheets);
|
|
||||||
sender.output(SheetListingOutput::ContentsChanged).unwrap();
|
|
||||||
}
|
|
||||||
SheetListingInput::ReloadSheets(sheets) => {
|
|
||||||
self.sheets.guard().clear();
|
|
||||||
for sheet_model_type in sheets {
|
|
||||||
self.sheets.guard().push_back(sheet_model_type);
|
|
||||||
}
|
|
||||||
sender.output(SheetListingOutput::ContentsChanged).unwrap();
|
|
||||||
}
|
|
||||||
SheetListingInput::None => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shuffle_sheets(sheets: &mut FactoryVecDeque<SheetModel>) {
|
|
||||||
let mut new_order: Vec<usize> = (0..sheets.len()).collect();
|
|
||||||
new_order.shuffle(&mut rand::thread_rng());
|
|
||||||
order_sheets(sheets, &mut new_order);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sort_sheets(sheets: &mut FactoryVecDeque<SheetModel>) {
|
|
||||||
let mut order = Vec::new();
|
|
||||||
{
|
|
||||||
let guard = sheets.guard();
|
|
||||||
let mut numerated_sheets: Vec<_> = guard.iter().enumerate().collect();
|
|
||||||
numerated_sheets.sort_by(|a, b| a.1.sheet.cmp(&b.1.sheet).reverse());
|
|
||||||
for (i, _) in numerated_sheets {
|
|
||||||
order.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
order_sheets(sheets, &mut order);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn order_sheets(sheets: &mut FactoryVecDeque<SheetModel>, order: &mut Vec<usize>) {
|
|
||||||
assert!(sheets.len() == order.len());
|
|
||||||
|
|
||||||
let mut wish_positions = vec![0; sheets.len()];
|
|
||||||
for (i, i2) in order.iter().enumerate() {
|
|
||||||
wish_positions[*i2] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..sheets.len() {
|
|
||||||
let new_i = order[i];
|
|
||||||
let old_i = i;
|
|
||||||
|
|
||||||
if old_i != new_i {
|
|
||||||
order.swap(old_i, wish_positions[old_i]);
|
|
||||||
wish_positions.swap(old_i, new_i);
|
|
||||||
sheets.guard().swap(old_i, new_i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
// Note this useful idiom: importing names from outer (for mod tests) scope.
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sort() {
|
|
||||||
let original: Vec<usize> = (0..100).collect();
|
|
||||||
let mut to_sort = original.clone();
|
|
||||||
to_sort.shuffle(&mut rand::thread_rng());
|
|
||||||
|
|
||||||
println!("To sort: {:?}", to_sort);
|
|
||||||
|
|
||||||
let mut order_builder: Vec<_> = to_sort.clone().into_iter().enumerate().collect();
|
|
||||||
order_builder.sort_by(|a, b| a.1.cmp(&b.1));
|
|
||||||
let mut order: Vec<_> = order_builder.into_iter().map(|(i, _)| i).collect();
|
|
||||||
|
|
||||||
println!("Initial order: {:?}", order);
|
|
||||||
|
|
||||||
let mut wish_positions = vec![0; to_sort.len()];
|
|
||||||
for (i, i2) in order.iter().enumerate() {
|
|
||||||
wish_positions[*i2] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..to_sort.len() {
|
|
||||||
let new_i = order[i];
|
|
||||||
let old_i = i;
|
|
||||||
|
|
||||||
println!("Swap {} and {}", old_i, new_i);
|
|
||||||
if old_i != new_i {
|
|
||||||
order.swap(old_i, wish_positions[old_i]);
|
|
||||||
wish_positions.swap(old_i, new_i);
|
|
||||||
to_sort.swap(old_i, new_i);
|
|
||||||
}
|
|
||||||
println!("order: {:?} - to_sort: {:?}", order, to_sort);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(original, to_sort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
|
use std::{cmp::Ordering, path::Path};
|
||||||
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
|
||||||
use crate::sheet::Sheet;
|
use crate::sheet::Sheet;
|
||||||
|
|
||||||
|
use super::sheet_listing::SheetListingInput;
|
||||||
|
|
||||||
pub struct SheetModel {
|
pub struct SheetModel {
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub sheet: Sheet,
|
pub sheet: Sheet,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RowActivated;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OnQueryUpdate {
|
pub struct OnQueryUpdate {
|
||||||
pub query: String,
|
pub query: String,
|
||||||
@ -19,8 +26,9 @@ impl FactoryComponent for SheetModel {
|
|||||||
type Init = Sheet;
|
type Init = Sheet;
|
||||||
type ParentWidget = gtk::ListBox;
|
type ParentWidget = gtk::ListBox;
|
||||||
type CommandOutput = ();
|
type CommandOutput = ();
|
||||||
|
type ParentInput = SheetListingInput;
|
||||||
type Input = OnQueryUpdate;
|
type Input = OnQueryUpdate;
|
||||||
type Output = ();
|
type Output = RowActivated;
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
#[root]
|
#[root]
|
||||||
@ -38,16 +46,17 @@ impl FactoryComponent for SheetModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_model(sheet: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
|
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
|
||||||
let label = match &sheet.kind {
|
let label = match &value.kind {
|
||||||
crate::sheet::SheetKind::Sheet { name, .. } => name,
|
crate::sheet::SheetKind::Sheet { name, .. } => name,
|
||||||
crate::sheet::SheetKind::Orphan {} => sheet.pdf.get_name(),
|
crate::sheet::SheetKind::Orphan { pdf } => pdf.get_name(),
|
||||||
crate::sheet::SheetKind::Book { name, .. } => name,
|
crate::sheet::SheetKind::Book { name, .. } => name,
|
||||||
|
crate::sheet::SheetKind::BookSheet { name, .. } => name,
|
||||||
};
|
};
|
||||||
|
|
||||||
SheetModel {
|
SheetModel {
|
||||||
label: label.to_string(),
|
label: label.to_string(),
|
||||||
sheet,
|
sheet: value,
|
||||||
visible: true,
|
visible: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user