Compare commits
11 Commits
182675d14e
...
master
Author | SHA1 | Date | |
---|---|---|---|
94de09f429 | |||
bdc2e7a050 | |||
67d5dac0d1 | |||
d20bcf6a2d | |||
808698dd1c | |||
59864de6bd | |||
dd36dab497 | |||
26133a692f | |||
9a212a85ea | |||
e201539219 | |||
d7379a2a9f |
1079
Cargo.lock
generated
1079
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||||
|
10
Readme.md
10
Readme.md
@@ -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
23
flake.lock
generated
@@ -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": {
|
||||||
|
35
flake.nix
35
flake.nix
@@ -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};
|
||||||
@@ -54,7 +50,9 @@
|
|||||||
|
|
||||||
buildInputs =
|
buildInputs =
|
||||||
with pkgs;
|
with pkgs;
|
||||||
[ gtk4 ]
|
[
|
||||||
|
gtk4
|
||||||
|
]
|
||||||
++ 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
|
||||||
pkgs.libiconv
|
pkgs.libiconv
|
||||||
@@ -83,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;
|
||||||
@@ -101,4 +114,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
in
|
||||||
|
packageOutputs;
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ base_resource_path = "/org/gtkrs/"
|
|||||||
|
|
||||||
# List of icon names you found (shipped with this crate)
|
# List of icon names you found (shipped with this crate)
|
||||||
# Note: the file ending `-symbolic.svg` isn't part of the icon name.
|
# Note: the file ending `-symbolic.svg` isn't part of the icon name.
|
||||||
icons = ["refresh", "edit", "arrow-sort-regular", "playlist-shuffle", "user-trash"]
|
icons = ["refresh", "edit", "arrow-sort-regular", "playlist-shuffle", "user-trash", "open-filled", "document-settings-filled"]
|
||||||
|
|
||||||
# Optional: Specify a folder containing your own SVG icons
|
# Optional: Specify a folder containing your own SVG icons
|
||||||
# icon_folder = "my_svg_icons"
|
# icon_folder = "my_svg_icons"
|
||||||
|
6
sheet-organizer.desktop
Normal file
6
sheet-organizer.desktop
Normal 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
BIN
sheet-organizer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 293 KiB |
39
src/config.rs
Normal file
39
src/config.rs
Normal 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)
|
||||||
|
}
|
37
src/main.rs
37
src/main.rs
@@ -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
|
||||||
|
25
src/sheet.rs
25
src/sheet.rs
@@ -1,10 +1,12 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
|
ffi::OsStr,
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use log::debug;
|
||||||
use strum_macros::{EnumDiscriminants, EnumIter};
|
use strum_macros::{EnumDiscriminants, EnumIter};
|
||||||
|
|
||||||
pub trait PdfSheet {
|
pub trait PdfSheet {
|
||||||
@@ -49,10 +51,27 @@ impl Ord for Sheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Sheet {
|
impl Sheet {
|
||||||
pub fn open_file(&self) {
|
pub fn construct_xopp_file_path(&self) -> PathBuf {
|
||||||
let path = &self.pdf.path;
|
let mut xopp_path = self.pdf.path.with_extension("").into_os_string();
|
||||||
|
xopp_path.push(".xopp");
|
||||||
|
PathBuf::from(xopp_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn construct_annotated_file_path(&self) -> PathBuf {
|
||||||
|
let mut annotated_path = self.pdf.path.with_extension("").into_os_string();
|
||||||
|
annotated_path.push("_annotated.pdf");
|
||||||
|
PathBuf::from(annotated_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_file_or_annotated_version_if_exists(&self) {
|
||||||
|
let annotated_version = self.construct_annotated_file_path();
|
||||||
|
if annotated_version.exists() {
|
||||||
// TODO: open on first_page
|
// TODO: open on first_page
|
||||||
opener::open(path).unwrap();
|
opener::open(annotated_version).unwrap();
|
||||||
|
} else {
|
||||||
|
// TODO: open on first_page
|
||||||
|
opener::open(&self.pdf.path).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_part_of_book(&self) -> bool {
|
pub fn is_part_of_book(&self) -> bool {
|
||||||
|
@@ -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();
|
||||||
|
|
||||||
|
@@ -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...");
|
||||||
@@ -64,6 +66,16 @@ fn validate_sheet_files(sheets: Vec<Sheet>, dir: impl AsRef<Path>) -> FileValida
|
|||||||
|
|
||||||
// TODO: improve performance?
|
// TODO: improve performance?
|
||||||
for pdf_file in find_all_pdfs_in_directory_recursive(dir) {
|
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
|
if let Some((i, _)) = invalidated_sheets
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, process::Command, sync::Arc};
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
@@ -11,7 +11,7 @@ use relm4::{
|
|||||||
use relm4_icons::icon_names;
|
use relm4_icons::icon_names;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::Database,
|
database::{self, Database},
|
||||||
sheet::{I64DateTime, Sheet},
|
sheet::{I64DateTime, Sheet},
|
||||||
sheet_dao, sheet_validation,
|
sheet_dao, sheet_validation,
|
||||||
ui::mcdu::McduOutput,
|
ui::mcdu::McduOutput,
|
||||||
@@ -28,11 +28,18 @@ pub struct AppModel {
|
|||||||
directory: Arc<PathBuf>,
|
directory: Arc<PathBuf>,
|
||||||
mcdu: Controller<McduModel>,
|
mcdu: Controller<McduModel>,
|
||||||
sheets_listing: Controller<SheetListingModel>,
|
sheets_listing: Controller<SheetListingModel>,
|
||||||
edit_mode: bool,
|
click_mode: ClickMode,
|
||||||
scroll_adjustment: Adjustment,
|
scroll_adjustment: Adjustment,
|
||||||
sheet_edit_dialog: Option<AsyncController<SheetEditDialogModel>>,
|
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),
|
||||||
@@ -40,7 +47,7 @@ pub enum AppInput {
|
|||||||
Refresh,
|
Refresh,
|
||||||
Sort,
|
Sort,
|
||||||
Shuffle,
|
Shuffle,
|
||||||
SetEditMode(bool),
|
SetClickMode(ClickMode),
|
||||||
SheetListingContentsChanged,
|
SheetListingContentsChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +85,6 @@ impl AsyncComponent for AppModel {
|
|||||||
set_margin_end: 10,
|
set_margin_end: 10,
|
||||||
connect_clicked[sender] => move |_| sender.input(AppInput::Refresh),
|
connect_clicked[sender] => move |_| sender.input(AppInput::Refresh),
|
||||||
},
|
},
|
||||||
gtk::ToggleButton {
|
|
||||||
set_icon_name: icon_names::EDIT,
|
|
||||||
set_margin_end: 10,
|
|
||||||
connect_clicked[sender] => move |button| sender.input(AppInput::SetEditMode(button.is_active())),
|
|
||||||
},
|
|
||||||
#[name = "button_sort"]
|
#[name = "button_sort"]
|
||||||
gtk::ToggleButton {
|
gtk::ToggleButton {
|
||||||
set_icon_name: icon_names::ARROW_SORT_REGULAR,
|
set_icon_name: icon_names::ARROW_SORT_REGULAR,
|
||||||
@@ -92,8 +94,25 @@ impl AsyncComponent for AppModel {
|
|||||||
gtk::ToggleButton {
|
gtk::ToggleButton {
|
||||||
set_icon_name: icon_names::PLAYLIST_SHUFFLE,
|
set_icon_name: icon_names::PLAYLIST_SHUFFLE,
|
||||||
set_group: Some(&button_sort),
|
set_group: Some(&button_sort),
|
||||||
|
set_margin_end: 10,
|
||||||
connect_clicked[sender] => move |_| sender.input(AppInput::Shuffle),
|
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(),
|
||||||
@@ -115,10 +134,12 @@ 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 display = gdk::Display::default().unwrap();
|
||||||
let theme = gtk::IconTheme::for_display(&display);
|
let theme = gtk::IconTheme::for_display(&display);
|
||||||
|
|
||||||
theme.add_resource_path("/org/gtkrs/icons/");
|
theme.add_resource_path("/org/gtkrs/icons/");
|
||||||
theme.add_resource_path("/org/gtkrs/icons/scalable/actions/");
|
// theme.add_resource_path("/org/gtkrs/icons/scalable/actions/");
|
||||||
|
|
||||||
let mcdu = McduModel::builder()
|
let mcdu = McduModel::builder()
|
||||||
.launch(())
|
.launch(())
|
||||||
@@ -142,7 +163,7 @@ impl AsyncComponent for AppModel {
|
|||||||
directory: Arc::new(init_data.directory),
|
directory: Arc::new(init_data.directory),
|
||||||
mcdu,
|
mcdu,
|
||||||
sheets_listing,
|
sheets_listing,
|
||||||
edit_mode: false,
|
click_mode: ClickMode::Open,
|
||||||
scroll_adjustment: Adjustment::builder().build(),
|
scroll_adjustment: Adjustment::builder().build(),
|
||||||
sheet_edit_dialog: None,
|
sheet_edit_dialog: None,
|
||||||
};
|
};
|
||||||
@@ -164,7 +185,9 @@ impl AsyncComponent for AppModel {
|
|||||||
.emit(SheetListingInput::Query(query.clone()));
|
.emit(SheetListingInput::Query(query.clone()));
|
||||||
}
|
}
|
||||||
AppInput::SheetPressed(sheet) => {
|
AppInput::SheetPressed(sheet) => {
|
||||||
if self.edit_mode {
|
match self.click_mode {
|
||||||
|
ClickMode::Open => open_sheet(&sheet, &self.database).await,
|
||||||
|
ClickMode::Edit => {
|
||||||
self.sheet_edit_dialog = Some(
|
self.sheet_edit_dialog = Some(
|
||||||
SheetEditDialogModel::builder()
|
SheetEditDialogModel::builder()
|
||||||
.transient_for(root)
|
.transient_for(root)
|
||||||
@@ -174,14 +197,9 @@ impl AsyncComponent for AppModel {
|
|||||||
})
|
})
|
||||||
.forward(sender.input_sender(), |_| todo!()),
|
.forward(sender.input_sender(), |_| todo!()),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
sheet.open_file();
|
|
||||||
let mut sheet = sheet;
|
|
||||||
sheet.last_opened = I64DateTime(Utc::now());
|
|
||||||
sheet_dao::update_sheet_last_opened(&self.database, &sheet)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
ClickMode::Annotate => annotate_sheet(&sheet).await,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
AppInput::Refresh => {
|
AppInput::Refresh => {
|
||||||
let db = Arc::clone(&self.database);
|
let db = Arc::clone(&self.database);
|
||||||
@@ -192,7 +210,7 @@ impl AsyncComponent for AppModel {
|
|||||||
}
|
}
|
||||||
AppInput::Sort => self.sheets_listing.emit(SheetListingInput::Sort),
|
AppInput::Sort => self.sheets_listing.emit(SheetListingInput::Sort),
|
||||||
AppInput::Shuffle => self.sheets_listing.emit(SheetListingInput::Shuffle),
|
AppInput::Shuffle => self.sheets_listing.emit(SheetListingInput::Shuffle),
|
||||||
AppInput::SetEditMode(edit_mode) => self.edit_mode = edit_mode,
|
AppInput::SetClickMode(click_mode) => self.click_mode = click_mode,
|
||||||
AppInput::SheetListingContentsChanged => self.scroll_adjustment.set_value(0.0),
|
AppInput::SheetListingContentsChanged => self.scroll_adjustment.set_value(0.0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,3 +228,19 @@ impl AsyncComponent for AppModel {
|
|||||||
.emit(SheetListingInput::ReloadSheets(sheets));
|
.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_dao::update_sheet_last_opened(database, &sheet)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn annotate_sheet(sheet: &Sheet) {
|
||||||
|
Command::new("xournalpp")
|
||||||
|
.arg(&sheet.pdf.path)
|
||||||
|
.spawn()
|
||||||
|
.expect("failed to execute process");
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user