Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57ded93e2a | |||
| 3934181769 | |||
| 34f240e539 | |||
| 6e05f96131 | |||
| 58d972c3f4 |
46 changed files with 2354 additions and 3673 deletions
|
|
@ -1 +0,0 @@
|
|||
experimental = [ "benchmarks" ]
|
||||
2
.envrc
2
.envrc
|
|
@ -1,4 +1,4 @@
|
|||
DATABASE_URL="sqlite://./test.db"
|
||||
DATABASE_URL="sqlite:///home/chris/.local/share/lumina/library-db.sqlite3"
|
||||
use flake .
|
||||
# eval $(guix shell -D --search-paths)
|
||||
|
||||
|
|
|
|||
18
.forgejo/workflows/demo.yaml
Normal file
18
.forgejo/workflows/demo.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
on: [push]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- run: |
|
||||
apt update
|
||||
apt install sudo
|
||||
apt install just
|
||||
|
||||
- uses: https://github.com/cachix/install-nix-action@v27
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
- run: nix develop --command just test
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
on: [push]
|
||||
jobs:
|
||||
clippy:
|
||||
runs-on: nixos-latest
|
||||
steps:
|
||||
- run: nix-env --install nodejs
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- run: nix --extra-experimental-features nix-command --extra-experimental-features flakes develop --command cargo clippy -- -D clippy::pedantic -D clippy::perf -D clippy::nursery -D clippy::unwrap_used
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
on: [push]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: nixos-latest
|
||||
steps:
|
||||
- run: nix-env --install nodejs
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- run: nix --extra-experimental-features nix-command --extra-experimental-features flakes develop --command just ci-test
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -8,9 +8,3 @@ data.db
|
|||
/perf.data
|
||||
/perf.data.old
|
||||
.aider*
|
||||
|
||||
test.db-shm
|
||||
test.db-wal
|
||||
test.lum
|
||||
test.pres
|
||||
profile.json.gz
|
||||
13
Cargo.lock
generated
13
Cargo.lock
generated
|
|
@ -4488,7 +4488,6 @@ dependencies = [
|
|||
"ron 0.8.1",
|
||||
"scraper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
|
|
@ -6989,16 +6988,16 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"indexmap 2.12.1",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -10205,12 +10204,6 @@ dependencies = [
|
|||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ derive_more = { version = "2.1.1", features = ["debug"] }
|
|||
reqwest = "0.13.1"
|
||||
scraper = "0.25.0"
|
||||
itertools = "0.14.0"
|
||||
serde_json = "1.0.149"
|
||||
|
||||
# rfd = { version = "0.15.4", default-features = false, features = ["xdg-portal"] }
|
||||
|
||||
|
|
|
|||
62
TODO.org
62
TODO.org
|
|
@ -1,34 +1,33 @@
|
|||
#+TITLE: The Task list for Lumina
|
||||
|
||||
|
||||
* TODO [#A] Need to fix tests now that the basic app is working
|
||||
Lots of them have been tweaked to be completing now, but there is more work to do and several need to likely be a lot more robust.
|
||||
|
||||
Still failing 4 tests, all to do with the db or lisp. I might throw out the lisp code at some point tho. I keep thinking that a better alternative would be to have a markdown serialization system such that you can write slides in markdown somehow and they would be able to be loaded.
|
||||
|
||||
* TODO [#A] Need to fixup how songs are edited in the editors
|
||||
Currently the song is cloned many times to pass around and then finally get updated in DB. Instead, we need to edit the song directly in the editor and after it's been changed appropriatel, run the update_song method to get the current song and create slides from it and then update it in the DB.
|
||||
|
||||
* TODO [#A] Make sure updating verse updates the lyrics too
|
||||
[[file:~/dev/lumina-iced/src/core/songs.rs::old_verse = verse;]]
|
||||
|
||||
This is necessary so that the entire song gets changed and we can propogate those changes then back to the db.
|
||||
|
||||
There is likely some work that still needs to be done here, I believe I am somehow deleting some of my verses.
|
||||
* TODO [#A] Add Action system
|
||||
This will be based on each slide having the ability to activate an action (i.e. OBS scene switch, OBS start or stop) when it is active.
|
||||
|
||||
This is working but the right click context menu is all the way on the edge of the ui so you can't control all the slides. It also needs a lot of help in making the system more robust and potentially lest reliant on the Presenter struct itself.
|
||||
|
||||
* TODO [#B] Font in the song editor doesn't always use the original version
|
||||
There seems to be some issue with fontdb not able to decipher all the versions of some fonts that are OTF and then end up loading the wrong ones in some issues.
|
||||
|
||||
* TODO [#B] Find a way to use auth-token in tests for ci
|
||||
If I can find out how to use my secrets in ci that would free up more tests, but I could also just turn that test off for the CI so that it won't constantly fail for now
|
||||
* TODO [#C] Rename menu actions to menu commands and build a reverse hashmap for settings to map commands to key-binding such that we can allow for remapping them on the fly.
|
||||
|
||||
* TODO [#B] Saving and loading font awareness
|
||||
Someday we should make the saving and loading to be aware of the fonts on the system and find a way to embed them into the save file.
|
||||
|
||||
* TODO [#B] Develop ui for settings
|
||||
|
||||
* TODO [#B] Develop library system for slides that are more than images or video i.e. content
|
||||
|
||||
* TODO [#C] Use orgize as a file parser and allow for orgdown files to represent a presentation.
|
||||
Orgize has some very nice features that will let me determine what things are in an orgdown file and thus take said file and turn it into a presentation.
|
||||
|
||||
After looking more and more at how the orgize docs describe things and the testing platform found at: https://poiscript.github.io/orgize/ I believe this will work. The main things are that I can possibly decide how to interpret certain pieces of orgdown to mean certain things in lumina. Essentially a properties drawer or tag can indicate backgrounds and other info for the slides or songs and then the notes blocks can indicate text that shouldn't be printed into the slide, thus allowing a single orgdown document to illustrate both an entire presentation, but also the notes and plan for the presenter.
|
||||
|
||||
I could potentially do the same with markdown, but since this is for me first, I'll use orgdown because I enjoy the syntax a lot more.
|
||||
* TODO [#B] Functions for text alignments
|
||||
This will need to be matched on for the =TextAlignment= from the user
|
||||
|
||||
* TODO [#C] Allow for a way to split the presentation up with a right click menu for the presentation preview row.
|
||||
|
||||
* TODO [#C] Text could be built by using SVG instead of the text element. Maybe I could construct my own text element even
|
||||
This does almost work. There is a clear amount of lag or rather hang up since switching to the =text_svg= element. I think I may only keep it till I can figure out how to do strokes and shadows in iced's normal text element.
|
||||
|
||||
|
|
@ -47,10 +46,8 @@ I tried out a way of generating the svg and rasterizing it ahead of time and the
|
|||
|
||||
The problem with this approach is that every change to a song's text or font metrics means we need to rebuild all the text items for that song. I need to think of a way for the text generation to be done asynchronously so that the ui isn't locked up.
|
||||
|
||||
I bet this is tricking up the loading mechanism. Loading only grabs all the backgrounds and audio pieces, not the text_svg pieces. So maybe it should so that the generator can run again and grab the same pieces from the filesystem rather than recreate them. This gets extra tricky because we may have fonts that are missing when loading a file. In such a case the loading mechanism ought to suggest to the user to grab those fonts and then perhaps load the cached file while being extra clear that any changes will mess up the text since they no longer possess the font that is in the loaded file. Maybe what we can do is during save, save a copy of all the fonts as well and then during load check to see if the computer has them, if they don't offer to install them on the spot such that they can use the font as is. I wonder if we are allowed to pass fonts around that way.
|
||||
|
||||
** Made this slightly faster
|
||||
Since strings are allocated on the heap, I've changed how to construct the svg string a bit, but honestly, it doesn't matter too much because most of the performance cost seems to be in rendering the string using resvg. So, this can still be something that get's fixed later, and I believe that fix will come in the form of a multi-channel signed distance field wgpu rendered text eventually. We can work on this much later though.
|
||||
Since strings are allocated on the heap, I've changed how to construct the svg string a bit, but honestly, it doesn't matter too much because most of the performance cost seems to be in rendering the string using resvg. So, this can still be something that get's fixed later
|
||||
|
||||
* TODO [#C] Make the presenter more modular so things are easier to change. This is vague...
|
||||
|
||||
|
|
@ -63,14 +60,6 @@ This is limited by the fact that I need to develop this in cosmic. I am honestly
|
|||
|
||||
This needs lots more attention
|
||||
|
||||
* DONE [#A] File saving and loading
|
||||
Need to make sure we can save a file with all files archived in it and load it back up.
|
||||
|
||||
This is giving me a lot of thoughts...
|
||||
1. That saving and loading needs to know about fonts as well.
|
||||
2. That TextSvgs should likely be saved as well since the other machines may not always have the same fonts.
|
||||
3. That means that TextSvg should have a path option that could hold the cached svg that has already been rendered and that this gets changed to the loaded files directory rather than using the default cache directory.
|
||||
|
||||
* DONE [#A] Add removal and reordering of service_items
|
||||
Reordering is finished
|
||||
* DONE [#A] Change return type of all components to an Action enum instead of the Task<Message> type [0%] [0/0]
|
||||
|
|
@ -78,23 +67,6 @@ Reordering is finished
|
|||
** DONE SongEditor
|
||||
** DONE Presenter
|
||||
|
||||
* DONE [#A] Need to fix tests now that the basic app is working
|
||||
Lots of them have been tweaked to be completing now, but there is more work to do and several need to likely be a lot more robust.
|
||||
|
||||
Still failing 4 tests, all to do with the db or lisp. I might throw out the lisp code at some point tho. I keep thinking that a better alternative would be to have a markdown serialization system such that you can write slides in markdown somehow and they would be able to be loaded.
|
||||
|
||||
* DONE [#A] Make sure updating verse updates the lyrics too
|
||||
[[file:~/dev/lumina-iced/src/core/songs.rs::old_verse = verse;]]
|
||||
|
||||
This is necessary so that the entire song gets changed and we can propogate those changes then back to the db.
|
||||
|
||||
There is likely some work that still needs to be done here, I believe I am somehow deleting some of my verses.
|
||||
* DONE [#A] Need to fixup how songs are edited in the editors
|
||||
Currently the song is cloned many times to pass around and then finally get updated in DB. Instead, we need to edit the song directly in the editor and after it's been changed appropriatel, run the update_song method to get the current song and create slides from it and then update it in the DB.
|
||||
|
||||
* DONE [#B] Functions for text alignments
|
||||
This will need to be matched on for the =TextAlignment= from the user
|
||||
|
||||
* DONE Move text_generation function to be asynchronous so that UI doesn't lock up during song editing.
|
||||
* DONE Build a presentation editor
|
||||
|
||||
|
|
|
|||
83
flake.lock
generated
83
flake.lock
generated
|
|
@ -1,16 +1,31 @@
|
|||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1770419512,
|
||||
"narHash": "sha256-o8Vcdz6B6bkiGUYkZqFwH3Pv1JwZyXht3dMtS7RchIo=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "2510f2cbc3ccd237f700bb213756a8f35c32d8d7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770794449,
|
||||
"narHash": "sha256-1nFkhcZx9+Sdw5OXwJqp5TxvGncqRqLeK781v0XV3WI=",
|
||||
"lastModified": 1765435813,
|
||||
"narHash": "sha256-C6tT7K1Lx6VsYw1BY5S3OavtapUvEnDQtmQB5DSgbCc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "b19d93fdf9761e6101f8cb5765d638bacebd9a1b",
|
||||
"rev": "6399553b7a300c77e7f07342904eb696a5b6bf9d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -65,11 +80,11 @@
|
|||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1769799857,
|
||||
"narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
|
||||
"lastModified": 1763384566,
|
||||
"narHash": "sha256-r+wgI+WvNaSdxQmqaM58lVNvJYJ16zoq+tKN20cLst4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
|
||||
"rev": "d4155d6ebb70fbe2314959842f744aa7cabbbf6a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -80,11 +95,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770562336,
|
||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
||||
"lastModified": 1765186076,
|
||||
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
||||
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -112,11 +127,11 @@
|
|||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1770562336,
|
||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
||||
"lastModified": 1765472234,
|
||||
"narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
||||
"rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -126,39 +141,23 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1744536153,
|
||||
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_3",
|
||||
"rust-overlay": "rust-overlay"
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1770702974,
|
||||
"narHash": "sha256-CbvWu72rpGHK5QynoXwuOnVzxX7njF2LYgk8wRSiAQ0=",
|
||||
"lastModified": 1765400135,
|
||||
"narHash": "sha256-D3+4hfNwUhG0fdCpDhOASLwEQ1jKuHi4mV72up4kLQM=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "07a594815f7c1d6e7e39f21ddeeedb75b21795f4",
|
||||
"rev": "fface27171988b3d605ef45cf986c25533116f7e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -185,24 +184,6 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770779462,
|
||||
"narHash": "sha256-ykcXTKtV+dOaKlOidAj6dpewBHjni9/oy/6VKcqfzfY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "8a53b3ade61914cdb10387db991b90a3a6f3c441",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
|
|
|
|||
157
flake.nix
157
flake.nix
|
|
@ -6,7 +6,7 @@
|
|||
naersk.url = "github:nix-community/naersk";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
};
|
||||
|
||||
outputs =
|
||||
|
|
@ -15,43 +15,46 @@
|
|||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
# overlays = [ rust-overlay.overlays.default ];
|
||||
inherit system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
# overlays = [cargo2nix.overlays.default];
|
||||
};
|
||||
inherit (pkgs) lib;
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain fenix.packages.${system}.stable.toolchain;
|
||||
naersk' = pkgs.callPackage naersk { };
|
||||
|
||||
# toolchain = (with pkgs.fenix.default; [cargo clippy rust-std rust-src rustc rustfmt rust-analyzer-nightly]);
|
||||
|
||||
unfilteredRoot = ./.; # The original, unfiltered source
|
||||
src = lib.fileset.toSource {
|
||||
root = unfilteredRoot;
|
||||
fileset = lib.fileset.unions [
|
||||
# Default files from crane (Rust and cargo files)
|
||||
(craneLib.fileset.commonCargoSources unfilteredRoot)
|
||||
# Include all the .sql migrations as well
|
||||
./migrations
|
||||
];
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
nbi = with pkgs; [
|
||||
# Rust tools
|
||||
# toolchain
|
||||
# (pkgs.fenix.default.withComponents [
|
||||
# "cargo"
|
||||
# "clippy"
|
||||
# "rust-std"
|
||||
# # "rust-src"
|
||||
# "rustc"
|
||||
# "rustfmt"
|
||||
# ])
|
||||
(rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override {
|
||||
extensions = [ "rust-src" "rust-analyzer" "clippy" ];
|
||||
}))
|
||||
alejandra
|
||||
(pkgs.fenix.stable.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
"rustc"
|
||||
"rustfmt"
|
||||
])
|
||||
cargo-nextest
|
||||
cargo-criterion
|
||||
# rust-analyzer-nightly
|
||||
rust-analyzer
|
||||
vulkan-loader
|
||||
wayland
|
||||
wayland-protocols
|
||||
libxkbcommon
|
||||
pkg-config
|
||||
sccache
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
bi = with pkgs; [
|
||||
gcc
|
||||
stdenv
|
||||
gnumake
|
||||
|
|
@ -60,13 +63,12 @@
|
|||
cmake
|
||||
clang
|
||||
libclang
|
||||
libxkbcommon
|
||||
makeWrapper
|
||||
vulkan-headers
|
||||
vulkan-loader
|
||||
vulkan-tools
|
||||
libGL
|
||||
cargo-flamegraph
|
||||
bacon
|
||||
|
||||
fontconfig
|
||||
glib
|
||||
|
|
@ -83,57 +85,78 @@
|
|||
mupdf
|
||||
# yt-dlp
|
||||
|
||||
just
|
||||
sqlx-cli
|
||||
cargo-watch
|
||||
samply
|
||||
] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
|
||||
# Additional darwin specific inputs can be set here
|
||||
pkgs.libiconv
|
||||
];
|
||||
|
||||
LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${
|
||||
ldLibPaths = "$LD_LIBRARY_PATH:${
|
||||
with pkgs;
|
||||
pkgs.lib.makeLibraryPath [
|
||||
pkgs.alsa-lib
|
||||
pkgs.gst_all_1.gst-libav
|
||||
pkgs.gst_all_1.gstreamer
|
||||
pkgs.gst_all_1.gst-plugins-bad
|
||||
pkgs.gst_all_1.gst-plugins-good
|
||||
pkgs.gst_all_1.gst-plugins-ugly
|
||||
pkgs.gst_all_1.gst-plugins-base
|
||||
pkgs.gst_all_1.gst-plugins-rs
|
||||
pkgs.gst_all_1.gst-vaapi
|
||||
pkgs.glib
|
||||
pkgs.fontconfig
|
||||
pkgs.vulkan-loader
|
||||
pkgs.wayland
|
||||
pkgs.wayland-protocols
|
||||
pkgs.libxkbcommon
|
||||
pkgs.mupdf
|
||||
pkgs.libclang
|
||||
lib.makeLibraryPath [
|
||||
alsa-lib
|
||||
gst_all_1.gst-libav
|
||||
gst_all_1.gstreamer
|
||||
gst_all_1.gst-plugins-bad
|
||||
gst_all_1.gst-plugins-good
|
||||
gst_all_1.gst-plugins-ugly
|
||||
gst_all_1.gst-plugins-base
|
||||
gst_all_1.gst-plugins-rs
|
||||
gst_all_1.gst-vaapi
|
||||
glib
|
||||
fontconfig
|
||||
vulkan-loader
|
||||
wayland
|
||||
wayland-protocols
|
||||
libxkbcommon
|
||||
mupdf
|
||||
libclang
|
||||
]
|
||||
}";
|
||||
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
strictDeps = true;
|
||||
nativeBuildInputs = nbi;
|
||||
buildInputs = bi;
|
||||
LD_LIBRARY_PATH = ldLibPaths;
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
lumina = craneLib.buildPackage (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
|
||||
nativeBuildInputs = (commonArgs.nativeBuildInputs or [ ]) ++ [
|
||||
pkgs.sqlx-cli
|
||||
];
|
||||
|
||||
preBuild = ''
|
||||
export DATABASE_URL=sqlite:./db.sqlite3
|
||||
sqlx database create
|
||||
sqlx migrate run
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
||||
in
|
||||
rec {
|
||||
devShell =
|
||||
pkgs.mkShell.override
|
||||
{
|
||||
# stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv;
|
||||
}
|
||||
{
|
||||
inherit nativeBuildInputs buildInputs LD_LIBRARY_PATH;
|
||||
# LIBCLANG_PATH = "${pkgs.clang}";
|
||||
DATABASE_URL = "sqlite://./test.db";
|
||||
# RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
|
||||
};
|
||||
defaultPackage = naersk'.buildPackage {
|
||||
inherit nativeBuildInputs buildInputs LD_LIBRARY_PATH;
|
||||
src = ./.;
|
||||
checks = {
|
||||
inherit lumina;
|
||||
};
|
||||
packages = {
|
||||
default = naersk'.buildPackage {
|
||||
inherit nativeBuildInputs buildInputs LD_LIBRARY_PATH;
|
||||
src = ./.;
|
||||
};
|
||||
devShells.default = craneLib.devShell {
|
||||
checks = self.checks.${system};
|
||||
inputsFrom = [ lumina ];
|
||||
packages = with pkgs; [
|
||||
sqlx-cli
|
||||
cargo-flamegraph
|
||||
bacon
|
||||
just
|
||||
cargo-watch
|
||||
];
|
||||
};
|
||||
packages.default = lumina;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
9
justfile
9
justfile
|
|
@ -20,14 +20,9 @@ run-file:
|
|||
clean:
|
||||
cargo clean
|
||||
test:
|
||||
cargo nextest run
|
||||
ci-test:
|
||||
cargo nextest run -- --skip test_db_and_model --skip test_update --skip test_song_slide_speed --skip test_song_to_slide --skip test_song_from_db
|
||||
bench:
|
||||
export NEXTEST_EXPERIMENTAL_BENCHMARKS=1
|
||||
cargo nextest bench
|
||||
cargo test --benches --tests --all-features -- --nocapture
|
||||
profile:
|
||||
samply record cargo run --release -- {{verbose}} {{ui}}
|
||||
cargo flamegraph -- {{verbose}} {{ui}}
|
||||
|
||||
alias b := build
|
||||
alias r := run
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
-- Add migration script here
|
||||
ALTER TABLE songs
|
||||
ADD COLUMN stroke_size INTEGER;
|
||||
|
||||
ALTER TABLE songs
|
||||
ADD COLUMN stroke_color TEXT;
|
||||
|
||||
ALTER TABLE songs
|
||||
ADD COLUMN shadow_size INTEGER;
|
||||
|
||||
ALTER TABLE songs
|
||||
ADD COLUMN shadow_offset_x INTEGER;
|
||||
|
||||
ALTER TABLE songs
|
||||
ADD COLUMN shadow_offset_y INTEGER;
|
||||
|
||||
ALTER TABLE songs
|
||||
ADD COLUMN shadow_color TEXT;
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
-- Add migration script here
|
||||
ALTER TABLE songs
|
||||
ADD COLUMN weight TEXT;
|
||||
|
||||
ALTER TABLE songs
|
||||
ADD COLUMN style TEXT;
|
||||
Binary file not shown.
BIN
res/nerdfont.ttf
BIN
res/nerdfont.ttf
Binary file not shown.
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0.877075 7.49988C0.877075 3.84219 3.84222 0.877045 7.49991 0.877045C11.1576 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1576 14.1227 7.49991 14.1227C3.84222 14.1227 0.877075 11.1575 0.877075 7.49988ZM7.49991 1.82704C4.36689 1.82704 1.82708 4.36686 1.82708 7.49988C1.82708 10.6329 4.36689 13.1727 7.49991 13.1727C10.6329 13.1727 13.1727 10.6329 13.1727 7.49988C13.1727 4.36686 10.6329 1.82704 7.49991 1.82704Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".05"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.78296 13.376C8.73904 9.95284 8.73904 5.04719 6.78296 1.62405L7.21708 1.37598C9.261 4.95283 9.261 10.0472 7.21708 13.624L6.78296 13.376Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".1"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.28204 13.4775C9.23929 9.99523 9.23929 5.00475 7.28204 1.52248L7.71791 1.2775C9.76067 4.9119 9.76067 10.0881 7.71791 13.7225L7.28204 13.4775Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".15"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.82098 13.5064C9.72502 9.99523 9.72636 5.01411 7.82492 1.50084L8.26465 1.26285C10.2465 4.92466 10.2451 10.085 8.26052 13.7448L7.82098 13.5064Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.41284 13.429C10.1952 9.92842 10.1957 5.07537 8.41435 1.57402L8.85999 1.34729C10.7139 4.99113 10.7133 10.0128 8.85841 13.6559L8.41284 13.429Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".25"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.02441 13.2956C10.6567 9.8379 10.6586 5.17715 9.03005 1.71656L9.48245 1.50366C11.1745 5.09919 11.1726 9.91629 9.47657 13.5091L9.02441 13.2956Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".3"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.66809 13.0655C11.1097 9.69572 11.1107 5.3121 9.67088 1.94095L10.1307 1.74457C11.6241 5.24121 11.6231 9.76683 10.1278 13.2622L9.66809 13.0655Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".35"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.331 12.7456C11.5551 9.52073 11.5564 5.49103 10.3347 2.26444L10.8024 2.0874C12.0672 5.42815 12.0659 9.58394 10.7985 12.9231L10.331 12.7456Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".4"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.0155 12.2986C11.9938 9.29744 11.9948 5.71296 11.0184 2.71067L11.4939 2.55603C12.503 5.6589 12.502 9.35178 11.4909 12.4535L11.0155 12.2986Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".45"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.7214 11.668C12.4254 9.01303 12.4262 5.99691 11.7237 3.34116L12.2071 3.21329C12.9318 5.95292 12.931 9.05728 12.2047 11.7961L11.7214 11.668Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
opacity=".5"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.4432 10.752C12.8524 8.63762 12.8523 6.36089 12.4429 4.2466L12.9338 4.15155C13.3553 6.32861 13.3554 8.66985 12.9341 10.847L12.4432 10.752Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" id="Layer_1" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<style type="text/css">
|
||||
.st0{fill:#6040EC;}
|
||||
.st1{fill:#0BDC49;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st1" d="M425.6,63.6H95.9c-2.1-1.4-4.5-2.1-7-2.1c-6.9,0-12.4,5.6-12.4,12.4c0,2.7,0.9,5.3,2.4,7.4v66.1 c0,6.6,5.4,12,12,12c6.6,0,12-5.4,12-12V87.6h151.9V458c0,6.6,5.4,12,12,12s12-5.4,12-12V87.6h146.8c2.8,0,5.1,2.6,5.1,5.9v53.6 c0,6.6,5.4,12,12,12s12-5.4,12-12V93.4C454.7,77,441.6,63.6,425.6,63.6z"/>
|
||||
<path class="st0" d="M404,42H86.4c-16,0-29.1,13.4-29.1,29.9v53.9c0,6.6,5.4,12,12,12s12-5.4,12-12V71.9c0-3.2,2.3-5.9,5.1-5.9 h146.8v385c0,6.6,5.4,12,12,12s12-5.4,12-12V66H404c2.8,0,5.1,2.6,5.1,5.9v53.6c0,6.6,5.4,12,12,12s12-5.4,12-12V71.9 C433.1,55.4,420.1,42,404,42z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 976 B |
784
src/core/file.rs
784
src/core/file.rs
|
|
@ -2,40 +2,28 @@ use crate::core::{
|
|||
kinds::ServiceItemKind, service_items::ServiceItem,
|
||||
slide::Background,
|
||||
};
|
||||
use cosmic::widget::image::Handle;
|
||||
use miette::{IntoDiagnostic, Result, miette};
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
iter,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tar::{Archive, Builder};
|
||||
use tracing::{debug, error};
|
||||
use zstd::{Decoder, Encoder};
|
||||
use tar::Builder;
|
||||
use tracing::error;
|
||||
use zstd::Encoder;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn save(
|
||||
pub async fn save(
|
||||
list: Vec<ServiceItem>,
|
||||
path: impl AsRef<Path>,
|
||||
overwrite: bool,
|
||||
) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
if overwrite && path.exists() {
|
||||
fs::remove_file(path).into_diagnostic()?;
|
||||
}
|
||||
let save_file = File::create(path).into_diagnostic()?;
|
||||
let ron_pretty = ron::ser::PrettyConfig::default();
|
||||
let ron = ron::ser::to_string_pretty(&list, ron_pretty)
|
||||
.into_diagnostic()?;
|
||||
let ron = process_service_items(&list)?;
|
||||
|
||||
let encoder = Encoder::new(save_file, 3)
|
||||
.expect("file encoder shouldn't fail")
|
||||
.auto_finish();
|
||||
let encoder = Encoder::new(save_file, 3).unwrap();
|
||||
let mut tar = Builder::new(encoder);
|
||||
let mut temp_dir = dirs::data_dir().expect(
|
||||
"there should be a data directory, ~/.local/share/ for linux, but couldn't find it",
|
||||
);
|
||||
let mut temp_dir = dirs::data_dir().unwrap();
|
||||
temp_dir.push("lumina");
|
||||
let mut s: String =
|
||||
iter::repeat_with(fastrand::alphanumeric).take(5).collect();
|
||||
|
|
@ -43,7 +31,6 @@ pub fn save(
|
|||
temp_dir.push(s);
|
||||
fs::create_dir_all(&temp_dir).into_diagnostic()?;
|
||||
let service_file = temp_dir.join("serviceitems.ron");
|
||||
debug!(?service_file);
|
||||
fs::File::create(&service_file).into_diagnostic()?;
|
||||
match fs::File::options()
|
||||
.read(true)
|
||||
|
|
@ -51,40 +38,19 @@ pub fn save(
|
|||
.open(service_file)
|
||||
{
|
||||
Ok(mut f) => {
|
||||
match f.write(ron.as_bytes()) {
|
||||
Ok(size) => {
|
||||
debug!(size);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(?e);
|
||||
return Err(miette!("PROBS: {e}"));
|
||||
}
|
||||
}
|
||||
match tar.append_file("serviceitems.ron", &mut f) {
|
||||
Ok(()) => {
|
||||
debug!(
|
||||
"should have added serviceitems.ron to the file"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(?e);
|
||||
return Err(miette!("PROBS: {e}"));
|
||||
}
|
||||
}
|
||||
f.write(ron.as_bytes()).into_diagnostic()?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("There were problems making a file i guess: {e}");
|
||||
return Err(miette!("There was a problem: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut append_file = |path: PathBuf| -> Result<()> {
|
||||
let file_name = path.file_name().unwrap_or_default();
|
||||
let mut file = fs::File::open(&path).into_diagnostic()?;
|
||||
tar.append_file(file_name, &mut file).into_diagnostic()?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// let list list.iter_mut().map(|item| {
|
||||
// match item.kind {
|
||||
// ServiceItemKind::Song(mut song) => {
|
||||
// song.background
|
||||
// }
|
||||
// }
|
||||
// }).collect();
|
||||
for item in list {
|
||||
let background;
|
||||
let audio: Option<PathBuf>;
|
||||
|
|
@ -118,417 +84,323 @@ pub fn save(
|
|||
todo!()
|
||||
}
|
||||
}
|
||||
if let Some(path) = audio
|
||||
&& path.exists()
|
||||
{
|
||||
debug!(?path);
|
||||
append_file(path)?;
|
||||
}
|
||||
if let Some(background) = background
|
||||
&& let path = background.path
|
||||
&& path.exists()
|
||||
{
|
||||
debug!(?path);
|
||||
append_file(path)?;
|
||||
}
|
||||
for slide in item.slides {
|
||||
if let Some(svg) = slide.text_svg
|
||||
&& let Some(path) = svg.path
|
||||
{
|
||||
append_file(path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match tar.finish() {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
error!(?e);
|
||||
return Err(miette!("tar error: {e}"));
|
||||
}
|
||||
}
|
||||
fs::remove_dir_all(temp_dir).into_diagnostic()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||
let decoder =
|
||||
Decoder::new(fs::File::open(&path).into_diagnostic()?)
|
||||
.into_diagnostic()?;
|
||||
let mut tar = Archive::new(decoder);
|
||||
|
||||
let mut cache_dir =
|
||||
dirs::cache_dir().expect("Should be a cache dir");
|
||||
cache_dir.push("lumina");
|
||||
cache_dir.push("cached_save_files");
|
||||
|
||||
let save_name_ext = path
|
||||
.as_ref()
|
||||
.extension()
|
||||
.expect("Should have extension")
|
||||
.to_str()
|
||||
.expect("Should be fine");
|
||||
let save_name_string = path
|
||||
.as_ref()
|
||||
.file_name()
|
||||
.expect("Should be a name")
|
||||
.to_os_string()
|
||||
.into_string()
|
||||
.expect("Should be fine");
|
||||
let save_name = save_name_string
|
||||
.trim_end_matches(&format!(".{save_name_ext}"));
|
||||
cache_dir.push(save_name);
|
||||
|
||||
if let Err(e) = fs::remove_dir_all(&cache_dir) {
|
||||
debug!("There is no dir here: {e}");
|
||||
}
|
||||
fs::create_dir_all(&cache_dir).into_diagnostic()?;
|
||||
|
||||
for entry in tar.entries().into_diagnostic()? {
|
||||
let mut entry = entry.into_diagnostic()?;
|
||||
entry.unpack_in(&cache_dir).into_diagnostic()?;
|
||||
}
|
||||
|
||||
let mut dir = fs::read_dir(&cache_dir).into_diagnostic()?;
|
||||
let ron_file = dir
|
||||
.find_map(|file| {
|
||||
if file.as_ref().ok()?.path().extension()?.to_str()?
|
||||
== "ron"
|
||||
{
|
||||
Some(file.ok()?.path())
|
||||
if let Some(file) = audio {
|
||||
let audio_file =
|
||||
temp_dir.join(file.file_name().expect(
|
||||
"Audio file couldn't be added to temp_dir",
|
||||
));
|
||||
if let Ok(file) = file.strip_prefix("file://") {
|
||||
fs::File::create(&audio_file).into_diagnostic()?;
|
||||
fs::copy(file, audio_file).into_diagnostic()?;
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.expect("Should have a ron file");
|
||||
|
||||
let ron_string =
|
||||
fs::read_to_string(ron_file).into_diagnostic()?;
|
||||
|
||||
let mut items =
|
||||
ron::de::from_str::<Vec<ServiceItem>>(&ron_string)
|
||||
.into_diagnostic()?;
|
||||
|
||||
for item in &mut items {
|
||||
let dir = fs::read_dir(&cache_dir).into_diagnostic()?;
|
||||
for file in dir {
|
||||
for slide in &mut item.slides {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
let audio_path =
|
||||
slide.audio().clone().unwrap_or_default();
|
||||
let text_path = slide
|
||||
.text_svg
|
||||
.as_ref()
|
||||
.and_then(|svg| svg.path.clone());
|
||||
if Some(file_name.as_os_str())
|
||||
== slide.background.path.file_name()
|
||||
{
|
||||
slide.background.path = file.path();
|
||||
} else if Some(file_name.as_os_str())
|
||||
== audio_path.file_name()
|
||||
{
|
||||
let new_slide = slide
|
||||
.clone()
|
||||
.set_audio(Some(file.path()));
|
||||
*slide = new_slide;
|
||||
} else if Some(file_name.as_os_str())
|
||||
== text_path
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.file_name()
|
||||
&& let Some(svg) = slide.text_svg.as_mut()
|
||||
{
|
||||
svg.path = Some(file.path());
|
||||
svg.handle =
|
||||
Some(Handle::from_path(file.path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &mut item.kind {
|
||||
ServiceItemKind::Song(song) => {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
let audio_path =
|
||||
song.audio.clone().unwrap_or_default();
|
||||
if Some(file_name.as_os_str())
|
||||
== song
|
||||
.background
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.path
|
||||
.file_name()
|
||||
{
|
||||
let background = song.background.clone();
|
||||
song.background =
|
||||
background.map(|mut background| {
|
||||
background.path = file.path();
|
||||
background
|
||||
});
|
||||
} else if Some(file_name.as_os_str())
|
||||
== audio_path.file_name()
|
||||
{
|
||||
song.audio = Some(file.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
ServiceItemKind::Video(video) => {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
if Some(file_name.as_os_str())
|
||||
== video.path.file_name()
|
||||
{
|
||||
video.path = file.path();
|
||||
}
|
||||
}
|
||||
}
|
||||
ServiceItemKind::Image(image) => {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
if Some(file_name.as_os_str())
|
||||
== image.path.file_name()
|
||||
{
|
||||
image.path = file.path();
|
||||
}
|
||||
}
|
||||
}
|
||||
ServiceItemKind::Presentation(presentation) => {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
if Some(file_name.as_os_str())
|
||||
== presentation.path.file_name()
|
||||
{
|
||||
presentation.path = file.path();
|
||||
}
|
||||
}
|
||||
}
|
||||
ServiceItemKind::Content(_slide) => todo!(),
|
||||
fs::File::create(&audio_file).into_diagnostic()?;
|
||||
fs::copy(file, audio_file).into_diagnostic()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use resvg::usvg::fontdb;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
core::{
|
||||
service_items::ServiceTrait,
|
||||
slide::{Slide, TextAlignment},
|
||||
songs::{Song, VerseName},
|
||||
},
|
||||
ui::text_svg::text_svg_generator,
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
|
||||
fn test_song() -> Song {
|
||||
let lyrics = "Some({Verse(number:4):\"Our Savior displayed\\nOn a criminal\\'s cross\\n\\nDarkness rejoiced as though\\nHeaven had lost\\n\\nBut then Jesus arose\\nWith our freedom in hand\\n\\nThat\\'s when death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Intro(number:1):\"Death Was Arrested\\nNorth Point Worship\",Verse(number:3):\"Released from my chains,\\nI\\'m a prisoner no more\\n\\nMy shame was a ransom\\nHe faithfully bore\\n\\nHe cancelled my debt and\\nHe called me His friend\\n\\nWhen death was arrested\\nAnd my life began\",Bridge(number:1):\"Oh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\\n\\nOh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\",Other(number:99):\"When death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Verse(number:2):\"Ash was redeemed\\nOnly beauty remains\\n\\nMy orphan heart\\nWas given a name\\n\\nMy mourning grew quiet,\\nMy feet rose to dance\\n\\nWhen death was arrested\\nAnd my life began\",Verse(number:1):\"Alone in my sorrow\\nAnd dead in my sin\\n\\nLost without hope\\nWith no place to begin\\n\\nYour love made a way\\nTo let mercy come in\\n\\nWhen death was arrested\\nAnd my life began\",Chorus(number:1):\"Oh, Your grace so free,\\nWashes over me\\n\\nYou have made me new,\\nNow life begins with You\\n\\nIt\\'s Your endless love,\\nPouring down on us\\n\\nYou have made us new,\\nNow life begins with You\"})".to_string();
|
||||
let verse_map: Option<HashMap<VerseName, String>> =
|
||||
ron::from_str(&lyrics).unwrap();
|
||||
Song {
|
||||
id: 7,
|
||||
title: "Death Was Arrested".to_string(),
|
||||
lyrics: Some(lyrics),
|
||||
author: Some(
|
||||
"North Point Worship".to_string(),
|
||||
),
|
||||
ccli: None,
|
||||
audio: Some("/home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3".into()),
|
||||
verse_order: Some(vec!["Some([Chorus(number:1),Intro(number:1),Other(number:99),Bridge(number:1),Verse(number:4),Verse(number:2),Verse(number:3),Verse(number:1)])".to_string()]),
|
||||
background: Some(Background::try_from("/home/chris/nc/tfc/openlp/Flood/motions/Ocean_Floor_HD.mp4").unwrap()),
|
||||
text_alignment: Some(TextAlignment::MiddleCenter),
|
||||
font: None,
|
||||
font_size: Some(120),
|
||||
font_style: None,
|
||||
font_weight: None,
|
||||
text_color: None,
|
||||
stroke_size: None,
|
||||
verses: Some(vec![VerseName::Chorus { number: 1 }, VerseName::Intro { number: 1 }, VerseName::Other { number: 99 }, VerseName::Bridge { number: 1 }, VerseName::Verse { number: 4 }, VerseName::Verse { number: 2 }, VerseName::Verse { number: 3 }, VerseName::Verse { number: 1 }
|
||||
]),
|
||||
verse_map,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_items() -> Vec<ServiceItem> {
|
||||
let song = test_song();
|
||||
let mut fontdb = fontdb::Database::new();
|
||||
fontdb.load_system_fonts();
|
||||
let fontdb = Arc::new(fontdb);
|
||||
let slides = song
|
||||
.to_slides()
|
||||
.unwrap()
|
||||
.into_par_iter()
|
||||
.map(|slide| {
|
||||
text_svg_generator(
|
||||
slide.clone(),
|
||||
&Arc::clone(&fontdb),
|
||||
)
|
||||
.map_or_else(
|
||||
|e| {
|
||||
assert!(false, "Couldn't create svg: {e}");
|
||||
slide
|
||||
},
|
||||
|slide| slide,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Slide>>();
|
||||
let items = vec![
|
||||
ServiceItem {
|
||||
database_id: 7,
|
||||
kind: ServiceItemKind::Song(song.clone()),
|
||||
id: 0,
|
||||
title: "Death was Arrested".into(),
|
||||
slides: slides.clone(),
|
||||
},
|
||||
ServiceItem {
|
||||
database_id: 7,
|
||||
kind: ServiceItemKind::Song(song),
|
||||
id: 1,
|
||||
title: "Death was Arrested".into(),
|
||||
slides: slides,
|
||||
},
|
||||
];
|
||||
items
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load() -> Result<(), String> {
|
||||
test_save();
|
||||
let path = PathBuf::from("./test.pres");
|
||||
let result = load(&path);
|
||||
match result {
|
||||
Ok(items) => {
|
||||
assert!(items.len() > 0);
|
||||
// assert_eq!(items, get_items());
|
||||
let cache_dir = cache_dir();
|
||||
assert!(fs::read_dir(&cache_dir).is_ok());
|
||||
assert!(
|
||||
find_paths(&items),
|
||||
"Some paths must not have the cache_dir in it's path"
|
||||
);
|
||||
find_svgs(&items)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_svgs(items: &Vec<ServiceItem>) -> Result<(), String> {
|
||||
let cache_dir = cache_dir();
|
||||
items.iter().try_for_each(|item| {
|
||||
if let ServiceItemKind::Song(..) = item.kind {
|
||||
item.slides.iter().try_for_each(|slide| {
|
||||
slide.text_svg.as_ref().map_or(Err(String::from("There is no TextSvg for this song")), |text_svg| {
|
||||
|
||||
if text_svg.handle.is_none() {
|
||||
return Err(String::from("There is no handle in this song's TextSvg"));
|
||||
};
|
||||
|
||||
text_svg.path.as_ref().map_or(Err(String::from("There is no path in this song's TextSvg")), |path| {
|
||||
if path.exists() {
|
||||
let mut path = path.clone();
|
||||
if path.metadata().unwrap().len() < 20000 {
|
||||
return Err(String::from("SVG text is too small, maybe the svg didn't generate properly"))
|
||||
}
|
||||
if path.pop() && path == cache_dir {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(String::from("The path of the TextSvg isn't in the load directory"))
|
||||
}
|
||||
} else {
|
||||
Err(String::from("The path in this TextSvg doesn't exist"))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
if let Some(file) = background {
|
||||
let background_file =
|
||||
temp_dir.join(file.path.file_name().expect(
|
||||
"Background file couldn't be added to temp_dir",
|
||||
));
|
||||
if let Ok(file) = file.path.strip_prefix("file://") {
|
||||
fs::File::create(&background_file)
|
||||
.into_diagnostic()?;
|
||||
fs::copy(file, background_file).into_diagnostic()?;
|
||||
} else {
|
||||
Ok(())
|
||||
fs::File::create(&background_file)
|
||||
.into_diagnostic()?;
|
||||
fs::copy(file.path, background_file)
|
||||
.into_diagnostic()?;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// checks to make sure all paths in slides and items point to cache_dir
|
||||
fn find_paths(items: &Vec<ServiceItem>) -> bool {
|
||||
let cache_dir = cache_dir();
|
||||
items.iter().all(|item| {
|
||||
match &item.kind {
|
||||
ServiceItemKind::Song(song) => {
|
||||
if let Some(bg) = &song.background {
|
||||
if !bg.path.starts_with(&cache_dir) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(audio) = &song.audio {
|
||||
if !audio.starts_with(&cache_dir) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
ServiceItemKind::Video(video) => {
|
||||
if !video.path.starts_with(&cache_dir) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ServiceItemKind::Image(image) => {
|
||||
if !image.path.starts_with(&cache_dir) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ServiceItemKind::Presentation(presentation) => {
|
||||
if !presentation.path.starts_with(&cache_dir) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ServiceItemKind::Content(_slide) => todo!(),
|
||||
}
|
||||
for slide in &item.slides {
|
||||
if !slide.background().path.starts_with(&cache_dir) {
|
||||
return false;
|
||||
}
|
||||
if !slide.audio().map_or(true, |audio| {
|
||||
audio.starts_with(&cache_dir)
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
fn cache_dir() -> PathBuf {
|
||||
let mut cache_dir = dirs::cache_dir().unwrap();
|
||||
cache_dir.push("lumina");
|
||||
cache_dir.push("cached_save_files");
|
||||
cache_dir.push("test");
|
||||
cache_dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save() {
|
||||
let path = PathBuf::from("./test.pres");
|
||||
let list = get_items();
|
||||
match save(list, &path, true) {
|
||||
Ok(_) => {
|
||||
assert!(path.is_file());
|
||||
let Ok(file) = fs::File::open(path) else {
|
||||
return assert!(false, "couldn't open file");
|
||||
};
|
||||
let Ok(size) = file.metadata().map(|data| data.len())
|
||||
else {
|
||||
return assert!(
|
||||
false,
|
||||
"couldn't get file metadata"
|
||||
);
|
||||
};
|
||||
assert!(size > 0);
|
||||
}
|
||||
Err(e) => assert!(false, "{e}"),
|
||||
}
|
||||
}
|
||||
tar.append_dir_all(path, temp_dir).into_diagnostic()?;
|
||||
tar.finish().into_diagnostic()
|
||||
}
|
||||
|
||||
async fn clear_temp_dir(_temp_dir: &Path) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn process_service_items(items: &Vec<ServiceItem>) -> Result<String> {
|
||||
Ok(items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let ron = ron::ser::to_string(item);
|
||||
ron.ok()
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
// async fn process_song(
|
||||
// database_id: i32,
|
||||
// db: &mut SqliteConnection,
|
||||
// ) -> Result<Value> {
|
||||
// let song = get_song_from_db(database_id, db).await?;
|
||||
// let song_ron = ron::to_value(&song)?;
|
||||
// let kind_ron = ron::to_value(ServiceItemKind::Song)?;
|
||||
// let json =
|
||||
// serde_json::json!({"item": song_json, "kind": kind_json});
|
||||
// Ok(json)
|
||||
// }
|
||||
|
||||
// async fn process_image(
|
||||
// database_id: i32,
|
||||
// db: &mut SqliteConnection,
|
||||
// ) -> Result<Value> {
|
||||
// let image = get_image_from_db(database_id, db).await?;
|
||||
// let image_json = serde_json::to_value(&image)?;
|
||||
// let kind_json = serde_json::to_value(ServiceItemKind::Image)?;
|
||||
// let json =
|
||||
// serde_json::json!({"item": image_json, "kind": kind_json});
|
||||
// Ok(json)
|
||||
// }
|
||||
|
||||
// async fn process_video(
|
||||
// database_id: i32,
|
||||
// db: &mut SqliteConnection,
|
||||
// ) -> Result<Value> {
|
||||
// let video = get_video_from_db(database_id, db).await?;
|
||||
// let video_json = serde_json::to_value(&video)?;
|
||||
// let kind_json = serde_json::to_value(ServiceItemKind::Video)?;
|
||||
// let json =
|
||||
// serde_json::json!({"item": video_json, "kind": kind_json});
|
||||
// Ok(json)
|
||||
// }
|
||||
|
||||
// async fn process_presentation(
|
||||
// database_id: i32,
|
||||
// db: &mut SqliteConnection,
|
||||
// ) -> Result<Value> {
|
||||
// let presentation =
|
||||
// get_presentation_from_db(database_id, db).await?;
|
||||
// let presentation_json = serde_json::to_value(&presentation)?;
|
||||
// let kind_json = match presentation.kind {
|
||||
// PresKind::Html => serde_json::to_value(
|
||||
// ServiceItemKind::Presentation(PresKind::Html),
|
||||
// )?,
|
||||
// PresKind::Pdf => serde_json::to_value(
|
||||
// ServiceItemKind::Presentation(PresKind::Pdf),
|
||||
// )?,
|
||||
// PresKind::Generic => serde_json::to_value(
|
||||
// ServiceItemKind::Presentation(PresKind::Generic),
|
||||
// )?,
|
||||
// };
|
||||
// let json = serde_json::json!({"item": presentation_json, "kind": kind_json});
|
||||
// Ok(json)
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test {
|
||||
// use std::path::PathBuf;
|
||||
|
||||
// use super::*;
|
||||
// use fs::canonicalize;
|
||||
// use pretty_assertions::assert_eq;
|
||||
// use sqlx::Connection;
|
||||
// use tracing::debug;
|
||||
|
||||
// async fn get_db() -> SqliteConnection {
|
||||
// let mut data = dirs::data_local_dir().unwrap();
|
||||
// data.push("lumina");
|
||||
// data.push("library-db.sqlite3");
|
||||
// let mut db_url = String::from("sqlite://");
|
||||
// db_url.push_str(data.to_str().unwrap());
|
||||
// SqliteConnection::connect(&db_url).await.expect("problems")
|
||||
// }
|
||||
|
||||
// #[tokio::test(flavor = "current_thread")]
|
||||
// async fn test_process_song() {
|
||||
// let mut db = get_db().await;
|
||||
// let result = process_song(7, &mut db).await;
|
||||
// let json_song_file = PathBuf::from("./test/test_song.json");
|
||||
// if let Ok(path) = canonicalize(json_song_file) {
|
||||
// debug!(file = ?&path);
|
||||
// if let Ok(s) = fs::read_to_string(path) {
|
||||
// debug!(s);
|
||||
// match result {
|
||||
// Ok(json) => assert_eq!(json.to_string(), s),
|
||||
// Err(e) => panic!(
|
||||
// "There was an error in processing the song: {e}"
|
||||
// ),
|
||||
// }
|
||||
// } else {
|
||||
// panic!("String wasn't read from file");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Cannot find absolute path to test_song.json");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[tokio::test(flavor = "current_thread")]
|
||||
// async fn test_process_image() {
|
||||
// let mut db = get_db().await;
|
||||
// let result = process_image(3, &mut db).await;
|
||||
// let json_image_file = PathBuf::from("./test/test_image.json");
|
||||
// if let Ok(path) = canonicalize(json_image_file) {
|
||||
// debug!(file = ?&path);
|
||||
// if let Ok(s) = fs::read_to_string(path) {
|
||||
// debug!(s);
|
||||
// match result {
|
||||
// Ok(json) => assert_eq!(json.to_string(), s),
|
||||
// Err(e) => panic!(
|
||||
// "There was an error in processing the image: {e}"
|
||||
// ),
|
||||
// }
|
||||
// } else {
|
||||
// panic!("String wasn't read from file");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Cannot find absolute path to test_image.json");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[tokio::test(flavor = "current_thread")]
|
||||
// async fn test_process_video() {
|
||||
// let mut db = get_db().await;
|
||||
// let result = process_video(73, &mut db).await;
|
||||
// let json_video_file = PathBuf::from("./test/test_video.json");
|
||||
// if let Ok(path) = canonicalize(json_video_file) {
|
||||
// debug!(file = ?&path);
|
||||
// if let Ok(s) = fs::read_to_string(path) {
|
||||
// debug!(s);
|
||||
// match result {
|
||||
// Ok(json) => assert_eq!(json.to_string(), s),
|
||||
// Err(e) => panic!(
|
||||
// "There was an error in processing the video: {e}"
|
||||
// ),
|
||||
// }
|
||||
// } else {
|
||||
// panic!("String wasn't read from file");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Cannot find absolute path to test_video.json");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[tokio::test(flavor = "current_thread")]
|
||||
// async fn test_process_presentation() {
|
||||
// let mut db = get_db().await;
|
||||
// let result = process_presentation(54, &mut db).await;
|
||||
// let json_presentation_file =
|
||||
// PathBuf::from("./test/test_presentation.json");
|
||||
// if let Ok(path) = canonicalize(json_presentation_file) {
|
||||
// debug!(file = ?&path);
|
||||
// if let Ok(s) = fs::read_to_string(path) {
|
||||
// debug!(s);
|
||||
// match result {
|
||||
// Ok(json) => assert_eq!(json.to_string(), s),
|
||||
// Err(e) => panic!(
|
||||
// "There was an error in processing the presentation: {e}"
|
||||
// ),
|
||||
// }
|
||||
// } else {
|
||||
// panic!("String wasn't read from file");
|
||||
// }
|
||||
// } else {
|
||||
// panic!(
|
||||
// "Cannot find absolute path to test_presentation.json"
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn get_items() -> Vec<ServiceItem> {
|
||||
// let items = vec![
|
||||
// ServiceItem {
|
||||
// database_id: 7,
|
||||
// kind: ServiceItemKind::Song,
|
||||
// id: 0,
|
||||
// },
|
||||
// ServiceItem {
|
||||
// database_id: 54,
|
||||
// kind: ServiceItemKind::Presentation(PresKind::Html),
|
||||
// id: 0,
|
||||
// },
|
||||
// ServiceItem {
|
||||
// database_id: 73,
|
||||
// kind: ServiceItemKind::Video,
|
||||
// id: 0,
|
||||
// },
|
||||
// ];
|
||||
// items
|
||||
// }
|
||||
|
||||
// #[tokio::test]
|
||||
// async fn test_service_items() {
|
||||
// let mut db = get_db().await;
|
||||
// let items = get_items();
|
||||
// let json_item_file =
|
||||
// PathBuf::from("./test/test_service_items.json");
|
||||
// let result = process_service_items(&items, &mut db).await;
|
||||
// if let Ok(path) = canonicalize(json_item_file) {
|
||||
// if let Ok(s) = fs::read_to_string(path) {
|
||||
// match result {
|
||||
// Ok(strings) => assert_eq!(strings.to_string(), s),
|
||||
// Err(e) => panic!("There was an error: {e}"),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // #[tokio::test]
|
||||
// // async fn test_save() {
|
||||
// // let path = PathBuf::from("~/dev/lumina/src/rust/core/test.pres");
|
||||
// // let list = get_items();
|
||||
// // match save(list, path).await {
|
||||
// // Ok(_) => assert!(true),
|
||||
// // Err(e) => panic!("There was an error: {e}"),
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// #[tokio::test]
|
||||
// async fn test_store() {
|
||||
// let path = PathBuf::from(
|
||||
// "/home/chris/dev/lumina/src/rust/core/test.pres",
|
||||
// );
|
||||
// let save_file = match File::create(path) {
|
||||
// Ok(f) => f,
|
||||
// Err(e) => panic!("Couldn't create save_file: {e}"),
|
||||
// };
|
||||
// let mut db = get_db().await;
|
||||
// let list = get_items();
|
||||
// if let Ok(json) = process_service_items(&list, &mut db).await
|
||||
// {
|
||||
// println!("{:?}", json);
|
||||
// match store_service_items(
|
||||
// &list, &mut db, &save_file, &json,
|
||||
// )
|
||||
// .await
|
||||
// {
|
||||
// Ok(_) => assert!(true),
|
||||
// Err(e) => panic!("There was an error: {e}"),
|
||||
// }
|
||||
// } else {
|
||||
// panic!("There was an error getting the json value");
|
||||
// }
|
||||
// }
|
||||
|
||||
// // #[tokio::test]
|
||||
// // async fn test_things() {
|
||||
// // let mut temp_dir = dirs::data_dir().unwrap();
|
||||
// // temp_dir.push("lumina");
|
||||
// // let mut s: String =
|
||||
// // iter::repeat_with(fastrand::alphanumeric)
|
||||
// // .take(5)
|
||||
// // .collect();
|
||||
// // s.insert_str(0, "temp_");
|
||||
// // temp_dir.push(s);
|
||||
// // let _ = fs::create_dir_all(&temp_dir);
|
||||
// // let mut db = get_db().await;
|
||||
// // let service_file = temp_dir.join("serviceitems.json");
|
||||
// // let list = get_items();
|
||||
// // if let Ok(json) = process_service_items(&list, &mut db).await {
|
||||
// // let _ = fs::File::create(&service_file);
|
||||
// // match fs::write(service_file, json.to_string()) {
|
||||
// // Ok(_) => assert!(true),
|
||||
// // Err(e) => panic!("There was an error: {e}"),
|
||||
// // }
|
||||
// // } else {
|
||||
// // panic!("There was an error getting the json value");
|
||||
// // }
|
||||
// // }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -72,10 +72,11 @@ impl Content for Image {
|
|||
|
||||
fn subtext(&self) -> String {
|
||||
if self.path.exists() {
|
||||
self.path.file_name().map_or_else(
|
||||
|| "Missing image".into(),
|
||||
|f| f.to_string_lossy().to_string(),
|
||||
)
|
||||
self.path
|
||||
.file_name()
|
||||
.map_or("Missing image".into(), |f| {
|
||||
f.to_string_lossy().to_string()
|
||||
})
|
||||
} else {
|
||||
"Missing image".into()
|
||||
}
|
||||
|
|
@ -88,7 +89,6 @@ impl From<Value> for Image {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
impl From<&Value> for Image {
|
||||
fn from(value: &Value) -> Self {
|
||||
match value {
|
||||
|
|
@ -269,9 +269,7 @@ mod test {
|
|||
fn test_image(title: String) -> Image {
|
||||
Image {
|
||||
title,
|
||||
path: PathBuf::from(
|
||||
"/home/chris/pics/memes/no-i-dont-think.gif",
|
||||
),
|
||||
path: PathBuf::from("~/pics/camprules2024.mp4"),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -282,10 +280,10 @@ mod test {
|
|||
items: vec![],
|
||||
kind: LibraryKind::Image,
|
||||
};
|
||||
let mut db = add_db().await.unwrap().acquire().await.unwrap();
|
||||
let mut db = crate::core::model::get_db().await;
|
||||
image_model.load_from_db(&mut db).await;
|
||||
if let Some(image) = image_model.find(|i| i.id == 23) {
|
||||
let test_image = test_image("no-i-dont-think.gif".into());
|
||||
if let Some(image) = image_model.find(|i| i.id == 3) {
|
||||
let test_image = test_image("nccq5".into());
|
||||
assert_eq!(test_image.title, image.title);
|
||||
} else {
|
||||
assert!(false);
|
||||
|
|
@ -319,9 +317,4 @@ mod test {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_db() -> Result<SqlitePool> {
|
||||
let db_url = String::from("sqlite://./test.db");
|
||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,9 @@ impl TryFrom<PathBuf> for ServiceItemKind {
|
|||
let ext = path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.ok_or_else(|| {
|
||||
miette::miette!(
|
||||
"There isn't an extension on this file"
|
||||
)
|
||||
})?;
|
||||
.ok_or(miette::miette!(
|
||||
"There isn't an extension on this file"
|
||||
))?;
|
||||
match ext {
|
||||
"png" | "jpg" | "jpeg" => {
|
||||
Ok(Self::Image(Image::from(path)))
|
||||
|
|
@ -40,7 +38,6 @@ impl TryFrom<PathBuf> for ServiceItemKind {
|
|||
"mp4" | "mkv" | "webm" => {
|
||||
Ok(Self::Video(Video::from(path)))
|
||||
}
|
||||
"pdf" => Ok(Self::Presentation(Presentation::from(path))),
|
||||
_ => Err(miette::miette!("Unknown item")),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{borrow::Cow, fs, mem::replace, path::PathBuf};
|
||||
use std::{borrow::Cow, mem::replace, path::PathBuf};
|
||||
|
||||
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
||||
use miette::{IntoDiagnostic, Result, miette};
|
||||
|
|
@ -84,36 +84,26 @@ impl<T> Model<T> {
|
|||
}
|
||||
|
||||
pub fn update_item(&mut self, item: T, index: i32) -> Result<()> {
|
||||
self.items
|
||||
.get_mut(
|
||||
usize::try_from(index)
|
||||
.expect("Shouldn't be negative"),
|
||||
)
|
||||
.map_or_else(
|
||||
|| {
|
||||
Err(miette!(
|
||||
"Item doesn't exist in model. Id was {index}"
|
||||
))
|
||||
},
|
||||
|current_item| {
|
||||
let _old_item = replace(current_item, item);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
if let Some(current_item) = self.items.get_mut(index as usize)
|
||||
{
|
||||
let _old_item = replace(current_item, item);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(miette!(
|
||||
"Item doesn't exist in model. Id was {}",
|
||||
index
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_item(&mut self, index: i32) -> Result<()> {
|
||||
self.items.remove(
|
||||
usize::try_from(index).expect("Shouldn't be negative"),
|
||||
);
|
||||
self.items.remove(index as usize);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_item(&self, index: i32) -> Option<&T> {
|
||||
self.items.get(
|
||||
usize::try_from(index).expect("shouldn't be negative"),
|
||||
)
|
||||
self.items.get(index as usize)
|
||||
}
|
||||
|
||||
pub fn find<P>(&self, f: P) -> Option<&T>
|
||||
|
|
@ -124,10 +114,7 @@ impl<T> Model<T> {
|
|||
}
|
||||
|
||||
pub fn insert_item(&mut self, item: T, index: i32) -> Result<()> {
|
||||
self.items.insert(
|
||||
usize::try_from(index).expect("Shouldn't be negative"),
|
||||
item,
|
||||
);
|
||||
self.items.insert(index as usize, item);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -144,13 +131,11 @@ impl<T> Model<T> {
|
|||
// }
|
||||
|
||||
pub async fn get_db() -> SqliteConnection {
|
||||
let mut data = dirs::data_local_dir()
|
||||
.expect("Should be able to find a data dir");
|
||||
let mut data = dirs::data_local_dir().unwrap();
|
||||
data.push("lumina");
|
||||
let _ = fs::create_dir_all(&data);
|
||||
data.push("library-db.sqlite3");
|
||||
let mut db_url = String::from("sqlite://");
|
||||
db_url.push_str(data.to_str().expect("Should be there"));
|
||||
db_url.push_str(data.to_str().unwrap());
|
||||
SqliteConnection::connect(&db_url).await.expect("problems")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,24 +65,27 @@ impl From<PathBuf> for Presentation {
|
|||
.to_str()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"pdf" => Document::open(&value.as_path()).map_or(
|
||||
PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index: 0,
|
||||
},
|
||||
|document| {
|
||||
document.page_count().map_or(
|
||||
"pdf" => {
|
||||
if let Ok(document) = Document::open(&value.as_path())
|
||||
{
|
||||
if let Ok(count) = document.page_count() {
|
||||
PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index: count - 1,
|
||||
}
|
||||
} else {
|
||||
PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index: 0,
|
||||
},
|
||||
|count| PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index: count - 1,
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
"html" => PresKind::Html,
|
||||
_ => PresKind::Generic,
|
||||
};
|
||||
|
|
@ -126,10 +129,11 @@ impl Content for Presentation {
|
|||
|
||||
fn subtext(&self) -> String {
|
||||
if self.path.exists() {
|
||||
self.path.file_name().map_or_else(
|
||||
|| "Missing presentation".into(),
|
||||
|f| f.to_string_lossy().to_string(),
|
||||
)
|
||||
self.path
|
||||
.file_name()
|
||||
.map_or("Missing presentation".into(), |f| {
|
||||
f.to_string_lossy().to_string()
|
||||
})
|
||||
} else {
|
||||
"Missing presentation".into()
|
||||
}
|
||||
|
|
@ -142,7 +146,6 @@ impl From<Value> for Presentation {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
impl From<&Value> for Presentation {
|
||||
fn from(value: &Value) -> Self {
|
||||
match value {
|
||||
|
|
@ -203,14 +206,15 @@ impl ServiceTrait for Presentation {
|
|||
let pages: Vec<Handle> = pages
|
||||
.enumerate()
|
||||
.filter_map(|(index, page)| {
|
||||
let index = i32::try_from(index)
|
||||
.expect("Shouldn't be that high");
|
||||
|
||||
if index < starting_index || index > ending_index {
|
||||
if (index as i32) < starting_index {
|
||||
return None;
|
||||
} else if (index as i32) > ending_index {
|
||||
return None;
|
||||
}
|
||||
|
||||
let page = page.ok()?;
|
||||
let Some(page) = page.ok() else {
|
||||
return None;
|
||||
};
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let Ok(pixmap) = page
|
||||
|
|
@ -244,10 +248,7 @@ impl ServiceTrait for Presentation {
|
|||
.video_loop(false)
|
||||
.video_start_time(0.0)
|
||||
.video_end_time(0.0)
|
||||
.pdf_index(
|
||||
u32::try_from(index)
|
||||
.expect("Shouldn't get that high"),
|
||||
)
|
||||
.pdf_index(index as u32)
|
||||
.pdf_page(page)
|
||||
.build()?;
|
||||
slides.push(slide);
|
||||
|
|
@ -333,38 +334,32 @@ impl Model<Presentation> {
|
|||
presentation.ending_index,
|
||||
) {
|
||||
PresKind::Pdf {
|
||||
starting_index: i32::try_from(
|
||||
starting_index,
|
||||
)
|
||||
.expect("Shouldn't get that high"),
|
||||
ending_index: i32::try_from(
|
||||
ending_index,
|
||||
)
|
||||
.expect("Shouldn't get that high"),
|
||||
starting_index: starting_index as i32,
|
||||
ending_index: ending_index as i32,
|
||||
}
|
||||
} else {
|
||||
let path =
|
||||
PathBuf::from(presentation.path);
|
||||
|
||||
Document::open(path.as_path()).map_or(
|
||||
PresKind::Generic,
|
||||
|document| {
|
||||
document.page_count().map_or(
|
||||
PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index: 0,
|
||||
},
|
||||
|count| {
|
||||
let ending_index =
|
||||
count - 1;
|
||||
PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index,
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
if let Ok(document) =
|
||||
Document::open(path.as_path())
|
||||
{
|
||||
if let Ok(count) =
|
||||
document.page_count()
|
||||
{
|
||||
let ending_index = count - 1;
|
||||
PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index,
|
||||
}
|
||||
} else {
|
||||
PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index: 0,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PresKind::Generic
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -398,22 +393,11 @@ pub async fn add_presentation_to_db(
|
|||
.unwrap_or_default();
|
||||
let html = presentation.kind == PresKind::Html;
|
||||
let mut db = db.detach();
|
||||
let (starting_index, ending_index) = if let PresKind::Pdf {
|
||||
starting_index,
|
||||
ending_index,
|
||||
} = presentation.kind
|
||||
{
|
||||
(starting_index, ending_index)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
query!(
|
||||
r#"INSERT INTO presentations (title, file_path, html, starting_index, ending_index) VALUES ($1, $2, $3, $4, $5)"#,
|
||||
r#"INSERT INTO presentations (title, file_path, html) VALUES ($1, $2, $3)"#,
|
||||
presentation.title,
|
||||
path,
|
||||
html,
|
||||
starting_index,
|
||||
ending_index
|
||||
)
|
||||
.execute(&mut db)
|
||||
.await
|
||||
|
|
@ -432,17 +416,16 @@ pub async fn update_presentation_in_db(
|
|||
.unwrap_or_default();
|
||||
let html = presentation.kind == PresKind::Html;
|
||||
let mut db = db.detach();
|
||||
let (starting_index, ending_index) = if let PresKind::Pdf {
|
||||
let mut starting_index = 0;
|
||||
let mut ending_index = 0;
|
||||
if let PresKind::Pdf {
|
||||
starting_index: s_index,
|
||||
ending_index: e_index,
|
||||
} =
|
||||
presentation.get_kind()
|
||||
} = presentation.get_kind()
|
||||
{
|
||||
(*s_index, *e_index)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
debug!(starting_index, ending_index);
|
||||
starting_index = *s_index;
|
||||
ending_index = *e_index;
|
||||
}
|
||||
let id = presentation.id;
|
||||
if let Err(e) =
|
||||
query!("SELECT id FROM presentations where id = $1", id)
|
||||
|
|
@ -456,10 +439,7 @@ pub async fn update_presentation_in_db(
|
|||
let Some(mut max) = ids.iter().map(|r| r.id).max() else {
|
||||
return Err(miette::miette!("cannot find max id"));
|
||||
};
|
||||
debug!(
|
||||
?e,
|
||||
"Presentation not found adding a new presentation"
|
||||
);
|
||||
debug!(?e, "Presentation not found");
|
||||
max += 1;
|
||||
let result = query!(
|
||||
r#"INSERT into presentations VALUES($1, $2, $3, $4, $5, $6)"#,
|
||||
|
|
@ -476,7 +456,7 @@ pub async fn update_presentation_in_db(
|
|||
|
||||
return match result {
|
||||
Ok(_) => {
|
||||
debug!("presentation should have been added");
|
||||
debug!("should have been updated");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -490,13 +470,11 @@ pub async fn update_presentation_in_db(
|
|||
|
||||
debug!(?presentation, "should be been updated");
|
||||
let result = query!(
|
||||
r#"UPDATE presentations SET title = $2, file_path = $3, html = $4, starting_index = $5, ending_index = $6 WHERE id = $1"#,
|
||||
r#"UPDATE presentations SET title = $2, file_path = $3, html = $4 WHERE id = $1"#,
|
||||
presentation.id,
|
||||
presentation.title,
|
||||
path,
|
||||
html,
|
||||
starting_index,
|
||||
ending_index
|
||||
html
|
||||
)
|
||||
.execute(&mut db)
|
||||
.await.into_diagnostic();
|
||||
|
|
@ -528,13 +506,12 @@ mod test {
|
|||
|
||||
fn test_presentation() -> Presentation {
|
||||
Presentation {
|
||||
id: 4,
|
||||
title: "mzt52.pdf".into(),
|
||||
path: PathBuf::from("/home/chris/docs/mzt52.pdf"),
|
||||
kind: PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index: 67,
|
||||
},
|
||||
id: 54,
|
||||
title: "20240327T133649--12-isaiah-and-jesus__lesson_project_tfc".into(),
|
||||
path: PathBuf::from(
|
||||
"file:///home/chris/docs/notes/lessons/20240327T133649--12-isaiah-and-jesus__lesson_project_tfc.html",
|
||||
),
|
||||
kind: PresKind::Html,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -544,21 +521,16 @@ mod test {
|
|||
assert_eq!(pres.get_kind(), &PresKind::Generic)
|
||||
}
|
||||
|
||||
async fn add_db() -> Result<SqlitePool> {
|
||||
let mut db_url = String::from("sqlite://./test.db");
|
||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_db_and_model() {
|
||||
let mut presentation_model: Model<Presentation> = Model {
|
||||
items: vec![],
|
||||
kind: LibraryKind::Presentation,
|
||||
};
|
||||
let mut db = add_db().await.unwrap().acquire().await.unwrap();
|
||||
let mut db = crate::core::model::get_db().await;
|
||||
presentation_model.load_from_db(&mut db).await;
|
||||
if let Some(presentation) =
|
||||
presentation_model.find(|p| p.id == 4)
|
||||
presentation_model.find(|p| p.id == 54)
|
||||
{
|
||||
let test_presentation = test_presentation();
|
||||
assert_eq!(&test_presentation, presentation);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl Eq for ServiceItem {}
|
|||
|
||||
impl PartialOrd for ServiceItem {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
self.id.partial_cmp(&other.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,11 +89,9 @@ impl TryFrom<PathBuf> for ServiceItem {
|
|||
let ext = path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.ok_or_else(|| {
|
||||
miette::miette!(
|
||||
"There isn't an extension on this file"
|
||||
)
|
||||
})?;
|
||||
.ok_or(miette::miette!(
|
||||
"There isn't an extension on this file"
|
||||
))?;
|
||||
match ext {
|
||||
"png" | "jpg" | "jpeg" => {
|
||||
Ok(Self::from(&Image::from(path)))
|
||||
|
|
@ -159,8 +157,6 @@ impl From<Value> for ServiceItem {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
#[allow(clippy::match_like_matches_macro)]
|
||||
impl From<&Value> for ServiceItem {
|
||||
fn from(value: &Value) -> Self {
|
||||
match value {
|
||||
|
|
@ -284,61 +280,64 @@ impl From<Vec<ServiceItem>> for Service {
|
|||
|
||||
impl From<&Song> for ServiceItem {
|
||||
fn from(song: &Song) -> Self {
|
||||
song.to_slides().map_or_else(
|
||||
|_| Self {
|
||||
kind: ServiceItemKind::Song(song.clone()),
|
||||
database_id: song.id,
|
||||
title: song.title.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
|slides| Self {
|
||||
if let Ok(slides) = song.to_slides() {
|
||||
Self {
|
||||
kind: ServiceItemKind::Song(song.clone()),
|
||||
database_id: song.id,
|
||||
title: song.title.clone(),
|
||||
slides,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
kind: ServiceItemKind::Song(song.clone()),
|
||||
database_id: song.id,
|
||||
title: song.title.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Video> for ServiceItem {
|
||||
fn from(video: &Video) -> Self {
|
||||
video.to_slides().map_or_else(
|
||||
|_| Self {
|
||||
kind: ServiceItemKind::Video(video.clone()),
|
||||
database_id: video.id,
|
||||
title: video.title.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
|slides| Self {
|
||||
if let Ok(slides) = video.to_slides() {
|
||||
Self {
|
||||
kind: ServiceItemKind::Video(video.clone()),
|
||||
database_id: video.id,
|
||||
title: video.title.clone(),
|
||||
slides,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
kind: ServiceItemKind::Video(video.clone()),
|
||||
database_id: video.id,
|
||||
title: video.title.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Image> for ServiceItem {
|
||||
fn from(image: &Image) -> Self {
|
||||
image.to_slides().map_or_else(
|
||||
|_| Self {
|
||||
kind: ServiceItemKind::Image(image.clone()),
|
||||
database_id: image.id,
|
||||
title: image.title.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
|slides| Self {
|
||||
if let Ok(slides) = image.to_slides() {
|
||||
Self {
|
||||
kind: ServiceItemKind::Image(image.clone()),
|
||||
database_id: image.id,
|
||||
title: image.title.clone(),
|
||||
slides,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
kind: ServiceItemKind::Image(image.clone()),
|
||||
database_id: image.id,
|
||||
title: image.title.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -369,11 +368,14 @@ impl From<&Presentation> for ServiceItem {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl Service {
|
||||
fn add_item(&mut self, item: impl Into<ServiceItem>) {
|
||||
fn add_item(
|
||||
&mut self,
|
||||
item: impl Into<ServiceItem>,
|
||||
) -> Result<()> {
|
||||
let service_item: ServiceItem = item.into();
|
||||
self.items.push(service_item);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_slides(&self) -> Result<Vec<Slide>> {
|
||||
|
|
@ -389,7 +391,7 @@ impl Service {
|
|||
.collect::<Vec<Slide>>();
|
||||
let mut final_slides = vec![];
|
||||
for (index, mut slide) in slides.into_iter().enumerate() {
|
||||
slide.set_index(i32::try_from(index).into_diagnostic()?);
|
||||
slide.set_index(index as i32);
|
||||
final_slides.push(slide);
|
||||
}
|
||||
Ok(final_slides)
|
||||
|
|
@ -452,15 +454,19 @@ mod test {
|
|||
let pres = test_presentation();
|
||||
let pres_item = ServiceItem::from(&pres);
|
||||
let mut service_model = Service::default();
|
||||
service_model.add_item(&song);
|
||||
assert_eq!(
|
||||
ServiceItemKind::Song(song),
|
||||
service_model.items[0].kind
|
||||
);
|
||||
assert_eq!(
|
||||
ServiceItemKind::Presentation(pres),
|
||||
pres_item.kind
|
||||
);
|
||||
assert_eq!(service_item, service_model.items[0]);
|
||||
match service_model.add_item(&song) {
|
||||
Ok(_) => {
|
||||
assert_eq!(
|
||||
ServiceItemKind::Song(song),
|
||||
service_model.items[0].kind
|
||||
);
|
||||
assert_eq!(
|
||||
ServiceItemKind::Presentation(pres),
|
||||
pres_item.kind
|
||||
);
|
||||
assert_eq!(service_item, service_model.items[0]);
|
||||
}
|
||||
Err(e) => panic!("Problem adding item: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#![allow(clippy::similar_names, unused)]
|
||||
use cosmic::widget::image::Handle;
|
||||
use cosmic::{
|
||||
cosmic_theme::palette::rgb::Rgba, widget::image::Handle,
|
||||
};
|
||||
// use cosmic::dialog::ashpd::url::Url;
|
||||
use crisp::types::{Keyword, Symbol, Value};
|
||||
use iced_video_player::Video;
|
||||
|
|
@ -11,7 +12,7 @@ use std::{
|
|||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::ui::text_svg::{Color, Font, Shadow, Stroke, TextSvg};
|
||||
use crate::ui::text_svg::TextSvg;
|
||||
|
||||
use super::songs::Song;
|
||||
|
||||
|
|
@ -22,20 +23,20 @@ pub struct Slide {
|
|||
id: i32,
|
||||
pub(crate) background: Background,
|
||||
text: String,
|
||||
font: Option<Font>,
|
||||
font: String,
|
||||
font_size: i32,
|
||||
stroke: Option<Stroke>,
|
||||
shadow: Option<Shadow>,
|
||||
stroke_size: i32,
|
||||
stroke_color: Option<Rgba>,
|
||||
text_alignment: TextAlignment,
|
||||
text_color: Option<Color>,
|
||||
audio: Option<PathBuf>,
|
||||
video_loop: bool,
|
||||
video_start_time: f32,
|
||||
video_end_time: f32,
|
||||
pdf_index: u32,
|
||||
pub text_svg: Option<TextSvg>,
|
||||
#[serde(skip)]
|
||||
pdf_page: Option<Handle>,
|
||||
#[serde(skip)]
|
||||
pub text_svg: Option<TextSvg>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
|
@ -49,6 +50,13 @@ pub enum BackgroundKind {
|
|||
Html,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Image {
|
||||
pub source: String,
|
||||
pub fit: String,
|
||||
pub children: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
|
|
@ -136,15 +144,12 @@ impl TryFrom<PathBuf> for Background {
|
|||
type Error = ParseError;
|
||||
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
||||
let path = if path.starts_with("~") {
|
||||
let path = path
|
||||
.to_str()
|
||||
.expect("Should have a string")
|
||||
.to_string();
|
||||
let path = path.to_str().unwrap().to_string();
|
||||
let path = path.trim_start_matches("file://");
|
||||
let home = dirs::home_dir()
|
||||
.expect("We should have a home directory")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.expect("Gah")
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let path = path.replace('~', &home);
|
||||
PathBuf::from(path)
|
||||
|
|
@ -192,18 +197,16 @@ impl TryFrom<&str> for Background {
|
|||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let value = value.trim_start_matches("file://");
|
||||
if value.starts_with('~') {
|
||||
dirs::home_dir().map_or_else(
|
||||
|| Self::try_from(PathBuf::from(value)),
|
||||
|home| {
|
||||
home.to_str().map_or_else(
|
||||
|| Self::try_from(PathBuf::from(value)),
|
||||
|home| {
|
||||
let value = value.replace('~', home);
|
||||
Self::try_from(PathBuf::from(value))
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
if let Some(home) = home.to_str() {
|
||||
let value = value.replace('~', home);
|
||||
Self::try_from(PathBuf::from(value))
|
||||
} else {
|
||||
Self::try_from(PathBuf::from(value))
|
||||
}
|
||||
} else {
|
||||
Self::try_from(PathBuf::from(value))
|
||||
}
|
||||
} else if value.starts_with("./") {
|
||||
Err(ParseError::CannotCanonicalize)
|
||||
} else {
|
||||
|
|
@ -267,116 +270,68 @@ impl From<&Slide> for Value {
|
|||
}
|
||||
|
||||
impl Slide {
|
||||
#[must_use]
|
||||
pub fn set_text(mut self, text: impl AsRef<str>) -> Self {
|
||||
self.text = text.as_ref().into();
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_text_svg(mut self, text_svg: TextSvg) -> Self {
|
||||
self.text_svg = Some(text_svg);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn set_font(mut self, font: impl AsRef<str>) -> Self {
|
||||
self.font = Some(font.as_ref().into());
|
||||
self.font = font.as_ref().into();
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn set_font_size(mut self, font_size: i32) -> Self {
|
||||
self.font_size = font_size;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn set_audio(mut self, audio: Option<PathBuf>) -> Self {
|
||||
self.audio = audio;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn set_pdf_index(mut self, pdf_index: u32) -> Self {
|
||||
self.pdf_index = pdf_index;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn set_stroke(mut self, stroke: Stroke) -> Self {
|
||||
self.stroke = Some(stroke);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn set_shadow(mut self, shadow: Shadow) -> Self {
|
||||
self.shadow = Some(shadow);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn set_text_color(mut self, color: Color) -> Self {
|
||||
self.text_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn background(&self) -> &Background {
|
||||
&self.background
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn text(&self) -> String {
|
||||
self.text.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn text_alignment(&self) -> TextAlignment {
|
||||
self.text_alignment
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn font_size(&self) -> i32 {
|
||||
self.font_size
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn font(&self) -> Option<Font> {
|
||||
pub fn font(&self) -> String {
|
||||
self.font.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn video_loop(&self) -> bool {
|
||||
self.video_loop
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn audio(&self) -> Option<PathBuf> {
|
||||
self.audio.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pdf_page(&self) -> Option<Handle> {
|
||||
self.pdf_page.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn text_color(&self) -> Option<Color> {
|
||||
self.text_color.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn stroke(&self) -> Option<Stroke> {
|
||||
self.stroke.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn shadow(&self) -> Option<Shadow> {
|
||||
self.shadow.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn pdf_index(&self) -> u32 {
|
||||
self.pdf_index
|
||||
}
|
||||
|
|
@ -411,6 +366,10 @@ impl Slide {
|
|||
self.id = index;
|
||||
}
|
||||
|
||||
pub(crate) fn text_to_image(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// pub fn slides_from_item(item: &ServiceItem) -> Result<Vec<Self>> {
|
||||
// todo!()
|
||||
// }
|
||||
|
|
@ -431,8 +390,7 @@ impl From<&Value> for Slide {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
||||
fn lisp_to_slide(lisp: &Vec<Value>) -> Slide {
|
||||
const DEFAULT_BACKGROUND_LOCATION: usize = 1;
|
||||
const DEFAULT_TEXT_LOCATION: usize = 0;
|
||||
|
||||
|
|
@ -496,7 +454,6 @@ fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
fn lisp_to_font_size(lisp: &Value) -> i32 {
|
||||
match lisp {
|
||||
Value::List(list) => {
|
||||
|
|
@ -529,7 +486,6 @@ fn lisp_to_text(lisp: &Value) -> impl Into<String> {
|
|||
|
||||
// Need to return a Result here so that we can propogate
|
||||
// errors and then handle them appropriately
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
pub fn lisp_to_background(lisp: &Value) -> Background {
|
||||
match lisp {
|
||||
Value::List(list) => {
|
||||
|
|
@ -588,12 +544,9 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
|||
pub struct SlideBuilder {
|
||||
background: Option<Background>,
|
||||
text: Option<String>,
|
||||
font: Option<Font>,
|
||||
font: Option<String>,
|
||||
font_size: Option<i32>,
|
||||
audio: Option<PathBuf>,
|
||||
stroke: Option<Stroke>,
|
||||
shadow: Option<Shadow>,
|
||||
text_color: Option<Color>,
|
||||
text_alignment: Option<TextAlignment>,
|
||||
video_loop: Option<bool>,
|
||||
video_start_time: Option<f32>,
|
||||
|
|
@ -632,20 +585,12 @@ impl SlideBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) fn text_color(
|
||||
mut self,
|
||||
text_color: impl Into<Color>,
|
||||
) -> Self {
|
||||
let _ = self.text_color.insert(text_color.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn audio(mut self, audio: impl Into<PathBuf>) -> Self {
|
||||
let _ = self.audio.insert(audio.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn font(mut self, font: impl Into<Font>) -> Self {
|
||||
pub(crate) fn font(mut self, font: impl Into<String>) -> Self {
|
||||
let _ = self.font.insert(font.into());
|
||||
self
|
||||
}
|
||||
|
|
@ -655,27 +600,6 @@ impl SlideBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) fn color(mut self, color: impl Into<Color>) -> Self {
|
||||
let _ = self.text_color.insert(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn stroke(
|
||||
mut self,
|
||||
stroke: impl Into<Stroke>,
|
||||
) -> Self {
|
||||
let _ = self.stroke.insert(stroke.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn shadow(
|
||||
mut self,
|
||||
shadow: impl Into<Shadow>,
|
||||
) -> Self {
|
||||
let _ = self.shadow.insert(shadow.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn text_alignment(
|
||||
mut self,
|
||||
text_alignment: TextAlignment,
|
||||
|
|
@ -733,6 +657,9 @@ impl SlideBuilder {
|
|||
let Some(text) = self.text else {
|
||||
return Err(miette!("No text"));
|
||||
};
|
||||
let Some(font) = self.font else {
|
||||
return Err(miette!("No font"));
|
||||
};
|
||||
let Some(font_size) = self.font_size else {
|
||||
return Err(miette!("No font_size"));
|
||||
};
|
||||
|
|
@ -751,13 +678,10 @@ impl SlideBuilder {
|
|||
Ok(Slide {
|
||||
background,
|
||||
text,
|
||||
font: self.font,
|
||||
font,
|
||||
font_size,
|
||||
text_alignment,
|
||||
audio: self.audio,
|
||||
stroke: self.stroke,
|
||||
shadow: self.shadow,
|
||||
text_color: self.text_color,
|
||||
video_loop,
|
||||
video_start_time,
|
||||
video_end_time,
|
||||
|
|
@ -769,6 +693,12 @@ impl SlideBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
impl Image {
|
||||
fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
|
@ -781,7 +711,7 @@ mod test {
|
|||
text: "This is frodo".to_string(),
|
||||
background: Background::try_from("~/pics/frodo.jpg")
|
||||
.unwrap(),
|
||||
font: Some("Quicksand".to_string().into()),
|
||||
font: "Quicksand".to_string(),
|
||||
font_size: 140,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -794,11 +724,30 @@ mod test {
|
|||
"~/vids/test/camprules2024.mp4",
|
||||
)
|
||||
.unwrap(),
|
||||
font: Some("Quicksand".to_string().into()),
|
||||
font: "Quicksand".to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lisp_serialize() {
|
||||
let lisp =
|
||||
read_to_string("./test_presentation.lisp").expect("oops");
|
||||
let lisp_value = crisp::reader::read(&lisp);
|
||||
match lisp_value {
|
||||
Value::List(value) => {
|
||||
let slide = Slide::from(value[0].clone());
|
||||
let test_slide = test_slide();
|
||||
assert_eq!(slide, test_slide);
|
||||
|
||||
let second_slide = Slide::from(value[1].clone());
|
||||
let second_test_slide = test_second_slide();
|
||||
assert_eq!(second_slide, second_test_slide)
|
||||
}
|
||||
_ => panic!("this should be a lisp"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ron_deserialize() {
|
||||
let slide = read_to_string("./test_presentation.ron")
|
||||
|
|
|
|||
|
|
@ -1,136 +1,16 @@
|
|||
use itertools::Itertools;
|
||||
use miette::{IntoDiagnostic, Result, miette};
|
||||
use reqwest::header;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq)]
|
||||
pub struct OnlineSong {
|
||||
pub lyrics: String,
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub site: String,
|
||||
pub link: String,
|
||||
lyrics: String,
|
||||
title: String,
|
||||
author: String,
|
||||
site: String,
|
||||
link: String,
|
||||
}
|
||||
|
||||
pub async fn search_genius_links(
|
||||
query: impl AsRef<str> + std::fmt::Display,
|
||||
) -> Result<Vec<OnlineSong>> {
|
||||
let auth_token = env!("GENIUS_TOKEN");
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert(
|
||||
header::AUTHORIZATION,
|
||||
header::HeaderValue::from_static(auth_token),
|
||||
);
|
||||
let client = reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.into_diagnostic()?;
|
||||
let response = client
|
||||
.get(format!("https://api.genius.com/search?q={query}"))
|
||||
.send()
|
||||
.await
|
||||
.into_diagnostic()?
|
||||
.error_for_status()
|
||||
.into_diagnostic()?
|
||||
.text()
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
let json: Value =
|
||||
serde_json::from_str(&response).into_diagnostic()?;
|
||||
let hits = json
|
||||
.get("response")
|
||||
.expect("respose")
|
||||
.get("hits")
|
||||
.expect("hits")
|
||||
.as_array()
|
||||
.expect("array");
|
||||
Ok(hits
|
||||
.iter()
|
||||
.map(|hit| {
|
||||
let result = hit.get("result").expect("result");
|
||||
let title = result
|
||||
.get("full_title")
|
||||
.expect("title")
|
||||
.as_str()
|
||||
.expect("title")
|
||||
.to_string();
|
||||
let title = title.replace("\u{a0}", " ");
|
||||
let author = result
|
||||
.get("artist_names")
|
||||
.expect("artists")
|
||||
.as_str()
|
||||
.expect("artists")
|
||||
.to_string();
|
||||
let link = result
|
||||
.get("url")
|
||||
.expect("url")
|
||||
.as_str()
|
||||
.expect("url")
|
||||
.to_string();
|
||||
OnlineSong {
|
||||
lyrics: String::new(),
|
||||
title,
|
||||
author,
|
||||
site: String::from("https://genius.com"),
|
||||
link,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_genius_lyrics(
|
||||
mut song: OnlineSong,
|
||||
) -> Result<OnlineSong> {
|
||||
let html = reqwest::get(&song.link)
|
||||
.await
|
||||
.into_diagnostic()?
|
||||
.error_for_status()
|
||||
.into_diagnostic()?
|
||||
.text()
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
let document = scraper::Html::parse_document(&html);
|
||||
let Ok(lyrics_root_selector) = scraper::Selector::parse(
|
||||
r#"div[data-lyrics-container="true"]"#,
|
||||
) else {
|
||||
return Err(miette!("error in finding lyrics_root"));
|
||||
};
|
||||
|
||||
let lyrics = document
|
||||
.select(&lyrics_root_selector)
|
||||
.map(|root| {
|
||||
// dbg!(&root);
|
||||
root.inner_html()
|
||||
})
|
||||
.collect::<String>();
|
||||
let lyrics = lyrics.find("[").map_or_else(
|
||||
|| {
|
||||
lyrics.find("</div></div></div>").map_or(
|
||||
lyrics.clone(),
|
||||
|position| {
|
||||
lyrics.split_at(position + 18).1.to_string()
|
||||
},
|
||||
)
|
||||
},
|
||||
|position| lyrics.split_at(position).1.to_string(),
|
||||
);
|
||||
let lyrics = lyrics.replace("<br>", "\n");
|
||||
song.lyrics = lyrics;
|
||||
Ok(song)
|
||||
}
|
||||
|
||||
pub async fn search_lyrics_com_links(
|
||||
pub async fn search_online_song_links(
|
||||
query: impl AsRef<str> + std::fmt::Display,
|
||||
) -> Result<Vec<String>> {
|
||||
let html =
|
||||
|
|
@ -144,18 +24,16 @@ pub async fn search_lyrics_com_links(
|
|||
.into_diagnostic()?;
|
||||
|
||||
let document = scraper::Html::parse_document(&html);
|
||||
let Ok(best_matches_selector) =
|
||||
scraper::Selector::parse(".best-matches")
|
||||
else {
|
||||
return Err(miette!("error in finding matches"));
|
||||
};
|
||||
let Ok(lyric_selector) = scraper::Selector::parse("a") else {
|
||||
return Err(miette!("error in finding a links"));
|
||||
};
|
||||
let best_matches_selector =
|
||||
scraper::Selector::parse(".best-matches").unwrap();
|
||||
let lyric_selector = scraper::Selector::parse("a").unwrap();
|
||||
|
||||
Ok(document
|
||||
.select(&best_matches_selector)
|
||||
.flat_map(|best_section| best_section.select(&lyric_selector))
|
||||
.filter_map(|best_section| {
|
||||
Some(best_section.select(&lyric_selector))
|
||||
})
|
||||
.flatten()
|
||||
.map(|a| {
|
||||
a.value().attr("href").unwrap_or("").trim().to_string()
|
||||
})
|
||||
|
|
@ -169,21 +47,18 @@ pub async fn search_lyrics_com_links(
|
|||
.collect())
|
||||
}
|
||||
|
||||
// leaving this lint unfixed because I don't know if we will need this
|
||||
// id value or not in the future and I'd like to keep the code understanding
|
||||
// of what this variable might be.
|
||||
#[allow(clippy::no_effect_underscore_binding)]
|
||||
pub async fn lyrics_com_link_to_song(
|
||||
pub async fn link_to_online_song(
|
||||
links: Vec<impl AsRef<str> + std::fmt::Display>,
|
||||
) -> Result<Vec<OnlineSong>> {
|
||||
let mut songs = vec![];
|
||||
for link in links {
|
||||
let parts = link
|
||||
.as_ref()
|
||||
.to_string()
|
||||
.split('/')
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<String>>();
|
||||
let link = format!("https://www.lyrics.com/lyric/{link}");
|
||||
dbg!(&link);
|
||||
let _id = &parts[0];
|
||||
let author = &parts[1].replace('+', " ");
|
||||
let title = &parts[2].replace('+', " ");
|
||||
|
|
@ -198,18 +73,19 @@ pub async fn lyrics_com_link_to_song(
|
|||
.into_diagnostic()?;
|
||||
|
||||
let document = scraper::Html::parse_document(&html);
|
||||
let Ok(lyric_selector) =
|
||||
scraper::Selector::parse(".lyric-body")
|
||||
else {
|
||||
return Err(miette!("error in finding lyric-body",));
|
||||
};
|
||||
let lyric_selector =
|
||||
scraper::Selector::parse(".lyric-body").unwrap();
|
||||
|
||||
let lyrics = document
|
||||
.select(&lyric_selector)
|
||||
.map(|a| a.text().collect::<String>())
|
||||
.map(|a| {
|
||||
dbg!(&a);
|
||||
a.text().collect::<String>()
|
||||
})
|
||||
.dedup()
|
||||
.next();
|
||||
|
||||
dbg!(&lyrics);
|
||||
if let Some(lyrics) = lyrics {
|
||||
let song = OnlineSong {
|
||||
lyrics,
|
||||
|
|
@ -227,47 +103,11 @@ pub async fn lyrics_com_link_to_song(
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::core::songs::Song;
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_genius() -> Result<(), String> {
|
||||
let song = OnlineSong {
|
||||
lyrics: String::new(),
|
||||
title: "Death Was Arrested by North Point Worship (Ft. Seth Condrey)".to_string(),
|
||||
author: "North Point Worship (Ft. Seth Condrey)".to_string(),
|
||||
site: "https://genius.com".to_string(),
|
||||
link: "https://genius.com/North-point-worship-death-was-arrested-lyrics".to_string(),
|
||||
};
|
||||
let hits = search_genius_links("Death was arrested")
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let titles: Vec<String> =
|
||||
hits.iter().map(|song| song.title.clone()).collect();
|
||||
dbg!(titles);
|
||||
for hit in hits {
|
||||
let new_song = get_genius_lyrics(hit)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
dbg!(&new_song);
|
||||
if !new_song.lyrics.starts_with("[Verse 1]") {
|
||||
assert!(new_song.lyrics.len() > 10);
|
||||
} else {
|
||||
assert!(new_song.lyrics.contains("[Verse 2]"));
|
||||
if !new_song.lyrics.contains("[Chorus]") {
|
||||
assert!(new_song.lyrics.contains("[Chorus 1]"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_to_song() -> Result<(), String> {
|
||||
async fn test_search_to_song() {
|
||||
let song = OnlineSong {
|
||||
lyrics: "Alone in my sorrow and dead in my sin\nLost without hope with no place to begin\nYour love Made a way to let mercy come in\nWhen death was arrested and my life began\n\nAsh was redeemed only beauty remains\nMy orphan heart was given a name\nMy mourning grew quiet my feet rose to dance\nWhen death was arrested and my life began\n\nOh, Your grace so free\nWashes over me\nYou have made me new\nNow life begins with You\nIt's your endless love\nPouring down on us\nYou have made us new\nNow life begins with You\n\nReleased from my chains I'm a prisoner no more\nMy shame was a ransom He faithfully bore\nHe cancelled my debt and He called me His friend\nWhen death was arrested and my life began\n\nOh, Your grace so free\nWashes over me\nYou have made me new\nNow life begins with You\nIt's your endless love\nPouring down on us\nYou have made us new\nNow life begins with You\n\nOur savior displayed on a criminal's cross\nDarkness rejoiced as though heaven had lost\nBut then Jesus arose with our freedom in hand\nThat's when death was arrested and my life began\n\nOh, Your grace so free\nWashes over me\nYou have made me new\nNow life begins with You\nIt's your endless love\nPouring down on us\nYou have made us new\nNow life begins with You\n\nOh, we're free, free\nForever we're free\nCome join the song\nOf all the redeemed\nYes, we're free free\nForever amen\nWhen death was arrested and my life began\n\nOh, we're free, free\nForever we're free\nCome join the song\nOf all the redeemed\nYes, we're free free\nForever amen\nWhen death was arrested and my life began\n\nWhen death was arrested and my life began\nWhen death was arrested and my life began".to_string(),
|
||||
title: "Death Was Arrested".to_string(),
|
||||
|
|
@ -275,42 +115,33 @@ mod test {
|
|||
site: "https://www.lyrics.com".to_string(),
|
||||
link: "https://www.lyrics.com/lyric/35090938/North+Point+InsideOut/Death+Was+Arrested".to_string(),
|
||||
};
|
||||
let links = search_lyrics_com_links("Death was arrested")
|
||||
.await
|
||||
.map_err(|e| format!("{e}"))?;
|
||||
let songs = lyrics_com_link_to_song(links)
|
||||
.await
|
||||
.map_err(|e| format!("{e}"))?;
|
||||
if let Some(first) = songs.iter().find_or_first(|song| {
|
||||
song.author == "North Point InsideOut"
|
||||
}) {
|
||||
assert_eq!(&song, first);
|
||||
online_song_to_song(song)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn online_song_to_song(song: OnlineSong) -> Result<(), String> {
|
||||
let song = Song::from(song);
|
||||
if let Some(verse_map) = song.verse_map.as_ref() {
|
||||
if verse_map.len() < 2 {
|
||||
return Err(format!(
|
||||
"VerseMap wasn't built right likely: {:?}",
|
||||
song
|
||||
));
|
||||
let search =
|
||||
search_online_song_links("Death was arrested").await;
|
||||
match search {
|
||||
Ok(links) => {
|
||||
let songs = link_to_online_song(links).await;
|
||||
match songs {
|
||||
Ok(songs) => {
|
||||
if let Some(first) =
|
||||
songs.iter().find_or_first(|song| {
|
||||
song.author
|
||||
== "North Point InsideOut"
|
||||
})
|
||||
{
|
||||
assert_eq!(&song, first);
|
||||
}
|
||||
}
|
||||
Err(e) => assert!(false, "{}", e),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(String::from(
|
||||
"There is no VerseMap in this song",
|
||||
));
|
||||
};
|
||||
Ok(())
|
||||
Err(e) => assert!(false, "{}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_online_search() {
|
||||
let search =
|
||||
search_lyrics_com_links("Death was arrested").await;
|
||||
search_online_song_links("Death was arrested").await;
|
||||
match search {
|
||||
Ok(songs) => {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -3,11 +3,8 @@ use std::{
|
|||
};
|
||||
|
||||
use cosmic::{
|
||||
cosmic_theme::palette::Srgb,
|
||||
iced::{
|
||||
clipboard::mime::AsMimeTypes,
|
||||
font::{Style, Weight},
|
||||
},
|
||||
cosmic_theme::palette::rgb::Rgba,
|
||||
iced::clipboard::mime::AsMimeTypes,
|
||||
};
|
||||
use crisp::types::{Keyword, Symbol, Value};
|
||||
use itertools::Itertools;
|
||||
|
|
@ -19,17 +16,14 @@ use sqlx::{
|
|||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{
|
||||
Slide, SlideBuilder,
|
||||
core::{
|
||||
content::Content,
|
||||
kinds::ServiceItemKind,
|
||||
model::{LibraryKind, Model},
|
||||
service_items::ServiceTrait,
|
||||
slide::{self, Background, TextAlignment},
|
||||
song_search::OnlineSong,
|
||||
},
|
||||
ui::text_svg::{Color, Font, Stroke, shadow, stroke},
|
||||
use crate::{Slide, SlideBuilder, core::slide};
|
||||
|
||||
use super::{
|
||||
content::Content,
|
||||
kinds::ServiceItemKind,
|
||||
model::{LibraryKind, Model},
|
||||
service_items::ServiceTrait,
|
||||
slide::{Background, TextAlignment},
|
||||
};
|
||||
|
||||
#[derive(
|
||||
|
|
@ -47,14 +41,11 @@ pub struct Song {
|
|||
pub text_alignment: Option<TextAlignment>,
|
||||
pub font: Option<String>,
|
||||
pub font_size: Option<i32>,
|
||||
pub font_weight: Option<Weight>,
|
||||
pub font_style: Option<Style>,
|
||||
pub text_color: Option<Srgb>,
|
||||
pub stroke_size: Option<u16>,
|
||||
pub stroke_color: Option<Srgb>,
|
||||
pub shadow_size: Option<u16>,
|
||||
pub shadow_offset: Option<(i16, i16)>,
|
||||
pub shadow_color: Option<Srgb>,
|
||||
pub stroke_size: Option<i32>,
|
||||
pub stroke_color: Option<Rgba>,
|
||||
pub shadow_size: Option<i32>,
|
||||
pub shadow_offset: Option<(i32, i32)>,
|
||||
pub shadow_color: Option<Rgba>,
|
||||
pub verses: Option<Vec<VerseName>>,
|
||||
pub verse_map: Option<HashMap<VerseName, String>>,
|
||||
}
|
||||
|
|
@ -85,9 +76,8 @@ pub enum VerseName {
|
|||
}
|
||||
|
||||
impl VerseName {
|
||||
#[must_use]
|
||||
pub fn from_string(name: &str) -> Self {
|
||||
match name {
|
||||
pub fn from_string(name: String) -> Self {
|
||||
match name.as_str() {
|
||||
"Verse" => Self::Verse { number: 1 },
|
||||
"Pre-Chorus" => Self::PreChorus { number: 1 },
|
||||
"Chorus" => Self::Chorus { number: 1 },
|
||||
|
|
@ -97,12 +87,11 @@ impl VerseName {
|
|||
"Outro" => Self::Outro { number: 1 },
|
||||
"Instrumental" => Self::Instrumental { number: 1 },
|
||||
"Other" => Self::Other { number: 1 },
|
||||
// Blank is included in wildcard
|
||||
"Blank" => Self::Blank,
|
||||
_ => Self::Blank,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn all_names() -> Vec<String> {
|
||||
vec![
|
||||
"Verse".into(),
|
||||
|
|
@ -118,7 +107,6 @@ impl VerseName {
|
|||
]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn next(&self) -> Self {
|
||||
match self {
|
||||
Self::Verse { number } => {
|
||||
|
|
@ -261,9 +249,7 @@ impl Content for Song {
|
|||
}
|
||||
|
||||
fn subtext(&self) -> String {
|
||||
self.author
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Author missing".into())
|
||||
self.author.clone().unwrap_or("Author missing".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,78 +267,31 @@ impl ServiceTrait for Song {
|
|||
let lyrics: Vec<String> = self
|
||||
.verses
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
miette!("There are no verses assigned yet.")
|
||||
})?
|
||||
.ok_or(miette!("There are no verses assigned yet."))?
|
||||
.iter()
|
||||
.filter_map(|verse| self.get_lyric(verse))
|
||||
.flat_map(|lyric| {
|
||||
.map(|lyric| {
|
||||
lyric
|
||||
.split("\n\n")
|
||||
.map(std::string::ToString::to_string)
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
debug!(?lyrics);
|
||||
let slides: Vec<Slide> = lyrics
|
||||
.iter()
|
||||
.filter_map(|l| {
|
||||
let font = Font::default()
|
||||
.name(
|
||||
self.font
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Calibri".into()),
|
||||
)
|
||||
.style(self.font_style.unwrap_or_default())
|
||||
.weight(self.font_weight.unwrap_or_default())
|
||||
.size(
|
||||
u8::try_from(self.font_size.unwrap_or(100))
|
||||
.unwrap_or(100),
|
||||
);
|
||||
let stroke_size =
|
||||
self.stroke_size.unwrap_or_default();
|
||||
let stroke: Stroke = stroke(
|
||||
stroke_size,
|
||||
self.stroke_color
|
||||
.map(Color::from)
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let shadow_size =
|
||||
self.shadow_size.unwrap_or_default();
|
||||
let shadow = shadow(
|
||||
self.shadow_offset.unwrap_or_default().0,
|
||||
self.shadow_offset.unwrap_or_default().1,
|
||||
shadow_size,
|
||||
self.shadow_color
|
||||
.map(Color::from)
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let builder = SlideBuilder::new();
|
||||
let builder = if shadow_size > 0 {
|
||||
builder.shadow(shadow)
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
let builder = if stroke_size > 0 {
|
||||
builder.stroke(stroke)
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
builder
|
||||
SlideBuilder::new()
|
||||
.background(
|
||||
self.background.clone().unwrap_or_default(),
|
||||
)
|
||||
.font(font)
|
||||
.font(self.font.clone().unwrap_or_default())
|
||||
.font_size(self.font_size.unwrap_or_default())
|
||||
.text_alignment(
|
||||
self.text_alignment.unwrap_or_default(),
|
||||
)
|
||||
.text_color(
|
||||
self.text_color.unwrap_or_else(|| {
|
||||
Srgb::new(1.0, 1.0, 1.0)
|
||||
}),
|
||||
)
|
||||
.audio(self.audio.clone().unwrap_or_default())
|
||||
.video_loop(true)
|
||||
.video_start_time(0.0)
|
||||
|
|
@ -371,10 +310,28 @@ impl ServiceTrait for Song {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
const VERSE_KEYWORDS: [&str; 24] = [
|
||||
"Verse 1", "Verse 2", "Verse 3", "Verse 4", "Verse 5", "Verse 6",
|
||||
"Verse 7", "Verse 8", "Chorus 1", "Chorus 2", "Chorus 3",
|
||||
"Chorus 4", "Bridge 1", "Bridge 2", "Bridge 3", "Bridge 4",
|
||||
"Intro 1", "Intro 2", "Ending 1", "Ending 2", "Other 1",
|
||||
"Other 2", "Other 3", "Other 4",
|
||||
];
|
||||
|
||||
impl FromRow<'_, SqliteRow> for Song {
|
||||
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
||||
let lyrics: &str = row.try_get("lyrics")?;
|
||||
let lyrics: &str = row.try_get(8)?;
|
||||
// let Some((mut verses, mut verse_map)) =
|
||||
// lyrics_to_verse(lyrics.clone()).ok()
|
||||
// else {
|
||||
// return Err(sqlx::Error::ColumnDecode {
|
||||
// index: "8".into(),
|
||||
// source: miette!(
|
||||
// "Couldn't decode the song into verses"
|
||||
// )
|
||||
// .into(),
|
||||
// });
|
||||
// };
|
||||
|
||||
let Ok(verse_map) = ron::de::from_str::<
|
||||
Option<HashMap<VerseName, String>>,
|
||||
|
|
@ -387,7 +344,7 @@ impl FromRow<'_, SqliteRow> for Song {
|
|||
.into(),
|
||||
});
|
||||
};
|
||||
let verse_order: &str = row.try_get("verse_order")?;
|
||||
let verse_order: &str = row.try_get(0)?;
|
||||
let Ok(verses) =
|
||||
ron::de::from_str::<Option<Vec<VerseName>>>(verse_order)
|
||||
else {
|
||||
|
|
@ -407,69 +364,25 @@ impl FromRow<'_, SqliteRow> for Song {
|
|||
.collect()
|
||||
};
|
||||
|
||||
let stroke_size = match row.try_get("stroke_size") {
|
||||
Ok(size) => Some(size),
|
||||
Err(e) => {
|
||||
error!(?e);
|
||||
None
|
||||
}
|
||||
};
|
||||
let stroke_color = row
|
||||
.try_get("stroke_color")
|
||||
.ok()
|
||||
.and_then(|color: String| {
|
||||
ron::de::from_str::<Option<Srgb>>(&color).ok()
|
||||
})
|
||||
.flatten();
|
||||
let shadow_size = row.try_get("shadow_size").ok();
|
||||
let shadow_color = row
|
||||
.try_get("shadow_color")
|
||||
.ok()
|
||||
.and_then(|color: String| {
|
||||
ron::de::from_str::<Option<Srgb>>(&color).ok()
|
||||
})
|
||||
.flatten();
|
||||
let shadow_offset = match (
|
||||
row.try_get("shadow_offset_x").ok(),
|
||||
row.try_get("shadow_offset_y").ok(),
|
||||
) {
|
||||
(Some(x), Some(y)) => Some((x, y)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let style_string: String = row.try_get("style")?;
|
||||
let font_style =
|
||||
ron::de::from_str::<Option<Style>>(&style_string)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let weight_string: String = row.try_get("weight")?;
|
||||
let font_weight =
|
||||
ron::de::from_str::<Option<Weight>>(&weight_string)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
Ok(Self {
|
||||
id: row.try_get("id")?,
|
||||
title: row.try_get("title")?,
|
||||
id: row.try_get(12)?,
|
||||
title: row.try_get(5)?,
|
||||
lyrics: Some(lyrics.to_string()),
|
||||
author: row.try_get("author")?,
|
||||
ccli: row.try_get("ccli")?,
|
||||
author: row.try_get(10)?,
|
||||
ccli: row.try_get(9)?,
|
||||
audio: Some(PathBuf::from({
|
||||
let string: String = row.try_get("audio")?;
|
||||
let string: String = row.try_get(11)?;
|
||||
string
|
||||
})),
|
||||
verse_order: Some(verse_order),
|
||||
background: {
|
||||
let string: String = row.try_get("background")?;
|
||||
let string: String = row.try_get(7)?;
|
||||
Background::try_from(string).ok()
|
||||
},
|
||||
text_alignment: Some({
|
||||
let horizontal_alignment: String =
|
||||
row.try_get("horizontal_text_alignment")?;
|
||||
let vertical_alignment: String =
|
||||
row.try_get("vertical_text_alignment")?;
|
||||
// debug!(horizontal_alignment, vertical_alignment);
|
||||
let horizontal_alignment: String = row.try_get(4)?;
|
||||
let vertical_alignment: String = row.try_get(3)?;
|
||||
debug!(horizontal_alignment, vertical_alignment);
|
||||
match (
|
||||
horizontal_alignment.to_lowercase().as_str(),
|
||||
vertical_alignment.to_lowercase().as_str(),
|
||||
|
|
@ -490,15 +403,9 @@ impl FromRow<'_, SqliteRow> for Song {
|
|||
_ => TextAlignment::MiddleCenter,
|
||||
}
|
||||
}),
|
||||
font: row.try_get("font")?,
|
||||
font_size: row.try_get("font_size")?,
|
||||
font_style,
|
||||
font_weight,
|
||||
stroke_size,
|
||||
stroke_color,
|
||||
shadow_size,
|
||||
shadow_color,
|
||||
shadow_offset,
|
||||
font: row.try_get(6)?,
|
||||
font_size: row.try_get(1)?,
|
||||
stroke_size: None,
|
||||
verses,
|
||||
verse_map,
|
||||
..Default::default()
|
||||
|
|
@ -506,27 +413,6 @@ impl FromRow<'_, SqliteRow> for Song {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<OnlineSong> for Song {
|
||||
fn from(value: OnlineSong) -> Self {
|
||||
let mut song = Self::default();
|
||||
song.verse_map = Some(HashMap::new());
|
||||
for line in value.lyrics.lines() {
|
||||
let next_verse = song.get_next_verse_name();
|
||||
if let Some(verse_map) = song.verse_map.as_mut() {
|
||||
verse_map
|
||||
.entry(next_verse)
|
||||
.or_insert_with(|| line.to_string());
|
||||
}
|
||||
if let Some(verses) = song.verses.as_mut() {
|
||||
verses.push(next_verse);
|
||||
} else {
|
||||
song.verses = Some(vec![next_verse]);
|
||||
}
|
||||
}
|
||||
song
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value> for Song {
|
||||
fn from(value: Value) -> Self {
|
||||
match value {
|
||||
|
|
@ -536,9 +422,72 @@ impl From<Value> for Song {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn lyrics_to_verse(
|
||||
lyrics: String,
|
||||
) -> Result<(Vec<VerseName>, HashMap<VerseName, String>)> {
|
||||
let mut verse_list = Vec::new();
|
||||
if lyrics.is_empty() {
|
||||
return Err(miette!("There is no lyrics here"));
|
||||
}
|
||||
|
||||
let raw_lyrics = lyrics.as_str();
|
||||
|
||||
let mut lyric_map = HashMap::new();
|
||||
let mut verse_title = String::new();
|
||||
let mut lyric = String::new();
|
||||
for (i, line) in raw_lyrics.split('\n').enumerate() {
|
||||
if VERSE_KEYWORDS.contains(&line) {
|
||||
if i != 0 {
|
||||
lyric_map.insert(verse_title, lyric);
|
||||
lyric = String::new();
|
||||
verse_title = line.to_string();
|
||||
} else {
|
||||
verse_title = line.to_string();
|
||||
}
|
||||
} else {
|
||||
lyric.push_str(line);
|
||||
lyric.push('\n');
|
||||
}
|
||||
}
|
||||
lyric_map.insert(verse_title, lyric);
|
||||
let mut verse_map = HashMap::new();
|
||||
for (verse_name, lyric) in lyric_map {
|
||||
let mut verse_elements = verse_name.split_whitespace();
|
||||
let verse_keyword = verse_elements.next();
|
||||
let Some(keyword) = verse_keyword else {
|
||||
return Err(miette!(
|
||||
"Can't parse a proper verse keyword from lyrics"
|
||||
));
|
||||
};
|
||||
let verse_index = verse_elements.next();
|
||||
let Some(index) = verse_index else {
|
||||
return Err(miette!(
|
||||
"Can't parse a proper verse index from lyrics"
|
||||
));
|
||||
};
|
||||
let index = index.parse::<usize>().into_diagnostic()?;
|
||||
let verse = match keyword {
|
||||
"Verse" => VerseName::Verse { number: index },
|
||||
"Pre-Chorus" => VerseName::PreChorus { number: index },
|
||||
"Chorus" => VerseName::Chorus { number: index },
|
||||
"Post-Chorus" => VerseName::PostChorus { number: index },
|
||||
"Bridge" => VerseName::Bridge { number: index },
|
||||
"Intro" => VerseName::Intro { number: index },
|
||||
"Outro" => VerseName::Outro { number: index },
|
||||
"Instrumental" => {
|
||||
VerseName::Instrumental { number: index }
|
||||
}
|
||||
"Other" => VerseName::Other { number: index },
|
||||
_ => VerseName::Other { number: 99 },
|
||||
};
|
||||
verse_list.push(verse);
|
||||
let lyric = lyric.trim().to_string();
|
||||
verse_map.insert(verse, lyric);
|
||||
}
|
||||
|
||||
Ok((verse_list, verse_map))
|
||||
}
|
||||
|
||||
pub fn lisp_to_song(list: Vec<Value>) -> Song {
|
||||
const DEFAULT_SONG_ID: i32 = 0;
|
||||
// const DEFAULT_SONG_LOCATION: usize = 0;
|
||||
|
|
@ -617,8 +566,7 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
|
|||
.position(|v| v == &Value::Keyword(Keyword::from("title")))
|
||||
{
|
||||
let pos = key_pos + 1;
|
||||
list.get(pos)
|
||||
.map_or_else(|| String::from("song"), String::from)
|
||||
list.get(pos).map_or(String::from("song"), String::from)
|
||||
} else {
|
||||
String::from("song")
|
||||
};
|
||||
|
|
@ -661,7 +609,10 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
|
|||
|| text.contains("i1")
|
||||
}
|
||||
_ => false,
|
||||
} && matches!(&inner[1], Value::String(_)))
|
||||
} && match &inner[1] {
|
||||
Value::String(_) => true,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
|
|
@ -683,7 +634,7 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
|
|||
|
||||
let verse_title = match lyric_verse.as_str() {
|
||||
"i1" => r"\n\nIntro 1\n",
|
||||
"i2" => r"\n\nIntro 2\n",
|
||||
"i2" => r"\n\nIntro 1\n",
|
||||
"v1" => r"\n\nVerse 1\n",
|
||||
"v2" => r"\n\nVerse 2\n",
|
||||
"v3" => r"\n\nVerse 3\n",
|
||||
|
|
@ -734,7 +685,7 @@ pub async fn get_song_from_db(
|
|||
index: i32,
|
||||
db: &mut SqliteConnection,
|
||||
) -> Result<Song> {
|
||||
let row = query("SELECT verse_order, font_size, background_type, horizontal_text_alignment, vertical_text_alignment, title, font, background, lyrics, ccli, author, audio, stroke_size, stroke_color, shadow_color, shadow_size, shadow_offset_x, shadow_offset_y, style, weight, id from songs where id = $1").bind(index).fetch_one(db).await.into_diagnostic()?;
|
||||
let row = query(r#"SELECT verse_order as "verse_order!", font_size as "font_size!: i32", background_type as "background_type!", horizontal_text_alignment as "horizontal_text_alignment!", vertical_text_alignment as "vertical_text_alignment!", title as "title!", font as "font!", background as "background!", lyrics as "lyrics!", ccli as "ccli!", author as "author!", audio as "audio!", id as "id: i32" from songs where id = $1"#).bind(index).fetch_one(db).await.into_diagnostic()?;
|
||||
Song::from_row(&row).into_diagnostic()
|
||||
}
|
||||
|
||||
|
|
@ -751,15 +702,22 @@ impl Model<Song> {
|
|||
|
||||
pub async fn load_from_db(&mut self, db: &mut SqlitePool) {
|
||||
// static DATABASE_URL: &str = "sqlite:///home/chris/.local/share/lumina/library-db.sqlite3";
|
||||
let db1 = db.acquire().await.expect("Database not found");
|
||||
let result = query("SELECT verse_order, font_size, background_type, horizontal_text_alignment, vertical_text_alignment, title, font, background, lyrics, ccli, author, audio, stroke_size, shadow_size, stroke_color, shadow_color, shadow_offset_x, shadow_offset_y, style, weight, id from songs").fetch_all(&mut db1.detach()).await;
|
||||
let db1 = db.acquire().await.unwrap();
|
||||
let result = query(r#"SELECT verse_order as "verse_order!", font_size as "font_size!: i32", background_type as "background_type!", horizontal_text_alignment as "horizontal_text_alignment!", vertical_text_alignment as "vertical_text_alignment!", title as "title!", font as "font!", background as "background!", lyrics as "lyrics!", ccli as "ccli!", author as "author!", audio as "audio!", id as "id: i32" from songs"#).fetch_all(&mut db1.detach()).await;
|
||||
match result {
|
||||
Ok(s) => {
|
||||
for song in s {
|
||||
// let db2 = db.acquire().await.unwrap();
|
||||
let db2 = db.acquire().await.unwrap();
|
||||
match Song::from_row(&song) {
|
||||
Ok(song) => {
|
||||
let _ = self.add_item(song);
|
||||
match update_song_in_db(song.clone(), db2)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let _ = self.add_item(song);
|
||||
}
|
||||
Err(e) => error!(?e),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
|
|
@ -795,14 +753,16 @@ pub async fn add_song_to_db(
|
|||
let mut song = Song::default();
|
||||
|
||||
let verse_order = {
|
||||
song.verse_order.clone().map_or_else(String::new, |vo| {
|
||||
if let Some(vo) = song.verse_order.clone() {
|
||||
vo.into_iter()
|
||||
.map(|mut s| {
|
||||
s.push(' ');
|
||||
s
|
||||
})
|
||||
.collect::<String>()
|
||||
})
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
|
||||
let audio = song
|
||||
|
|
@ -830,9 +790,7 @@ pub async fn add_song_to_db(
|
|||
.execute(&mut db)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
song.id = i32::try_from(res.last_insert_rowid()).expect(
|
||||
"Fairly confident that this number won't get that high",
|
||||
);
|
||||
song.id = res.last_insert_rowid() as i32;
|
||||
Ok(song)
|
||||
}
|
||||
|
||||
|
|
@ -857,55 +815,30 @@ pub async fn update_song_in_db(
|
|||
let lyrics = item.verse_map.map(|map| {
|
||||
map.iter()
|
||||
.map(|(name, lyric)| {
|
||||
let lyric = lyric.trim_end_matches('\n').to_string();
|
||||
let lyric = lyric.trim_end_matches("\n").to_string();
|
||||
(name.to_owned(), lyric)
|
||||
})
|
||||
.collect::<HashMap<VerseName, String>>()
|
||||
});
|
||||
let lyrics = ron::ser::to_string(&lyrics).into_diagnostic()?;
|
||||
|
||||
let (vertical_alignment, horizontal_alignment) =
|
||||
item.text_alignment.map_or_else(
|
||||
|| ("center", "center"),
|
||||
|ta| match ta {
|
||||
TextAlignment::TopLeft => ("top", "left"),
|
||||
TextAlignment::TopCenter => ("top", "center"),
|
||||
TextAlignment::TopRight => ("top", "right"),
|
||||
TextAlignment::MiddleLeft => ("center", "left"),
|
||||
TextAlignment::MiddleCenter => ("center", "center"),
|
||||
TextAlignment::MiddleRight => ("center", "right"),
|
||||
TextAlignment::BottomLeft => ("bottom", "left"),
|
||||
TextAlignment::BottomCenter => ("bottom", "center"),
|
||||
TextAlignment::BottomRight => ("bottom", "right"),
|
||||
},
|
||||
);
|
||||
let (vertical_alignment, horizontal_alignment) = item
|
||||
.text_alignment
|
||||
.map(|ta| match ta {
|
||||
TextAlignment::TopLeft => ("top", "left"),
|
||||
TextAlignment::TopCenter => ("top", "center"),
|
||||
TextAlignment::TopRight => ("top", "right"),
|
||||
TextAlignment::MiddleLeft => ("center", "left"),
|
||||
TextAlignment::MiddleCenter => ("center", "center"),
|
||||
TextAlignment::MiddleRight => ("center", "right"),
|
||||
TextAlignment::BottomLeft => ("bottom", "left"),
|
||||
TextAlignment::BottomCenter => ("bottom", "center"),
|
||||
TextAlignment::BottomRight => ("bottom", "right"),
|
||||
})
|
||||
.unwrap_or_else(|| ("center", "center"));
|
||||
|
||||
let stroke_size = item.stroke_size.unwrap_or_default();
|
||||
let shadow_size = item.shadow_size.unwrap_or_default();
|
||||
let (shadow_offset_x, shadow_offset_y) =
|
||||
item.shadow_offset.unwrap_or_default();
|
||||
|
||||
let stroke_color =
|
||||
ron::ser::to_string(&item.stroke_color).into_diagnostic()?;
|
||||
let shadow_color =
|
||||
ron::ser::to_string(&item.shadow_color).into_diagnostic()?;
|
||||
|
||||
let style =
|
||||
ron::ser::to_string(&item.font_style).into_diagnostic()?;
|
||||
let weight =
|
||||
ron::ser::to_string(&item.font_weight).into_diagnostic()?;
|
||||
|
||||
// debug!(
|
||||
// ?stroke_size,
|
||||
// ?stroke_color,
|
||||
// ?shadow_size,
|
||||
// ?shadow_color,
|
||||
// ?shadow_offset_x,
|
||||
// ?shadow_offset_y
|
||||
// );
|
||||
|
||||
let result = query!(
|
||||
r#"UPDATE songs SET title = $2, lyrics = $3, author = $4, ccli = $5, verse_order = $6, audio = $7, font = $8, font_size = $9, background = $10, horizontal_text_alignment = $11, vertical_text_alignment = $12, stroke_color = $13, shadow_color = $14, stroke_size = $15, shadow_size = $16, shadow_offset_x = $17, shadow_offset_y = $18, style = $19, weight = $20 WHERE id = $1"#,
|
||||
query!(
|
||||
r#"UPDATE songs SET title = $2, lyrics = $3, author = $4, ccli = $5, verse_order = $6, audio = $7, font = $8, font_size = $9, background = $10, horizontal_text_alignment = $11, vertical_text_alignment = $12 WHERE id = $1"#,
|
||||
item.id,
|
||||
item.title,
|
||||
lyrics,
|
||||
|
|
@ -916,34 +849,25 @@ pub async fn update_song_in_db(
|
|||
item.font,
|
||||
item.font_size,
|
||||
background,
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
stroke_color,
|
||||
shadow_color,
|
||||
stroke_size,
|
||||
shadow_size,
|
||||
shadow_offset_x,
|
||||
shadow_offset_y,
|
||||
style,
|
||||
weight
|
||||
horizontal_alignment
|
||||
)
|
||||
.execute(&mut db.detach())
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
debug!(rows_affected = ?result.rows_affected());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Song {
|
||||
#[must_use]
|
||||
pub fn get_lyric(&self, verse: &VerseName) -> Option<String> {
|
||||
self.verse_map.as_ref().and_then(|verse_map| {
|
||||
let lyric = self.verse_map.as_ref().and_then(|verse_map| {
|
||||
verse_map.get(verse).cloned().map(|lyric| {
|
||||
lyric.trim().trim_end_matches('\n').to_string()
|
||||
lyric.trim().trim_end_matches("\n").to_string()
|
||||
})
|
||||
})
|
||||
});
|
||||
lyric
|
||||
}
|
||||
|
||||
pub fn set_lyrics<T: Into<String>>(
|
||||
|
|
@ -957,7 +881,7 @@ impl Song {
|
|||
verse_map
|
||||
.entry(*verse)
|
||||
.and_modify(|old_lyrics| {
|
||||
old_lyrics.clone_from(&lyric_copy);
|
||||
*old_lyrics = lyric_copy.clone()
|
||||
})
|
||||
.or_insert(lyric_copy);
|
||||
// debug!(?verse_map, "should be updated");
|
||||
|
|
@ -971,20 +895,91 @@ impl Song {
|
|||
}
|
||||
|
||||
pub fn get_lyrics(&self) -> Result<Vec<String>> {
|
||||
// ---------------------------------
|
||||
// new implementation
|
||||
// ---------------------------------
|
||||
|
||||
if let Some(verses) = self.verses.as_ref() {
|
||||
let mut lyrics = vec![];
|
||||
for verse in verses {
|
||||
if verse == &VerseName::Blank {
|
||||
lyrics.push(String::new());
|
||||
lyrics.push("".into());
|
||||
continue;
|
||||
}
|
||||
if let Some(lyric) = self.get_lyric(verse) {
|
||||
lyrics.push(lyric);
|
||||
lyrics.push(lyric)
|
||||
}
|
||||
}
|
||||
return Ok(lyrics);
|
||||
} else {
|
||||
return Err(miette!("No verses in this song yet"));
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// old implementation
|
||||
// ---------------------------------
|
||||
|
||||
let mut lyric_list = Vec::new();
|
||||
if self.lyrics.is_none() {
|
||||
return Err(miette!("There is no lyrics here"));
|
||||
} else if self.verse_order.is_none() {
|
||||
return Err(miette!("There is no verse_order here"));
|
||||
} else if self
|
||||
.verse_order
|
||||
.clone()
|
||||
.is_some_and(|v| v.is_empty())
|
||||
{
|
||||
return Err(miette!("There is no verse_order here"));
|
||||
}
|
||||
if let Some(raw_lyrics) = self.lyrics.clone() {
|
||||
let raw_lyrics = raw_lyrics.as_str();
|
||||
let verse_order = self.verses.clone();
|
||||
|
||||
let mut lyric_map = HashMap::new();
|
||||
let mut verse_title = String::new();
|
||||
let mut lyric = String::new();
|
||||
for (i, line) in raw_lyrics.split('\n').enumerate() {
|
||||
if VERSE_KEYWORDS.contains(&line) {
|
||||
if i != 0 {
|
||||
lyric_map.insert(verse_title, lyric);
|
||||
lyric = String::new();
|
||||
verse_title = line.to_string();
|
||||
} else {
|
||||
verse_title = line.to_string();
|
||||
}
|
||||
} else {
|
||||
lyric.push_str(line);
|
||||
lyric.push('\n');
|
||||
}
|
||||
}
|
||||
lyric_map.insert(verse_title, lyric);
|
||||
|
||||
for verse in verse_order.unwrap_or_default() {
|
||||
let verse_name = &verse.get_name();
|
||||
if let Some(lyric) = lyric_map.get(verse_name) {
|
||||
if lyric.contains("\n\n") {
|
||||
let split_lyrics: Vec<&str> =
|
||||
lyric.split("\n\n").collect();
|
||||
for lyric in split_lyrics {
|
||||
if lyric.is_empty() {
|
||||
continue;
|
||||
}
|
||||
lyric_list.push(lyric.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
lyric_list.push(lyric.clone());
|
||||
} else {
|
||||
// error!("NOT WORKING!");
|
||||
}
|
||||
}
|
||||
// for lyric in lyric_list.iter() {
|
||||
// debug!(lyric = ?lyric)
|
||||
// }
|
||||
Ok(lyric_list)
|
||||
} else {
|
||||
Err(miette!("There are no lyrics"))
|
||||
}
|
||||
Err(miette!("No verses in this song yet"))
|
||||
}
|
||||
|
||||
pub fn update_verse_name(
|
||||
|
|
@ -995,11 +990,7 @@ impl Song {
|
|||
if let Some(verse_map) = self.verse_map.as_mut()
|
||||
&& let Some(lyric) = verse_map.remove(old_verse)
|
||||
{
|
||||
if verse == VerseName::Blank {
|
||||
verse_map.insert(verse, String::new());
|
||||
} else {
|
||||
verse_map.insert(verse, lyric);
|
||||
}
|
||||
verse_map.insert(verse, lyric);
|
||||
}
|
||||
let Some(verses) = self.verses.clone() else {
|
||||
return;
|
||||
|
|
@ -1009,7 +1000,7 @@ impl Song {
|
|||
.filter(|verse| verse != old_verse)
|
||||
.collect();
|
||||
new_verses.push(verse);
|
||||
self.verses = Some(new_verses);
|
||||
self.verses = Some(new_verses)
|
||||
}
|
||||
|
||||
// TODO update_verse needs to also change the lyrics for the song such that
|
||||
|
|
@ -1086,31 +1077,41 @@ impl Song {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_next_verse_name(&self) -> VerseName {
|
||||
if let Some(verse_names) = &self.verses {
|
||||
let verses = verse_names
|
||||
let verses: Vec<&VerseName> = verse_names
|
||||
.iter()
|
||||
.filter(|verse| {
|
||||
matches!(verse, VerseName::Verse { .. })
|
||||
.filter(|verse| match verse {
|
||||
VerseName::Verse { .. } => true,
|
||||
_ => false,
|
||||
})
|
||||
.sorted();
|
||||
let mut choruses = verse_names.iter().filter(|verse| {
|
||||
matches!(verse, VerseName::Chorus { .. })
|
||||
});
|
||||
let mut bridges = verse_names.iter().filter(|verse| {
|
||||
matches!(verse, VerseName::Bridge { .. })
|
||||
});
|
||||
if verses.len() == 0 {
|
||||
.sorted()
|
||||
.collect();
|
||||
let choruses: Vec<&VerseName> = verse_names
|
||||
.iter()
|
||||
.filter(|verse| match verse {
|
||||
VerseName::Chorus { .. } => true,
|
||||
_ => false,
|
||||
})
|
||||
.collect();
|
||||
let bridges: Vec<&VerseName> = verse_names
|
||||
.iter()
|
||||
.filter(|verse| match verse {
|
||||
VerseName::Bridge { .. } => true,
|
||||
_ => false,
|
||||
})
|
||||
.collect();
|
||||
if verses.is_empty() {
|
||||
VerseName::Verse { number: 1 }
|
||||
} else if choruses.next().is_none() {
|
||||
} else if choruses.is_empty() {
|
||||
VerseName::Chorus { number: 1 }
|
||||
} else if verses.len() == 1 {
|
||||
let verse_number =
|
||||
if let Some(VerseName::Verse { number }) =
|
||||
verses.last()
|
||||
{
|
||||
*number
|
||||
if let Some(last_verse) = verses.iter().last() {
|
||||
match last_verse {
|
||||
VerseName::Verse { number } => *number,
|
||||
_ => 0,
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
|
@ -1118,10 +1119,10 @@ impl Song {
|
|||
return VerseName::Verse { number: 1 };
|
||||
}
|
||||
VerseName::Verse { number: 2 }
|
||||
} else if bridges.next().is_none() {
|
||||
} else if bridges.is_empty() {
|
||||
VerseName::Bridge { number: 1 }
|
||||
} else {
|
||||
if let Some(last_verse) = verses.last()
|
||||
if let Some(last_verse) = verses.iter().last()
|
||||
&& let VerseName::Verse { number } = last_verse
|
||||
{
|
||||
return VerseName::Verse { number: number + 1 };
|
||||
|
|
@ -1144,20 +1145,22 @@ impl Song {
|
|||
verses.push(verse);
|
||||
} else {
|
||||
self.verses = Some(vec![verse]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn verse_name_from_str(
|
||||
&self,
|
||||
verse_name: &str, // chorus 2
|
||||
verse_name: String, // chorus 2
|
||||
old_verse_name: VerseName, // v4
|
||||
) -> VerseName {
|
||||
if old_verse_name.get_name() == verse_name {
|
||||
return old_verse_name;
|
||||
}
|
||||
self.verse_map.clone().map(|verse_map| {
|
||||
};
|
||||
if let Some(verses) =
|
||||
self.verse_map.clone().map(|verse_map| {
|
||||
verse_map.into_keys().collect::<Vec<VerseName>>()
|
||||
}).map_or_else(|| VerseName::from_string(verse_name), |verses| {
|
||||
})
|
||||
{
|
||||
verses
|
||||
.into_iter()
|
||||
.filter(|verse| {
|
||||
|
|
@ -1165,8 +1168,8 @@ impl Song {
|
|||
.get_name()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.expect("Shouldn't fail, the get_name() fn won't return a string that is blank or all whitespace")
|
||||
== verse_name
|
||||
.unwrap()
|
||||
== &verse_name
|
||||
})
|
||||
.sorted()
|
||||
.last()
|
||||
|
|
@ -1174,7 +1177,9 @@ impl Song {
|
|||
|| VerseName::from_string(verse_name),
|
||||
|verse_name| verse_name.next(),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
VerseName::from_string(verse_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn delete_verse(&mut self, verse: VerseName) {
|
||||
|
|
@ -1189,14 +1194,10 @@ impl Song {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ui::text_svg::text_svg_generator_with_cache;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use resvg::usvg::fontdb;
|
||||
|
||||
#[test]
|
||||
pub fn test_song_lyrics() {
|
||||
|
|
@ -1336,7 +1337,11 @@ You saved my soul"
|
|||
}
|
||||
|
||||
async fn add_db() -> Result<SqlitePool> {
|
||||
let db_url = String::from("sqlite://./test.db");
|
||||
let mut data = dirs::data_local_dir().unwrap();
|
||||
data.push("lumina");
|
||||
data.push("library-db.sqlite3");
|
||||
let mut db_url = String::from("sqlite://");
|
||||
db_url.push_str(data.to_str().unwrap());
|
||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
||||
}
|
||||
|
||||
|
|
@ -1374,7 +1379,7 @@ You saved my soul"
|
|||
#[tokio::test]
|
||||
async fn test_song_from_db() {
|
||||
let song = test_song();
|
||||
let mut db = add_db().await.unwrap().acquire().await.unwrap();
|
||||
let mut db = crate::core::model::get_db().await;
|
||||
let result = get_song_from_db(7, &mut db).await;
|
||||
match result {
|
||||
Ok(db_song) => {
|
||||
|
|
@ -1407,7 +1412,7 @@ You saved my soul"
|
|||
}
|
||||
}
|
||||
|
||||
pub fn test_song() -> Song {
|
||||
fn test_song() -> Song {
|
||||
let lyrics = "Some({Verse(number:4):\"Our Savior displayed\\nOn a criminal\\'s cross\\n\\nDarkness rejoiced as though\\nHeaven had lost\\n\\nBut then Jesus arose\\nWith our freedom in hand\\n\\nThat\\'s when death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Intro(number:1):\"Death Was Arrested\\nNorth Point Worship\",Verse(number:3):\"Released from my chains,\\nI\\'m a prisoner no more\\n\\nMy shame was a ransom\\nHe faithfully bore\\n\\nHe cancelled my debt and\\nHe called me His friend\\n\\nWhen death was arrested\\nAnd my life began\",Bridge(number:1):\"Oh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\\n\\nOh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\",Other(number:99):\"When death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Verse(number:2):\"Ash was redeemed\\nOnly beauty remains\\n\\nMy orphan heart\\nWas given a name\\n\\nMy mourning grew quiet,\\nMy feet rose to dance\\n\\nWhen death was arrested\\nAnd my life began\",Verse(number:1):\"Alone in my sorrow\\nAnd dead in my sin\\n\\nLost without hope\\nWith no place to begin\\n\\nYour love made a way\\nTo let mercy come in\\n\\nWhen death was arrested\\nAnd my life began\",Chorus(number:1):\"Oh, Your grace so free,\\nWashes over me\\n\\nYou have made me new,\\nNow life begins with You\\n\\nIt\\'s Your endless love,\\nPouring down on us\\n\\nYou have made us new,\\nNow life begins with You\"})".to_string();
|
||||
let verse_map: Option<HashMap<VerseName, String>> =
|
||||
ron::from_str(&lyrics).unwrap();
|
||||
|
|
@ -1433,6 +1438,15 @@ You saved my soul"
|
|||
}
|
||||
}
|
||||
|
||||
fn test_lisp_song() -> Value {
|
||||
let lisp = read_to_string("./test_song.lisp").expect("oops");
|
||||
let lisp_value = crisp::reader::read(&lisp);
|
||||
match lisp_value {
|
||||
Value::List(v) => v.first().unwrap().clone(),
|
||||
_ => Value::Nil,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verse_names_and_adding() {
|
||||
let mut song = Song::default();
|
||||
|
|
@ -1483,50 +1497,6 @@ You saved my soul"
|
|||
assert_eq!(name, VerseName::Verse { number: 5 });
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_song_to_slide() {
|
||||
let song = test_song();
|
||||
let songs: Vec<Song> = (0..100)
|
||||
.map(|index| {
|
||||
let mut song = song.clone();
|
||||
song.id = index;
|
||||
song
|
||||
})
|
||||
.collect();
|
||||
let fontdb = Arc::new(fontdb::Database::new());
|
||||
songs.into_par_iter().for_each(|song| {
|
||||
let slides = song.to_slides().unwrap();
|
||||
slides.into_par_iter().for_each(|slide| {
|
||||
text_svg_generator_with_cache(slide, &fontdb, None)
|
||||
.map_or_else(
|
||||
|e| assert!(false, "{e}"),
|
||||
|slide| {
|
||||
assert!(slide.text_svg.is_some_and(
|
||||
|svg| svg.handle.is_some()
|
||||
))
|
||||
},
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// extern crate test;
|
||||
// use test::{Bencher, black_box};
|
||||
|
||||
// #[bench]
|
||||
// fn bench_pow(b: &mut Bencher) {
|
||||
// // Optionally include some setup
|
||||
// let x: f64 = 211.0 * 11.0;
|
||||
// let y: f64 = 301.0 * 103.0;
|
||||
|
||||
// b.iter(|| {
|
||||
// // Inner closure, the actual test
|
||||
// for i in 1..100 {
|
||||
// black_box(x.powf(y).powf(x));
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// pub fn test_lisp_conversion() {
|
||||
// let value = test_lisp_song();
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ pub fn bg_from_video(
|
|||
} else {
|
||||
let output_duration = Command::new("ffprobe")
|
||||
.args(["-i", &video.to_string_lossy()])
|
||||
.output()?;
|
||||
io::stderr().write_all(&output_duration.stderr)?;
|
||||
.output()
|
||||
.expect("failed to execute ffprobe");
|
||||
io::stderr().write_all(&output_duration.stderr).unwrap();
|
||||
let mut at_second = 5;
|
||||
let mut log = str::from_utf8(&output_duration.stderr)
|
||||
.expect("Using non UTF-8 characters")
|
||||
|
|
@ -71,18 +72,15 @@ pub fn bg_from_video(
|
|||
pub fn bg_path_from_video(video: &Path) -> PathBuf {
|
||||
let video = PathBuf::from(video);
|
||||
debug!(?video);
|
||||
let mut data_dir =
|
||||
dirs::cache_dir().expect("Can't find cache dir");
|
||||
let mut data_dir = dirs::data_local_dir().unwrap();
|
||||
data_dir.push("lumina");
|
||||
data_dir.push("thumbnails");
|
||||
let _ = fs::create_dir_all(&data_dir);
|
||||
if !data_dir.exists() {
|
||||
fs::create_dir(&data_dir)
|
||||
.expect("Could not create thumbnails dir");
|
||||
}
|
||||
let mut screenshot = data_dir.clone();
|
||||
screenshot
|
||||
.push(video.file_name().expect("Should have file name"));
|
||||
screenshot.push(video.file_name().unwrap());
|
||||
screenshot.set_extension("png");
|
||||
screenshot
|
||||
}
|
||||
|
|
@ -93,9 +91,19 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_bg_video_creation() {
|
||||
let video = Path::new("./res/bigbuckbunny.mp4");
|
||||
let video = Path::new("/home/chris/vids/moms-funeral.mp4");
|
||||
let screenshot = bg_path_from_video(video);
|
||||
match bg_from_video(video, &screenshot) {
|
||||
let screenshot_string =
|
||||
screenshot.to_str().expect("Should be thing");
|
||||
assert_eq!(
|
||||
screenshot_string,
|
||||
"/home/chris/.local/share/lumina/thumbnails/moms-funeral.png"
|
||||
);
|
||||
|
||||
// let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
let result = bg_from_video(video, &screenshot);
|
||||
// let result = runtime.block_on(future);
|
||||
match result {
|
||||
Ok(_o) => assert!(screenshot.exists()),
|
||||
Err(e) => debug_assert!(
|
||||
false,
|
||||
|
|
@ -104,4 +112,18 @@ mod test {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bg_not_same() {
|
||||
let video = Path::new(
|
||||
"/home/chris/vids/All WebDev Sucks and you know it.webm",
|
||||
);
|
||||
let screenshot = bg_path_from_video(video);
|
||||
let screenshot_string =
|
||||
screenshot.to_str().expect("Should be thing");
|
||||
assert_ne!(
|
||||
screenshot_string,
|
||||
"/home/chris/.local/share/lumina/thumbnails/All WebDev Sucks and you know it.webm"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,10 +74,11 @@ impl Content for Video {
|
|||
|
||||
fn subtext(&self) -> String {
|
||||
if self.path.exists() {
|
||||
self.path.file_name().map_or_else(
|
||||
|| "Missing video".into(),
|
||||
|f| f.to_string_lossy().to_string(),
|
||||
)
|
||||
self.path
|
||||
.file_name()
|
||||
.map_or("Missing video".into(), |f| {
|
||||
f.to_string_lossy().to_string()
|
||||
})
|
||||
} else {
|
||||
"Missing video".into()
|
||||
}
|
||||
|
|
@ -90,21 +91,20 @@ impl From<Value> for Video {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
impl From<&Value> for Video {
|
||||
fn from(value: &Value) -> Self {
|
||||
match value {
|
||||
Value::List(list) => {
|
||||
let path = list
|
||||
.iter()
|
||||
.position(|v| {
|
||||
let path = if let Some(path_pos) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("source"))
|
||||
})
|
||||
.and_then(|path_pos| {
|
||||
let pos = path_pos + 1;
|
||||
list.get(pos)
|
||||
.map(|p| PathBuf::from(String::from(p)))
|
||||
});
|
||||
}) {
|
||||
let pos = path_pos + 1;
|
||||
list.get(pos)
|
||||
.map(|p| PathBuf::from(String::from(p)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let title = path.clone().map(|p| {
|
||||
let path =
|
||||
|
|
@ -114,41 +114,40 @@ impl From<&Value> for Video {
|
|||
title.to_string()
|
||||
});
|
||||
|
||||
let start_time = list
|
||||
.iter()
|
||||
.position(|v| {
|
||||
let start_time = if let Some(start_pos) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from(
|
||||
"start-time",
|
||||
))
|
||||
})
|
||||
.and_then(|start_pos| {
|
||||
let pos = start_pos + 1;
|
||||
list.get(pos).map(|p| i32::from(p) as f32)
|
||||
});
|
||||
}) {
|
||||
let pos = start_pos + 1;
|
||||
list.get(pos).map(|p| i32::from(p) as f32)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let end_time = list
|
||||
.iter()
|
||||
.position(|v| {
|
||||
let end_time = if let Some(end_pos) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from(
|
||||
"end-time",
|
||||
))
|
||||
})
|
||||
.and_then(|end_pos| {
|
||||
let pos = end_pos + 1;
|
||||
list.get(pos).map(|p| i32::from(p) as f32)
|
||||
});
|
||||
}) {
|
||||
let pos = end_pos + 1;
|
||||
list.get(pos).map(|p| i32::from(p) as f32)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let looping = list
|
||||
.iter()
|
||||
.position(|v| {
|
||||
let looping = if let Some(loop_pos) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("loop"))
|
||||
})
|
||||
.is_some_and(|loop_pos| {
|
||||
let pos = loop_pos + 1;
|
||||
list.get(pos).is_some_and(|l| {
|
||||
String::from(l) == *"true"
|
||||
})
|
||||
});
|
||||
}) {
|
||||
let pos = loop_pos + 1;
|
||||
list.get(pos)
|
||||
.is_some_and(|l| String::from(l) == *"true")
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Self {
|
||||
title: title.unwrap_or_default(),
|
||||
|
|
@ -311,9 +310,7 @@ mod test {
|
|||
fn test_video(title: String) -> Video {
|
||||
Video {
|
||||
title,
|
||||
path: PathBuf::from(
|
||||
"/home/chris/docs/notes/lessons/christ-our-hope.mp4",
|
||||
),
|
||||
path: PathBuf::from("~/vids/camprules2024.mp4"),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -324,10 +321,13 @@ mod test {
|
|||
items: vec![],
|
||||
kind: LibraryKind::Video,
|
||||
};
|
||||
let mut db = add_db().await.unwrap().acquire().await.unwrap();
|
||||
let mut db = crate::core::model::get_db().await;
|
||||
video_model.load_from_db(&mut db).await;
|
||||
if let Some(video) = video_model.find(|v| v.id == 2) {
|
||||
let test_video = test_video("christ-our-hope.mp4".into());
|
||||
if let Some(video) = video_model.find(|v| v.id == 73) {
|
||||
let test_video = test_video(
|
||||
"Getting started with Tokio. The ultimate starter guide to writing async Rust."
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(test_video.title, video.title);
|
||||
} else {
|
||||
assert!(false);
|
||||
|
|
@ -361,9 +361,4 @@ mod test {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_db() -> Result<SqlitePool> {
|
||||
let db_url = String::from("sqlite://./test.db");
|
||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
289
src/lisp.rs
289
src/lisp.rs
|
|
@ -35,157 +35,156 @@ pub fn parse_lisp(value: Value) -> Vec<ServiceItem> {
|
|||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test {
|
||||
// use std::{fs::read_to_string, path::PathBuf};
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{fs::read_to_string, path::PathBuf};
|
||||
|
||||
// use crate::core::{
|
||||
// images::Image,
|
||||
// kinds::ServiceItemKind,
|
||||
// service_items::ServiceTrait,
|
||||
// slide::{Background, TextAlignment},
|
||||
// songs::Song,
|
||||
// videos::Video,
|
||||
// };
|
||||
use crate::core::{
|
||||
images::Image,
|
||||
kinds::ServiceItemKind,
|
||||
service_items::ServiceTrait,
|
||||
slide::{Background, TextAlignment},
|
||||
songs::Song,
|
||||
videos::Video,
|
||||
};
|
||||
|
||||
// use super::*;
|
||||
// use pretty_assertions::assert_eq;
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
// #[test]
|
||||
// fn test_parsing_lisp() {
|
||||
// let lisp =
|
||||
// read_to_string("./test_slides.lisp").expect("oops");
|
||||
// let lisp_value = crisp::reader::read(&lisp);
|
||||
// let hard_coded_items =
|
||||
// vec![service_item_1(), service_item_2()];
|
||||
// match lisp_value {
|
||||
// Value::List(value) => {
|
||||
// let mut lisp_items = vec![];
|
||||
// for value in value {
|
||||
// let mut vec = parse_lisp(value);
|
||||
// lisp_items.append(&mut vec);
|
||||
// }
|
||||
// assert_eq!(lisp_items, hard_coded_items)
|
||||
// }
|
||||
// _ => panic!("this should be a lisp"),
|
||||
// }
|
||||
// }
|
||||
#[test]
|
||||
fn test_parsing_lisp() {
|
||||
let lisp =
|
||||
read_to_string("./test_slides.lisp").expect("oops");
|
||||
let lisp_value = crisp::reader::read(&lisp);
|
||||
let hard_coded_items =
|
||||
vec![service_item_1(), service_item_2()];
|
||||
match lisp_value {
|
||||
Value::List(value) => {
|
||||
let mut lisp_items = vec![];
|
||||
for value in value {
|
||||
let mut vec = parse_lisp(value);
|
||||
lisp_items.append(&mut vec);
|
||||
}
|
||||
assert_eq!(lisp_items, hard_coded_items)
|
||||
}
|
||||
_ => panic!("this should be a lisp"),
|
||||
}
|
||||
}
|
||||
|
||||
// // Planning on removing lisp potentially
|
||||
// // #[test]
|
||||
// // fn test_parsing_lisp_presentation() {
|
||||
// // let lisp = read_to_string("./testypres.lisp").expect("oops");
|
||||
// // let lisp_value = crisp::reader::read(&lisp);
|
||||
// // let hard_coded_items = vec![
|
||||
// // service_item_1(),
|
||||
// // service_item_2(),
|
||||
// // service_item_3(),
|
||||
// // ];
|
||||
// // match lisp_value {
|
||||
// // Value::List(value) => {
|
||||
// // let mut lisp_items = vec![];
|
||||
// // for value in value {
|
||||
// // let mut vec = parse_lisp(value);
|
||||
// // lisp_items.append(&mut vec);
|
||||
// // }
|
||||
// // let item_1 = &lisp_items[0];
|
||||
// // let item_2 = &lisp_items[1];
|
||||
// // let item_3 = &lisp_items[2];
|
||||
// // assert_eq!(item_1, &hard_coded_items[0]);
|
||||
// // assert_eq!(item_2, &hard_coded_items[1]);
|
||||
// // assert_eq!(item_3, &hard_coded_items[2]);
|
||||
#[test]
|
||||
fn test_parsing_lisp_presentation() {
|
||||
let lisp = read_to_string("./testypres.lisp").expect("oops");
|
||||
let lisp_value = crisp::reader::read(&lisp);
|
||||
let hard_coded_items = vec![
|
||||
service_item_1(),
|
||||
service_item_2(),
|
||||
service_item_3(),
|
||||
];
|
||||
match lisp_value {
|
||||
Value::List(value) => {
|
||||
let mut lisp_items = vec![];
|
||||
for value in value {
|
||||
let mut vec = parse_lisp(value);
|
||||
lisp_items.append(&mut vec);
|
||||
}
|
||||
let item_1 = &lisp_items[0];
|
||||
let item_2 = &lisp_items[1];
|
||||
let item_3 = &lisp_items[2];
|
||||
assert_eq!(item_1, &hard_coded_items[0]);
|
||||
assert_eq!(item_2, &hard_coded_items[1]);
|
||||
assert_eq!(item_3, &hard_coded_items[2]);
|
||||
|
||||
// // assert_eq!(lisp_items, hard_coded_items);
|
||||
// // }
|
||||
// // _ => panic!("this should be a lisp"),
|
||||
// // }
|
||||
// // }
|
||||
assert_eq!(lisp_items, hard_coded_items);
|
||||
}
|
||||
_ => panic!("this should be a lisp"),
|
||||
}
|
||||
}
|
||||
|
||||
// fn service_item_1() -> ServiceItem {
|
||||
// let image = Image {
|
||||
// title: "This is frodo".to_string(),
|
||||
// path: PathBuf::from("~/pics/frodo.jpg"),
|
||||
// ..Default::default()
|
||||
// };
|
||||
// let slide = &image.to_slides().unwrap()[0];
|
||||
// let slide = slide
|
||||
// .clone()
|
||||
// .set_text("This is frodo")
|
||||
// .set_font("Quicksand")
|
||||
// .set_font_size(70)
|
||||
// .set_audio(None);
|
||||
// ServiceItem {
|
||||
// title: "This is frodo".to_string(),
|
||||
// kind: ServiceItemKind::Content(slide.clone()),
|
||||
// slides: vec![slide],
|
||||
// ..Default::default()
|
||||
// }
|
||||
// }
|
||||
fn service_item_1() -> ServiceItem {
|
||||
let image = Image {
|
||||
title: "This is frodo".to_string(),
|
||||
path: PathBuf::from("~/pics/frodo.jpg"),
|
||||
..Default::default()
|
||||
};
|
||||
let slide = &image.to_slides().unwrap()[0];
|
||||
let slide = slide
|
||||
.clone()
|
||||
.set_text("This is frodo")
|
||||
.set_font("Quicksand")
|
||||
.set_font_size(70)
|
||||
.set_audio(None);
|
||||
ServiceItem {
|
||||
title: "This is frodo".to_string(),
|
||||
kind: ServiceItemKind::Content(slide.clone()),
|
||||
slides: vec![slide],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// fn service_item_2() -> ServiceItem {
|
||||
// let video = Video::from(PathBuf::from(
|
||||
// "~/vids/test/camprules2024.mp4",
|
||||
// ));
|
||||
// let slide = &video.to_slides().unwrap()[0];
|
||||
// ServiceItem {
|
||||
// title: "camprules2024.mp4".to_string(),
|
||||
// kind: ServiceItemKind::Video(Video {
|
||||
// title: "camprules2024.mp4".to_string(),
|
||||
// path: PathBuf::from("~/vids/test/camprules2024.mp4"),
|
||||
// start_time: None,
|
||||
// end_time: None,
|
||||
// looping: false,
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// slides: vec![slide.clone()],
|
||||
// ..Default::default()
|
||||
// }
|
||||
// }
|
||||
fn service_item_2() -> ServiceItem {
|
||||
let video = Video::from(PathBuf::from(
|
||||
"~/vids/test/camprules2024.mp4",
|
||||
));
|
||||
let slide = &video.to_slides().unwrap()[0];
|
||||
ServiceItem {
|
||||
title: "camprules2024.mp4".to_string(),
|
||||
kind: ServiceItemKind::Video(Video {
|
||||
title: "camprules2024.mp4".to_string(),
|
||||
path: PathBuf::from("~/vids/test/camprules2024.mp4"),
|
||||
start_time: None,
|
||||
end_time: None,
|
||||
looping: false,
|
||||
..Default::default()
|
||||
}),
|
||||
slides: vec![slide.clone()],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// fn service_item_3() -> ServiceItem {
|
||||
// ServiceItem {
|
||||
// title: "Death Was Arrested".to_string(),
|
||||
// kind: ServiceItemKind::Song(test_song()),
|
||||
// database_id: 7,
|
||||
// ..Default::default()
|
||||
// }
|
||||
// }
|
||||
fn service_item_3() -> ServiceItem {
|
||||
ServiceItem {
|
||||
title: "Death Was Arrested".to_string(),
|
||||
kind: ServiceItemKind::Song(test_song()),
|
||||
database_id: 7,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// fn test_song() -> Song {
|
||||
// Song {
|
||||
// id: 7,
|
||||
// title: "Death Was Arrested".to_string(),
|
||||
// lyrics: Some("Intro 1\nDeath Was Arrested\nNorth Point Worship\n\nVerse 1\nAlone in my sorrow\nAnd dead in my sin\n\nLost without hope\nWith no place to begin\n\nYour love made a way\nTo let mercy come in\n\nWhen death was arrested\nAnd my life began\n\nVerse 2\nAsh was redeemed\nOnly beauty remains\n\nMy orphan heart\nWas given a name\n\nMy mourning grew quiet,\nMy feet rose to dance\n\nWhen death was arrested\nAnd my life began\n\nChorus 1\nOh, Your grace so free,\nWashes over me\n\nYou have made me new,\nNow life begins with You\n\nIt's Your endless love,\nPouring down on us\n\nYou have made us new,\nNow life begins with You\n\nVerse 3\nReleased from my chains,\nI'm a prisoner no more\n\nMy shame was a ransom\nHe faithfully bore\n\nHe cancelled my debt and\nHe called me His friend\n\nWhen death was arrested\nAnd my life began\n\nVerse 4\nOur Savior displayed\nOn a criminal's cross\n\nDarkness rejoiced as though\nHeaven had lost\n\nBut then Jesus arose\nWith our freedom in hand\n\nThat's when death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began\n\nBridge 1\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nEnding 1\nWhen death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began".to_string()),
|
||||
// author: Some(
|
||||
// "North Point Worship".to_string(),
|
||||
// ),
|
||||
// ccli: None,
|
||||
// audio: Some("file:///home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3".into()),
|
||||
// verse_order: Some(vec![
|
||||
// "I1".to_string(),
|
||||
// "V1".to_string(),
|
||||
// "V2".to_string(),
|
||||
// "C1".to_string(),
|
||||
// "V3".to_string(),
|
||||
// "C1".to_string(),
|
||||
// "V4".to_string(),
|
||||
// "C1".to_string(),
|
||||
// "B1".to_string(),
|
||||
// "B1".to_string(),
|
||||
// "E1".to_string(),
|
||||
// "E2".to_string(),
|
||||
// ]),
|
||||
// background: Some(Background::try_from("file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg").unwrap()),
|
||||
// text_alignment: Some(TextAlignment::MiddleCenter),
|
||||
// font: Some("Quicksand Bold".to_string()),
|
||||
// font_size: Some(60),
|
||||
// stroke_size: Some(2),
|
||||
// verses: None,
|
||||
// verse_map: None,
|
||||
// stroke_color: todo!(),
|
||||
// shadow_size: todo!(),
|
||||
// shadow_offset: todo!(),
|
||||
// shadow_color: todo!(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
fn test_song() -> Song {
|
||||
Song {
|
||||
id: 7,
|
||||
title: "Death Was Arrested".to_string(),
|
||||
lyrics: Some("Intro 1\nDeath Was Arrested\nNorth Point Worship\n\nVerse 1\nAlone in my sorrow\nAnd dead in my sin\n\nLost without hope\nWith no place to begin\n\nYour love made a way\nTo let mercy come in\n\nWhen death was arrested\nAnd my life began\n\nVerse 2\nAsh was redeemed\nOnly beauty remains\n\nMy orphan heart\nWas given a name\n\nMy mourning grew quiet,\nMy feet rose to dance\n\nWhen death was arrested\nAnd my life began\n\nChorus 1\nOh, Your grace so free,\nWashes over me\n\nYou have made me new,\nNow life begins with You\n\nIt's Your endless love,\nPouring down on us\n\nYou have made us new,\nNow life begins with You\n\nVerse 3\nReleased from my chains,\nI'm a prisoner no more\n\nMy shame was a ransom\nHe faithfully bore\n\nHe cancelled my debt and\nHe called me His friend\n\nWhen death was arrested\nAnd my life began\n\nVerse 4\nOur Savior displayed\nOn a criminal's cross\n\nDarkness rejoiced as though\nHeaven had lost\n\nBut then Jesus arose\nWith our freedom in hand\n\nThat's when death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began\n\nBridge 1\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nEnding 1\nWhen death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began".to_string()),
|
||||
author: Some(
|
||||
"North Point Worship".to_string(),
|
||||
),
|
||||
ccli: None,
|
||||
audio: Some("file:///home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3".into()),
|
||||
verse_order: Some(vec![
|
||||
"I1".to_string(),
|
||||
"V1".to_string(),
|
||||
"V2".to_string(),
|
||||
"C1".to_string(),
|
||||
"V3".to_string(),
|
||||
"C1".to_string(),
|
||||
"V4".to_string(),
|
||||
"C1".to_string(),
|
||||
"B1".to_string(),
|
||||
"B1".to_string(),
|
||||
"E1".to_string(),
|
||||
"E2".to_string(),
|
||||
]),
|
||||
background: Some(Background::try_from("file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg").unwrap()),
|
||||
text_alignment: Some(TextAlignment::MiddleCenter),
|
||||
font: Some("Quicksand Bold".to_string()),
|
||||
font_size: Some(60),
|
||||
stroke_size: Some(2),
|
||||
verses: None,
|
||||
verse_map: None,
|
||||
stroke_color: todo!(),
|
||||
shadow_size: todo!(),
|
||||
shadow_offset: todo!(),
|
||||
shadow_color: todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
307
src/main.rs
307
src/main.rs
|
|
@ -1,13 +1,11 @@
|
|||
#![allow(clippy::missing_panics_doc)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
use clap::Parser;
|
||||
use clap::{Parser, command};
|
||||
use core::service_items::ServiceItem;
|
||||
use core::slide::{
|
||||
Background, BackgroundKind, Slide, SlideBuilder, TextAlignment,
|
||||
};
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::cosmic_config::{Config, CosmicConfigEntry};
|
||||
use cosmic::dialog::file_chooser::{open, save};
|
||||
use cosmic::dialog::file_chooser::save;
|
||||
use cosmic::iced::alignment::Vertical;
|
||||
use cosmic::iced::keyboard::{Key, Modifiers};
|
||||
use cosmic::iced::window::{Mode, Position};
|
||||
|
|
@ -35,12 +33,13 @@ use cosmic::widget::{container, text};
|
|||
use cosmic::widget::{icon, slider};
|
||||
use cosmic::{Application, ApplicationExt, Apply, Element, executor};
|
||||
use cosmic::{cosmic_config, theme};
|
||||
// use crisp::types::Value;
|
||||
// use lisp::parse_lisp;
|
||||
use crisp::types::Value;
|
||||
use lisp::parse_lisp;
|
||||
use miette::{IntoDiagnostic, Result, miette};
|
||||
use rayon::prelude::*;
|
||||
use resvg::usvg::fontdb;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, level_filters::LevelFilter};
|
||||
|
|
@ -62,7 +61,7 @@ use crate::ui::video_editor::{self, VideoEditor};
|
|||
use crate::ui::widgets::draggable;
|
||||
|
||||
pub mod core;
|
||||
// pub mod lisp;
|
||||
pub mod lisp;
|
||||
pub mod ui;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
|
|
@ -124,16 +123,17 @@ fn main() -> Result<()> {
|
|||
}
|
||||
};
|
||||
|
||||
let settings = if args.ui {
|
||||
let settings;
|
||||
if args.ui {
|
||||
debug!(target: "lumina", "main view");
|
||||
Settings::default().debug(false).is_daemon(true)
|
||||
settings = Settings::default().debug(false).is_daemon(true);
|
||||
} else {
|
||||
debug!("window view");
|
||||
Settings::default()
|
||||
settings = Settings::default()
|
||||
.debug(false)
|
||||
.no_main_window(true)
|
||||
.is_daemon(true)
|
||||
};
|
||||
.is_daemon(true);
|
||||
}
|
||||
|
||||
cosmic::app::run::<App>(settings, (args, config_handler, config))
|
||||
.map_err(|e| miette!("Invalid things... {}", e))
|
||||
|
|
@ -143,7 +143,6 @@ fn main() -> Result<()> {
|
|||
// Theme::dark()
|
||||
// }
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct App {
|
||||
core: Core,
|
||||
nav_model: nav_bar::Model,
|
||||
|
|
@ -179,7 +178,6 @@ struct App {
|
|||
obs_connection: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Present(presenter::Message),
|
||||
|
|
@ -221,7 +219,6 @@ enum Message {
|
|||
New,
|
||||
Open,
|
||||
OpenFile(PathBuf),
|
||||
OpenLoadItems(Vec<ServiceItem>),
|
||||
Save,
|
||||
SaveAsDialog,
|
||||
SaveAs(PathBuf),
|
||||
|
|
@ -276,8 +273,6 @@ impl cosmic::Application for App {
|
|||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn init(
|
||||
core: Core,
|
||||
input: Self::Flags,
|
||||
|
|
@ -292,56 +287,60 @@ impl cosmic::Application for App {
|
|||
let mut windows = vec![];
|
||||
|
||||
if input.0.ui {
|
||||
windows.push(
|
||||
core.main_window_id()
|
||||
.expect("should be a window here"),
|
||||
);
|
||||
windows.push(core.main_window_id().unwrap());
|
||||
}
|
||||
|
||||
let (config_handler, settings) = (input.1, input.2);
|
||||
|
||||
// let items = input.0.file.map_or_else(Vec::new, |file| {
|
||||
// match read_to_string(file) {
|
||||
// Ok(lisp) => {
|
||||
// let mut service_items = vec![];
|
||||
// let lisp = crisp::reader::read(&lisp);
|
||||
// match lisp {
|
||||
// Value::List(vec) => {
|
||||
// for value in vec {
|
||||
// let mut inner_vector =
|
||||
// parse_lisp(value);
|
||||
// service_items
|
||||
// .append(&mut inner_vector);
|
||||
// }
|
||||
// }
|
||||
// _ => todo!(),
|
||||
// }
|
||||
// service_items
|
||||
// }
|
||||
// Err(e) => {
|
||||
// warn!("Missing file or could not read: {e}");
|
||||
// vec![]
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
let items = if let Some(file) = input.0.file {
|
||||
match read_to_string(file) {
|
||||
Ok(lisp) => {
|
||||
let mut service_items = vec![];
|
||||
let lisp = crisp::reader::read(&lisp);
|
||||
match lisp {
|
||||
Value::List(vec) => {
|
||||
// let items = vec
|
||||
// .into_par_iter()
|
||||
// .map(|value| parse_lisp(value))
|
||||
// .collect();
|
||||
// slide_vector.append(items);
|
||||
for value in vec {
|
||||
let mut inner_vector =
|
||||
parse_lisp(value);
|
||||
service_items
|
||||
.append(&mut inner_vector);
|
||||
}
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
service_items
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Missing file or could not read: {e}");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// let items: Vec<ServiceItem> = items
|
||||
// .into_par_iter()
|
||||
// .map(|mut item| {
|
||||
// item.slides = item
|
||||
// .slides
|
||||
// .into_par_iter()
|
||||
// .map(|mut slide| {
|
||||
// text_svg::text_svg_generator(
|
||||
// &mut slide, &fontdb,
|
||||
// );
|
||||
// slide
|
||||
// })
|
||||
// .collect();
|
||||
// item
|
||||
// })
|
||||
// .collect();
|
||||
let items: Vec<ServiceItem> = vec![];
|
||||
let items: Vec<ServiceItem> = items
|
||||
.into_par_iter()
|
||||
.map(|mut item| {
|
||||
item.slides = item
|
||||
.slides
|
||||
.into_par_iter()
|
||||
.map(|mut slide| {
|
||||
text_svg::text_svg_generator(
|
||||
&mut slide,
|
||||
Arc::clone(&fontdb),
|
||||
);
|
||||
slide
|
||||
})
|
||||
.collect();
|
||||
item
|
||||
})
|
||||
.collect();
|
||||
|
||||
let presenter = Presenter::with_items(items.clone());
|
||||
let song_editor = SongEditor::new(Arc::clone(&fontdb));
|
||||
|
|
@ -436,7 +435,7 @@ impl cosmic::Application for App {
|
|||
batch.push(app.show_window());
|
||||
}
|
||||
|
||||
batch.push(add_library());
|
||||
batch.push(app.add_library());
|
||||
// batch.push(app.add_service(items, Arc::clone(&fontdb)));
|
||||
let batch = Task::batch(batch);
|
||||
(app, batch)
|
||||
|
|
@ -661,7 +660,7 @@ impl cosmic::Application for App {
|
|||
iced::keyboard::Event::ModifiersChanged(
|
||||
modifiers,
|
||||
) => Some(Message::ModifiersPressed(modifiers)),
|
||||
iced::keyboard::Event::KeyPressed { .. } => None,
|
||||
_ => None,
|
||||
},
|
||||
iced::Event::Mouse(_event) => None,
|
||||
iced::Event::Window(window_event) => {
|
||||
|
|
@ -714,7 +713,6 @@ impl cosmic::Application for App {
|
|||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn dialog(&self) -> Option<Element<'_, Self::Message>> {
|
||||
let cosmic::cosmic_theme::Spacing {
|
||||
space_xxs,
|
||||
|
|
@ -776,7 +774,6 @@ impl cosmic::Application for App {
|
|||
.spacing(space_s)
|
||||
.apply(container)
|
||||
.padding(space_xl)
|
||||
.max_width(600)
|
||||
.style(nav_bar_style);
|
||||
let modal = mouse_area(modal)
|
||||
.on_press(Message::None)
|
||||
|
|
@ -860,18 +857,11 @@ impl cosmic::Application for App {
|
|||
modal
|
||||
);
|
||||
Some(mouse_stack.into())
|
||||
} else if self.song_editor.importing() {
|
||||
Some(
|
||||
self.song_editor
|
||||
.import_view()
|
||||
.map(Message::SongEditor),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Key(key, modifiers) => {
|
||||
|
|
@ -961,7 +951,7 @@ impl cosmic::Application for App {
|
|||
}
|
||||
presentation_editor::Action::SplitAddPresentation((first, second)) => {
|
||||
if self.library.is_some() {
|
||||
let second_task = self.update(Message::Library(library::Message::AddPresentationSplit(Some(second))));
|
||||
let second_task = self.update(Message::Library(library::Message::AddPresentations(Some(vec![second]))));
|
||||
self.update(Message::Library(library::Message::UpdatePresentation(first))).chain(second_task)
|
||||
|
||||
} else {
|
||||
|
|
@ -1035,6 +1025,7 @@ impl cosmic::Application for App {
|
|||
}
|
||||
self.current_item =
|
||||
(item_index, slide_index);
|
||||
Task::batch(tasks)
|
||||
} else {
|
||||
// debug!("Slides are not longer");
|
||||
if self
|
||||
|
|
@ -1056,8 +1047,8 @@ impl cosmic::Application for App {
|
|||
self.current_item =
|
||||
(item_index + 1, 0);
|
||||
}
|
||||
Task::batch(tasks)
|
||||
}
|
||||
Task::batch(tasks)
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
|
|
@ -1096,11 +1087,14 @@ impl cosmic::Application for App {
|
|||
} else {
|
||||
// debug!("Change slide to previous items slides");
|
||||
let previous_item_slides_length =
|
||||
self.service
|
||||
if let Some(item) = self
|
||||
.service
|
||||
.get(item_index - 1)
|
||||
.map_or(0, |item| {
|
||||
item.slides.len()
|
||||
});
|
||||
{
|
||||
item.slides.len()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.current_item = (
|
||||
item_index - 1,
|
||||
previous_item_slides_length - 1,
|
||||
|
|
@ -1221,7 +1215,11 @@ impl cosmic::Application for App {
|
|||
})
|
||||
}
|
||||
Message::CloseWindow(id) => {
|
||||
id.map_or_else(Task::none, window::close)
|
||||
if let Some(id) = id {
|
||||
window::close(id)
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
Message::WindowOpened(id) => {
|
||||
debug!(?id, "Window opened");
|
||||
|
|
@ -1379,13 +1377,12 @@ impl cosmic::Application for App {
|
|||
item.slides = item
|
||||
.slides
|
||||
.into_par_iter()
|
||||
.map(|slide| {
|
||||
.map(|mut slide| {
|
||||
let fontdb = Arc::clone(&self.fontdb);
|
||||
text_svg::text_svg_generator(
|
||||
slide.clone(),
|
||||
&fontdb,
|
||||
)
|
||||
.unwrap_or(slide)
|
||||
&mut slide, fontdb,
|
||||
);
|
||||
slide
|
||||
})
|
||||
.collect();
|
||||
self.service.insert(index, item);
|
||||
|
|
@ -1420,13 +1417,12 @@ impl cosmic::Application for App {
|
|||
item.slides = item
|
||||
.slides
|
||||
.into_par_iter()
|
||||
.map(|slide| {
|
||||
.map(|mut slide| {
|
||||
let fontdb = Arc::clone(&self.fontdb);
|
||||
text_svg::text_svg_generator(
|
||||
slide.clone(),
|
||||
&fontdb,
|
||||
)
|
||||
.unwrap_or(slide)
|
||||
&mut slide, fontdb,
|
||||
);
|
||||
slide
|
||||
})
|
||||
.collect();
|
||||
self.service.push(item);
|
||||
|
|
@ -1444,7 +1440,7 @@ impl cosmic::Application for App {
|
|||
Task::none()
|
||||
}
|
||||
Message::Search(query) => {
|
||||
self.search_query.clone_from(&query);
|
||||
self.search_query = query.clone();
|
||||
self.search(query)
|
||||
}
|
||||
Message::UpdateSearchResults(items) => {
|
||||
|
|
@ -1501,50 +1497,22 @@ impl cosmic::Application for App {
|
|||
}
|
||||
Message::Open => {
|
||||
debug!("Open file");
|
||||
Task::perform(open_dialog(), |res| match res {
|
||||
Ok(file) => {
|
||||
cosmic::Action::App(Message::OpenFile(file))
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
?e,
|
||||
"There was an error during opening"
|
||||
);
|
||||
cosmic::Action::None
|
||||
}
|
||||
})
|
||||
Task::none()
|
||||
}
|
||||
Message::OpenFile(file) => {
|
||||
debug!(?file, "opening file");
|
||||
Task::perform(
|
||||
async move { file::load(file) },
|
||||
|res| match res {
|
||||
Ok(items) => cosmic::Action::App(
|
||||
Message::OpenLoadItems(items),
|
||||
),
|
||||
Err(e) => {
|
||||
error!(?e);
|
||||
cosmic::Action::None
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Message::OpenLoadItems(items) => {
|
||||
self.service = items.clone();
|
||||
self.presenter.service = items;
|
||||
Task::none()
|
||||
}
|
||||
Message::Save => {
|
||||
let service = self.service.clone();
|
||||
let file = self.file.clone();
|
||||
let file_name = self.file.file_name().expect("Since we are saving we should have given a name by now").to_owned();
|
||||
Task::perform(
|
||||
async move { file::save(service, file, true) },
|
||||
file::save(service, file.clone()),
|
||||
move |res| match res {
|
||||
Ok(()) => {
|
||||
tracing::info!(
|
||||
"saving file to: {:?}",
|
||||
file_name
|
||||
file
|
||||
);
|
||||
cosmic::Action::None
|
||||
}
|
||||
|
|
@ -1616,7 +1584,6 @@ impl cosmic::Application for App {
|
|||
}
|
||||
|
||||
// Main window view
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn view(&self) -> Element<Message> {
|
||||
let cosmic::cosmic_theme::Spacing {
|
||||
space_none,
|
||||
|
|
@ -1633,27 +1600,24 @@ impl cosmic::Application for App {
|
|||
);
|
||||
|
||||
let video_button_icon =
|
||||
self.presenter.video.as_ref().map_or_else(
|
||||
|| {
|
||||
button::icon(icon::from_name("media-play"))
|
||||
.tooltip("Play")
|
||||
.on_press(Message::Present(
|
||||
presenter::Message::StartVideo,
|
||||
))
|
||||
},
|
||||
|video| {
|
||||
let (icon_name, tooltip) = if video.paused() {
|
||||
("media-play", "Play")
|
||||
} else {
|
||||
("media-pause", "Pause")
|
||||
};
|
||||
button::icon(icon::from_name(icon_name))
|
||||
.tooltip(tooltip)
|
||||
.on_press(Message::Present(
|
||||
presenter::Message::StartVideo,
|
||||
))
|
||||
},
|
||||
);
|
||||
if let Some(video) = &self.presenter.video {
|
||||
let (icon_name, tooltip) = if video.paused() {
|
||||
("media-play", "Play")
|
||||
} else {
|
||||
("media-pause", "Pause")
|
||||
};
|
||||
button::icon(icon::from_name(icon_name))
|
||||
.tooltip(tooltip)
|
||||
.on_press(Message::Present(
|
||||
presenter::Message::StartVideo,
|
||||
))
|
||||
} else {
|
||||
button::icon(icon::from_name("media-play"))
|
||||
.tooltip("Play")
|
||||
.on_press(Message::Present(
|
||||
presenter::Message::StartVideo,
|
||||
))
|
||||
};
|
||||
|
||||
let slide_preview = column![
|
||||
Space::with_height(Length::Fill),
|
||||
|
|
@ -1695,10 +1659,13 @@ impl cosmic::Application for App {
|
|||
|
||||
let library = if self.library_open {
|
||||
Container::new(
|
||||
Container::new(self.library.as_ref().map_or_else(
|
||||
|| Element::from(Space::new(0, 0)),
|
||||
|library| library.view().map(Message::Library),
|
||||
))
|
||||
Container::new(
|
||||
if let Some(library) = &self.library {
|
||||
library.view().map(Message::Library)
|
||||
} else {
|
||||
Space::new(0, 0).into()
|
||||
},
|
||||
)
|
||||
.style(nav_bar_style),
|
||||
)
|
||||
.padding(space_s)
|
||||
|
|
@ -1839,8 +1806,14 @@ where
|
|||
.map(|id| cosmic::Action::App(Message::WindowOpened(id)))
|
||||
}
|
||||
|
||||
fn add_library(&self) -> Task<Message> {
|
||||
Task::perform(async move { Library::new().await }, |x| {
|
||||
cosmic::Action::App(Message::AddLibrary(x))
|
||||
})
|
||||
}
|
||||
|
||||
fn search(&self, query: String) -> Task<Message> {
|
||||
self.library.clone().map_or_else(Task::none, |library| {
|
||||
if let Some(library) = self.library.clone() {
|
||||
Task::perform(
|
||||
async move { library.search_items(query).await },
|
||||
|items| {
|
||||
|
|
@ -1849,19 +1822,9 @@ where
|
|||
))
|
||||
},
|
||||
)
|
||||
})
|
||||
// if let Some(library) = self.library.clone() {
|
||||
// Task::perform(
|
||||
// async move { library.search_items(query).await },
|
||||
// |items| {
|
||||
// cosmic::Action::App(Message::UpdateSearchResults(
|
||||
// items,
|
||||
// ))
|
||||
// },
|
||||
// )
|
||||
// } else {
|
||||
// Task::none()
|
||||
// }
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
fn process_key_press(
|
||||
|
|
@ -1935,7 +1898,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn service_list(&self) -> Element<Message> {
|
||||
let list =
|
||||
self.service.iter().enumerate().map(|(index, item)| {
|
||||
|
|
@ -2208,30 +2170,13 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn add_library() -> Task<Message> {
|
||||
Task::perform(async move { Library::new().await }, |x| {
|
||||
cosmic::Action::App(Message::AddLibrary(x))
|
||||
})
|
||||
}
|
||||
|
||||
async fn save_as_dialog() -> Result<PathBuf> {
|
||||
let dialog = save::Dialog::new();
|
||||
|
||||
save::file(dialog).await.into_diagnostic().map(|response| {
|
||||
response.url().map_or_else(
|
||||
|| Err(miette!("Can't convert url of file to a path")),
|
||||
|url| {
|
||||
Ok(url.to_file_path().expect("Should be a file here"))
|
||||
},
|
||||
|url| Ok(url.to_file_path().unwrap()),
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
async fn open_dialog() -> Result<PathBuf> {
|
||||
let dialog = open::Dialog::new();
|
||||
open::file(dialog).await.into_diagnostic().map(|response| {
|
||||
response.url().to_file_path().map_err(|e| {
|
||||
miette!("Can't convert to file path: {:?}", e)
|
||||
})
|
||||
})?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::{io, path::PathBuf};
|
|||
|
||||
use crate::core::images::Image;
|
||||
use cosmic::{
|
||||
Apply, Element, Task,
|
||||
Element, Task,
|
||||
dialog::file_chooser::{FileFilter, open::Dialog},
|
||||
iced::{Length, alignment::Vertical},
|
||||
iced_widget::{column, row},
|
||||
|
|
@ -52,7 +52,7 @@ impl ImageEditor {
|
|||
self.update_entire_image(&image);
|
||||
}
|
||||
Message::ChangeTitle(title) => {
|
||||
self.title.clone_from(&title);
|
||||
self.title = title.clone();
|
||||
if let Some(image) = &self.image {
|
||||
let mut image = image.clone();
|
||||
image.title = title;
|
||||
|
|
@ -77,11 +77,13 @@ impl ImageEditor {
|
|||
let task = Task::perform(
|
||||
pick_image(),
|
||||
move |image_result| {
|
||||
image_result.map_or(Message::None, |image| {
|
||||
if let Ok(image) = image_result {
|
||||
let mut image = Image::from(image);
|
||||
image.id = image_id;
|
||||
Message::Update(image)
|
||||
})
|
||||
} else {
|
||||
Message::None
|
||||
}
|
||||
},
|
||||
);
|
||||
return Action::Task(task);
|
||||
|
|
@ -93,10 +95,12 @@ impl ImageEditor {
|
|||
|
||||
#[must_use]
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
let container = self.image.as_ref().map_or_else(
|
||||
|| Space::new(0, 0).apply(container),
|
||||
|pic| widget::image(pic.path.clone()).apply(container),
|
||||
);
|
||||
let container = if let Some(pic) = &self.image {
|
||||
let image = widget::image(pic.path.clone());
|
||||
container(image)
|
||||
} else {
|
||||
container(Space::new(0, 0))
|
||||
};
|
||||
let column = column![
|
||||
self.toolbar(),
|
||||
container.center_x(Length::FillPortion(2))
|
||||
|
|
@ -135,7 +139,7 @@ impl ImageEditor {
|
|||
|
||||
fn update_entire_image(&mut self, image: &Image) {
|
||||
self.image = Some(image.clone());
|
||||
self.title.clone_from(&image.title);
|
||||
self.title = image.title.clone();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,9 +167,7 @@ async fn pick_image() -> Result<PathBuf, ImageError> {
|
|||
error!(?e);
|
||||
ImageError::DialogClosed
|
||||
})
|
||||
.map(|file| {
|
||||
file.url().to_file_path().expect("Should be a file here")
|
||||
})
|
||||
.map(|file| file.url().to_file_path().unwrap())
|
||||
// rfd::AsyncFileDialog::new()
|
||||
// .set_title("Choose a background...")
|
||||
// .add_filter(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ use crate::core::{
|
|||
videos::{self, Video, add_video_to_db, update_video_in_db},
|
||||
};
|
||||
|
||||
#[allow(clippy::struct_field_names)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Library {
|
||||
song_library: Model<Song>,
|
||||
|
|
@ -71,7 +70,6 @@ impl MenuAction for MenuMessage {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Action {
|
||||
OpenItem(Option<(LibraryKind, i32)>),
|
||||
DraggedItem(ServiceItem),
|
||||
|
|
@ -106,7 +104,6 @@ pub enum Message {
|
|||
AddImages(Option<Vec<Image>>),
|
||||
AddVideos(Option<Vec<Video>>),
|
||||
AddPresentations(Option<Vec<Presentation>>),
|
||||
AddPresentationSplit(Option<Presentation>),
|
||||
}
|
||||
|
||||
impl<'a> Library {
|
||||
|
|
@ -140,10 +137,6 @@ impl<'a> Library {
|
|||
self.song_library.get_item(index)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::match_same_arms)]
|
||||
pub fn update(&'a mut self, message: Message) -> Action {
|
||||
match message {
|
||||
Message::None => (),
|
||||
|
|
@ -255,38 +248,8 @@ impl<'a> Library {
|
|||
Task::batch(tasks).chain(after_task),
|
||||
);
|
||||
}
|
||||
Message::AddPresentationSplit(presentation) => {
|
||||
debug!(?presentation, "adding to db");
|
||||
if let Some(presentation) = presentation {
|
||||
if let Err(e) = self
|
||||
.presentation_library
|
||||
.add_item(presentation.clone())
|
||||
{
|
||||
error!(?e);
|
||||
}
|
||||
return Action::Task(
|
||||
Task::future(self.db.acquire()).and_then(
|
||||
move |db| {
|
||||
Task::perform(
|
||||
add_presentation_to_db(
|
||||
presentation.clone(),
|
||||
db,
|
||||
),
|
||||
move |res| {
|
||||
debug!("added to db");
|
||||
if let Err(e) = res {
|
||||
error!(?e);
|
||||
}
|
||||
Message::None
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Message::AddPresentations(presentations) => {
|
||||
debug!(?presentations, "adding to db");
|
||||
debug!(?presentations);
|
||||
let mut index = self.presentation_library.items.len();
|
||||
// Check if empty
|
||||
let mut tasks = Vec::new();
|
||||
|
|
@ -628,7 +591,7 @@ impl<'a> Library {
|
|||
}
|
||||
Message::VideoChanged => debug!("vid shoulda changed"),
|
||||
Message::UpdatePresentation(presentation) => {
|
||||
let Some((kind, _index)) = self.editing_item else {
|
||||
let Some((kind, index)) = self.editing_item else {
|
||||
error!("Not editing an item");
|
||||
return Action::None;
|
||||
};
|
||||
|
|
@ -637,14 +600,6 @@ impl<'a> Library {
|
|||
error!("Not editing a presentation item");
|
||||
return Action::None;
|
||||
}
|
||||
let index = self
|
||||
.presentation_library
|
||||
.items
|
||||
.iter()
|
||||
.position(|pres| pres.id == presentation.id)
|
||||
.unwrap_or_default()
|
||||
.try_into()
|
||||
.unwrap_or_default();
|
||||
|
||||
match self
|
||||
.presentation_library
|
||||
|
|
@ -697,7 +652,6 @@ impl<'a> Library {
|
|||
Message::AddFiles(items) => {
|
||||
let mut tasks = Vec::new();
|
||||
let last_item = &items.last();
|
||||
|
||||
let after_task = match last_item {
|
||||
Some(ServiceItemKind::Image(_image)) => {
|
||||
Task::done(Message::OpenItem(Some((
|
||||
|
|
@ -942,7 +896,6 @@ impl<'a> Library {
|
|||
container(library_dnd).padding(2).into()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn library_item<T>(
|
||||
&'a self,
|
||||
model: &'a Model<T>,
|
||||
|
|
@ -1000,28 +953,18 @@ impl<'a> Library {
|
|||
.style(|t| {
|
||||
container::Style::default()
|
||||
.background({
|
||||
self.library_hovered.map_or_else(
|
||||
|| {
|
||||
Background::Color(
|
||||
t.cosmic().button.base.into(),
|
||||
)
|
||||
},
|
||||
|library| {
|
||||
Background::Color(
|
||||
if library == model.kind {
|
||||
t.cosmic()
|
||||
.button
|
||||
.hover
|
||||
.into()
|
||||
} else {
|
||||
t.cosmic()
|
||||
.button
|
||||
.base
|
||||
.into()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
match self.library_hovered {
|
||||
Some(lib) => Background::Color(
|
||||
if lib == model.kind {
|
||||
t.cosmic().button.hover.into()
|
||||
} else {
|
||||
t.cosmic().button.base.into()
|
||||
},
|
||||
),
|
||||
None => Background::Color(
|
||||
t.cosmic().button.base.into(),
|
||||
),
|
||||
}
|
||||
})
|
||||
.border(Border::default().rounded(
|
||||
t.cosmic().corner_radii.radius_s,
|
||||
|
|
@ -1044,7 +987,6 @@ impl<'a> Library {
|
|||
column({
|
||||
model.items.iter().enumerate().map(
|
||||
|(index, item)| {
|
||||
let i32_index = i32::try_from(index).expect("shouldn't be negative");
|
||||
let kind = model.kind;
|
||||
let visual_item = self
|
||||
.single_item(index, item, model)
|
||||
|
|
@ -1055,21 +997,21 @@ impl<'a> Library {
|
|||
let mouse_area = mouse_area.on_enter(Message::HoverItem(
|
||||
Some((
|
||||
model.kind,
|
||||
i32_index ,
|
||||
index as i32,
|
||||
)),
|
||||
))
|
||||
.on_double_click(
|
||||
Message::OpenItem(Some((
|
||||
model.kind,
|
||||
i32_index,
|
||||
index as i32,
|
||||
))),
|
||||
)
|
||||
.on_right_press(Message::OpenContext(i32_index ))
|
||||
.on_right_press(Message::OpenContext(index as i32))
|
||||
.on_exit(Message::HoverItem(None))
|
||||
.on_press(Message::SelectItem(
|
||||
Some((
|
||||
model.kind,
|
||||
i32_index,
|
||||
index as i32,
|
||||
)),
|
||||
));
|
||||
|
||||
|
|
@ -1096,7 +1038,7 @@ impl<'a> Library {
|
|||
)
|
||||
}})
|
||||
.drag_content(move || {
|
||||
KindWrapper((kind, i32_index))
|
||||
KindWrapper((kind, index as i32))
|
||||
})
|
||||
.into()
|
||||
},
|
||||
|
|
@ -1123,7 +1065,6 @@ impl<'a> Library {
|
|||
column![library_button, lib_container].into()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn single_item<T>(
|
||||
&'a self,
|
||||
index: usize,
|
||||
|
|
@ -1143,28 +1084,30 @@ impl<'a> Library {
|
|||
.center_x(Length::Fill);
|
||||
let subtext = container(responsive(move |size| {
|
||||
let color: Color = if item.background().is_some() {
|
||||
if let Some(items) = &self.selected_items
|
||||
&& items.contains(&(
|
||||
model.kind,
|
||||
i32::try_from(index)
|
||||
.expect("Should never be negative"),
|
||||
))
|
||||
{
|
||||
theme::active().cosmic().control_0().into()
|
||||
if let Some(items) = &self.selected_items {
|
||||
if items.contains(&(model.kind, index as i32)) {
|
||||
theme::active().cosmic().control_0().into()
|
||||
} else {
|
||||
theme::active()
|
||||
.cosmic()
|
||||
.accent_text_color()
|
||||
.into()
|
||||
}
|
||||
} else {
|
||||
theme::active()
|
||||
.cosmic()
|
||||
.accent_text_color()
|
||||
.into()
|
||||
}
|
||||
} else if let Some(items) = &self.selected_items
|
||||
&& items.contains(&(
|
||||
model.kind,
|
||||
i32::try_from(index)
|
||||
.expect("Should never be negative"),
|
||||
))
|
||||
{
|
||||
theme::active().cosmic().control_0().into()
|
||||
} else if let Some(items) = &self.selected_items {
|
||||
if items.contains(&(model.kind, index as i32)) {
|
||||
theme::active().cosmic().control_0().into()
|
||||
} else {
|
||||
theme::active()
|
||||
.cosmic()
|
||||
.destructive_text_color()
|
||||
.into()
|
||||
}
|
||||
} else {
|
||||
theme::active()
|
||||
.cosmic()
|
||||
|
|
@ -1192,16 +1135,15 @@ impl<'a> Library {
|
|||
.style(move |t| {
|
||||
container::Style::default()
|
||||
.background(Background::Color(
|
||||
if let Some(items) = &self.selected_items
|
||||
&& let Ok(index) = i32::try_from(index)
|
||||
{
|
||||
if items.contains(&(model.kind, index)) {
|
||||
if let Some(items) = &self.selected_items {
|
||||
if items.contains(&(model.kind, index as i32))
|
||||
{
|
||||
t.cosmic().accent.selected.into()
|
||||
} else if let Some((library, hovered)) =
|
||||
self.hovered_item
|
||||
{
|
||||
if model.kind == library
|
||||
&& hovered == index
|
||||
&& hovered == index as i32
|
||||
{
|
||||
t.cosmic().button.hover.into()
|
||||
} else {
|
||||
|
|
@ -1212,9 +1154,10 @@ impl<'a> Library {
|
|||
}
|
||||
} else if let Some((library, hovered)) =
|
||||
self.hovered_item
|
||||
&& let Ok(index) = i32::try_from(index)
|
||||
{
|
||||
if model.kind == library && hovered == index {
|
||||
if model.kind == library
|
||||
&& hovered == index as i32
|
||||
{
|
||||
t.cosmic().button.hover.into()
|
||||
} else {
|
||||
t.cosmic().button.base.into()
|
||||
|
|
@ -1266,36 +1209,45 @@ impl<'a> Library {
|
|||
query: String,
|
||||
) -> Vec<ServiceItemKind> {
|
||||
let query = query.to_lowercase();
|
||||
let items = self
|
||||
let mut items: Vec<ServiceItemKind> = self
|
||||
.song_library
|
||||
.items
|
||||
.iter()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|song| song.title.to_lowercase().contains(&query))
|
||||
.map(|song| ServiceItemKind::Song(song.clone()));
|
||||
let videos = self
|
||||
.map(ServiceItemKind::Song)
|
||||
.collect();
|
||||
let videos: Vec<ServiceItemKind> = self
|
||||
.video_library
|
||||
.items
|
||||
.iter()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|vid| vid.title.to_lowercase().contains(&query))
|
||||
.map(|video| ServiceItemKind::Video(video.clone()));
|
||||
let images = self
|
||||
.map(ServiceItemKind::Video)
|
||||
.collect();
|
||||
let images: Vec<ServiceItemKind> = self
|
||||
.image_library
|
||||
.items
|
||||
.iter()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|image| {
|
||||
image.title.to_lowercase().contains(&query)
|
||||
})
|
||||
.map(|image| ServiceItemKind::Image(image.clone()));
|
||||
let presentations = self
|
||||
.map(ServiceItemKind::Image)
|
||||
.collect();
|
||||
let presentations: Vec<ServiceItemKind> = self
|
||||
.presentation_library
|
||||
.items
|
||||
.iter()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|pres| pres.title.to_lowercase().contains(&query))
|
||||
.map(|pres| ServiceItemKind::Presentation(pres.clone()));
|
||||
let items = items.chain(videos);
|
||||
let items = items.chain(images);
|
||||
let items = items.chain(presentations);
|
||||
.map(ServiceItemKind::Presentation)
|
||||
.collect();
|
||||
items.extend(videos);
|
||||
items.extend(images);
|
||||
items.extend(presentations);
|
||||
let mut items: Vec<(usize, ServiceItemKind)> = items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
(
|
||||
levenshtein::distance(
|
||||
|
|
@ -1307,7 +1259,7 @@ impl<'a> Library {
|
|||
})
|
||||
.collect();
|
||||
|
||||
items.sort_by_key(|a| a.0);
|
||||
items.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
items.into_iter().map(|item| item.1).collect()
|
||||
}
|
||||
|
||||
|
|
@ -1336,14 +1288,13 @@ impl<'a> Library {
|
|||
self.modifiers_pressed = modifiers;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn delete_items(&mut self) -> Action {
|
||||
// Need to make this function collect tasks to be run off of
|
||||
// who should be deleted
|
||||
let Some(items) = self.selected_items.as_mut() else {
|
||||
return Action::None;
|
||||
};
|
||||
items.sort_by_key(|(_, index)| *index);
|
||||
items.sort_by(|(_, index), (_, other)| index.cmp(other));
|
||||
let tasks: Vec<Task<Message>> = items
|
||||
.iter()
|
||||
.rev()
|
||||
|
|
@ -1531,20 +1482,14 @@ async fn add_presentations() -> Option<Vec<Presentation>> {
|
|||
}
|
||||
|
||||
async fn add_db() -> Result<SqlitePool> {
|
||||
let mut data = dirs::data_local_dir()
|
||||
.expect("Should always find a data dir");
|
||||
let mut data = dirs::data_local_dir().unwrap();
|
||||
data.push("lumina");
|
||||
data.push("library-db.sqlite3");
|
||||
let mut db_url = String::from("sqlite://");
|
||||
db_url.push_str(
|
||||
data.to_str().expect("Should always be a file here"),
|
||||
);
|
||||
db_url.push_str(data.to_str().unwrap());
|
||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn elide_text(text: impl AsRef<str>, width: f32) -> String {
|
||||
const CHAR_SIZE: f32 = 8.0;
|
||||
let text: String = text.as_ref().to_owned();
|
||||
|
|
@ -1552,25 +1497,10 @@ pub fn elide_text(text: impl AsRef<str>, width: f32) -> String {
|
|||
if text_length > width {
|
||||
format!(
|
||||
"{}...",
|
||||
if let Some((first, _second)) = text.split_at_checked(
|
||||
text.split_at(
|
||||
((width / CHAR_SIZE) - 3.0).floor() as usize
|
||||
) {
|
||||
first
|
||||
} else if let Some((first, _second)) = text
|
||||
.split_at_checked(
|
||||
((width / CHAR_SIZE) - 5.0).floor() as usize
|
||||
)
|
||||
{
|
||||
first
|
||||
} else if let Some((first, _second)) = text
|
||||
.split_at_checked(
|
||||
((width / CHAR_SIZE) - 7.0).floor() as usize
|
||||
)
|
||||
{
|
||||
first
|
||||
} else {
|
||||
&text
|
||||
}
|
||||
)
|
||||
.0
|
||||
)
|
||||
} else {
|
||||
text
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::core::model::LibraryKind;
|
||||
|
||||
// pub mod double_ended_slider;
|
||||
pub mod double_ended_slider;
|
||||
pub mod image_editor;
|
||||
pub mod library;
|
||||
pub mod presentation_editor;
|
||||
|
|
|
|||
|
|
@ -95,7 +95,6 @@ impl PresentationEditor {
|
|||
context_menu_id: None,
|
||||
}
|
||||
}
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn update(&mut self, message: Message) -> Action {
|
||||
match message {
|
||||
Message::ChangePresentation(presentation) => {
|
||||
|
|
@ -123,7 +122,7 @@ impl PresentationEditor {
|
|||
}
|
||||
}
|
||||
Message::ChangeTitle(title) => {
|
||||
self.title.clone_from(&title);
|
||||
self.title = title.clone();
|
||||
if let Some(presentation) = &self.presentation {
|
||||
let mut presentation = presentation.clone();
|
||||
presentation.title = title;
|
||||
|
|
@ -148,17 +147,17 @@ impl PresentationEditor {
|
|||
let task = Task::perform(
|
||||
pick_presentation(),
|
||||
move |presentation_result| {
|
||||
presentation_result.map_or(
|
||||
Message::None,
|
||||
|presentation| {
|
||||
let mut presentation =
|
||||
Presentation::from(presentation);
|
||||
presentation.id = presentation_id;
|
||||
Message::ChangePresentationFile(
|
||||
presentation,
|
||||
)
|
||||
},
|
||||
)
|
||||
if let Ok(presentation) = presentation_result
|
||||
{
|
||||
let mut presentation =
|
||||
Presentation::from(presentation);
|
||||
presentation.id = presentation_id;
|
||||
Message::ChangePresentationFile(
|
||||
presentation,
|
||||
)
|
||||
} else {
|
||||
Message::None
|
||||
}
|
||||
},
|
||||
);
|
||||
return Action::Task(task);
|
||||
|
|
@ -192,22 +191,21 @@ impl PresentationEditor {
|
|||
}
|
||||
}
|
||||
Message::AddSlides(slides) => {
|
||||
debug!(?slides);
|
||||
self.slides = slides;
|
||||
}
|
||||
Message::None => (),
|
||||
Message::NextPage => {
|
||||
let next_index =
|
||||
self.current_slide_index.unwrap_or_default() + 1;
|
||||
|
||||
let last_index = if let Some(presentation) =
|
||||
self.presentation.as_ref()
|
||||
let mut last_index =
|
||||
self.page_count.unwrap_or_default();
|
||||
if let Some(presentation) = self.presentation.as_ref()
|
||||
&& let PresKind::Pdf { ending_index, .. } =
|
||||
presentation.kind
|
||||
{
|
||||
ending_index
|
||||
} else {
|
||||
self.page_count.unwrap_or_default()
|
||||
};
|
||||
last_index = ending_index;
|
||||
}
|
||||
|
||||
if next_index > last_index {
|
||||
return Action::None;
|
||||
|
|
@ -243,16 +241,14 @@ impl PresentationEditor {
|
|||
Message::PrevPage => {
|
||||
let previous_index =
|
||||
self.current_slide_index.unwrap_or_default() - 1;
|
||||
|
||||
let first_index = if let Some(presentation) =
|
||||
self.presentation.as_ref()
|
||||
let mut first_index =
|
||||
self.page_count.unwrap_or_default();
|
||||
if let Some(presentation) = self.presentation.as_ref()
|
||||
&& let PresKind::Pdf { starting_index, .. } =
|
||||
presentation.kind
|
||||
{
|
||||
starting_index
|
||||
} else {
|
||||
self.page_count.unwrap_or_default()
|
||||
};
|
||||
first_index = starting_index;
|
||||
}
|
||||
|
||||
if previous_index < first_index {
|
||||
return Action::None;
|
||||
|
|
@ -283,9 +279,8 @@ impl PresentationEditor {
|
|||
Message::ChangeSlide(index) => {
|
||||
self.current_slide =
|
||||
self.document.as_ref().and_then(|doc| {
|
||||
let page = doc
|
||||
.load_page(i32::try_from(index).ok()?)
|
||||
.ok()?;
|
||||
let page =
|
||||
doc.load_page(index as i32).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page
|
||||
|
|
@ -303,17 +298,16 @@ impl PresentationEditor {
|
|||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide_index = i32::try_from(index).ok();
|
||||
self.current_slide_index = Some(index as i32);
|
||||
}
|
||||
Message::HoverSlide(slide) => {
|
||||
self.hovered_slide = slide;
|
||||
}
|
||||
Message::ContextMenu(index) => {
|
||||
self.context_menu_id = i32::try_from(index).ok();
|
||||
self.context_menu_id = Some(index as i32);
|
||||
}
|
||||
Message::SplitBefore => {
|
||||
if let Ok((first, second)) = self.split_before() {
|
||||
debug!(?first, ?second);
|
||||
self.update_entire_presentation(&first);
|
||||
return Action::SplitAddPresentation((
|
||||
first, second,
|
||||
|
|
@ -322,7 +316,6 @@ impl PresentationEditor {
|
|||
}
|
||||
Message::SplitAfter => {
|
||||
if let Ok((first, second)) = self.split_after() {
|
||||
debug!(?first, ?second);
|
||||
self.update_entire_presentation(&first);
|
||||
return Action::SplitAddPresentation((
|
||||
first, second,
|
||||
|
|
@ -334,74 +327,66 @@ impl PresentationEditor {
|
|||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
let presentation = self.current_slide.as_ref().map_or_else(
|
||||
|| container(Space::new(0, 0)),
|
||||
|slide| {
|
||||
container(
|
||||
widget::image(slide)
|
||||
.content_fit(ContentFit::ScaleDown),
|
||||
)
|
||||
.style(|_| {
|
||||
container::background(Background::Color(
|
||||
cosmic::iced::Color::WHITE,
|
||||
))
|
||||
let presentation = if let Some(slide) = &self.current_slide {
|
||||
container(
|
||||
widget::image(slide)
|
||||
.content_fit(ContentFit::ScaleDown),
|
||||
)
|
||||
.style(|_| {
|
||||
container::background(Background::Color(
|
||||
cosmic::iced::Color::WHITE,
|
||||
))
|
||||
})
|
||||
} else {
|
||||
container(Space::new(0, 0))
|
||||
};
|
||||
let pdf_pages: Vec<Element<Message>> = if let Some(pages) =
|
||||
&self.slides
|
||||
{
|
||||
pages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, page)| {
|
||||
let image = widget::image(page)
|
||||
.height(theme::spacing().space_xxxl * 3)
|
||||
.content_fit(ContentFit::ScaleDown);
|
||||
let slide = container(image).style(|_| {
|
||||
container::background(Background::Color(
|
||||
cosmic::iced::Color::WHITE,
|
||||
))
|
||||
});
|
||||
let clickable_slide = container(
|
||||
mouse_area(slide)
|
||||
.on_enter(Message::HoverSlide(Some(
|
||||
index as i32,
|
||||
)))
|
||||
.on_exit(Message::HoverSlide(None))
|
||||
.on_right_press(Message::ContextMenu(
|
||||
index,
|
||||
))
|
||||
.on_press(Message::ChangeSlide(index)),
|
||||
)
|
||||
.padding(theme::spacing().space_m)
|
||||
.clip(true)
|
||||
.class(
|
||||
if let Some(hovered_index) =
|
||||
self.hovered_slide
|
||||
{
|
||||
if index as i32 == hovered_index {
|
||||
theme::Container::Primary
|
||||
} else {
|
||||
theme::Container::Card
|
||||
}
|
||||
} else {
|
||||
theme::Container::Card
|
||||
},
|
||||
);
|
||||
clickable_slide.into()
|
||||
})
|
||||
},
|
||||
);
|
||||
let pdf_pages: Vec<Element<Message>> =
|
||||
self.slides.as_ref().map_or_else(
|
||||
|| vec![horizontal_space().into()],
|
||||
|pages| {
|
||||
pages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, page)| {
|
||||
let image = widget::image(page)
|
||||
.height(
|
||||
theme::spacing().space_xxxl * 3,
|
||||
)
|
||||
.content_fit(ContentFit::ScaleDown);
|
||||
let slide = container(image).style(|_| {
|
||||
container::background(Background::Color(
|
||||
cosmic::iced::Color::WHITE,
|
||||
))
|
||||
});
|
||||
let clickable_slide = container(
|
||||
mouse_area(slide)
|
||||
.on_enter(Message::HoverSlide(
|
||||
i32::try_from(index).ok(),
|
||||
))
|
||||
.on_exit(Message::HoverSlide(
|
||||
None,
|
||||
))
|
||||
.on_right_press(
|
||||
Message::ContextMenu(index),
|
||||
)
|
||||
.on_press(Message::ChangeSlide(
|
||||
index,
|
||||
)),
|
||||
)
|
||||
.padding(theme::spacing().space_m)
|
||||
.clip(true)
|
||||
.class(self.hovered_slide.map_or(
|
||||
theme::Container::Card,
|
||||
|hovered_index| {
|
||||
if i32::try_from(index).is_ok_and(
|
||||
|index| {
|
||||
index == hovered_index
|
||||
},
|
||||
) {
|
||||
theme::Container::Primary
|
||||
} else {
|
||||
theme::Container::Card
|
||||
}
|
||||
},
|
||||
));
|
||||
clickable_slide.into()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
.collect()
|
||||
} else {
|
||||
vec![horizontal_space().into()]
|
||||
};
|
||||
let pages_column = container(
|
||||
self.context_menu(
|
||||
scrollable(
|
||||
|
|
@ -431,18 +416,13 @@ impl PresentationEditor {
|
|||
}
|
||||
|
||||
fn toolbar(&self) -> Element<Message> {
|
||||
let title_box = text_input(
|
||||
"Title...",
|
||||
self.presentation
|
||||
.as_ref()
|
||||
.map_or("", |presentation| &presentation.title),
|
||||
)
|
||||
.on_input(Message::ChangeTitle);
|
||||
let title_box = text_input("Title...", &self.title)
|
||||
.on_input(Message::ChangeTitle);
|
||||
|
||||
let presentation_selector = button::icon(
|
||||
icon::from_name("folder-presentations-symbolic").scale(2),
|
||||
)
|
||||
.label("Change Presentation")
|
||||
.label("Presentation")
|
||||
.tooltip("Select a presentation")
|
||||
.on_press(Message::PickPresentation)
|
||||
.padding(10);
|
||||
|
|
@ -505,7 +485,7 @@ impl PresentationEditor {
|
|||
presentation: &Presentation,
|
||||
) {
|
||||
self.presentation = Some(presentation.clone());
|
||||
self.title.clone_from(&presentation.title);
|
||||
self.title = presentation.title.clone();
|
||||
self.document =
|
||||
Document::open(&presentation.path.as_path()).ok();
|
||||
self.page_count = self
|
||||
|
|
@ -513,51 +493,21 @@ impl PresentationEditor {
|
|||
.as_ref()
|
||||
.and_then(|doc| doc.page_count().ok());
|
||||
warn!("changing presentation");
|
||||
let pages = if let PresKind::Pdf {
|
||||
starting_index,
|
||||
ending_index,
|
||||
} = presentation.kind
|
||||
{
|
||||
self.current_slide =
|
||||
self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(starting_index).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page
|
||||
.to_pixmap(&matrix, &colorspace, true, true)
|
||||
.ok()?;
|
||||
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(0).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page
|
||||
.to_pixmap(&matrix, &colorspace, true, true)
|
||||
.ok()?;
|
||||
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide_index = Some(starting_index);
|
||||
get_pages(
|
||||
starting_index..=ending_index,
|
||||
presentation.path.clone(),
|
||||
)
|
||||
} else {
|
||||
self.current_slide =
|
||||
self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(0).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page
|
||||
.to_pixmap(&matrix, &colorspace, true, true)
|
||||
.ok()?;
|
||||
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide_index = Some(0);
|
||||
get_pages(.., presentation.path.clone())
|
||||
};
|
||||
self.slides = pages;
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide_index = Some(0);
|
||||
}
|
||||
|
||||
fn split_before(&self) -> Result<(Presentation, Presentation)> {
|
||||
|
|
@ -583,10 +533,7 @@ impl PresentationEditor {
|
|||
};
|
||||
let second_presentation = Presentation {
|
||||
id: 0,
|
||||
title: format!(
|
||||
"{} (2)",
|
||||
current_presentation.title.clone()
|
||||
),
|
||||
title: current_presentation.title.clone(),
|
||||
path: current_presentation.path.clone(),
|
||||
kind: match current_presentation.kind {
|
||||
PresKind::Pdf { ending_index, .. } => {
|
||||
|
|
@ -630,10 +577,7 @@ impl PresentationEditor {
|
|||
};
|
||||
let second_presentation = Presentation {
|
||||
id: 0,
|
||||
title: format!(
|
||||
"{} (2)",
|
||||
current_presentation.title.clone()
|
||||
),
|
||||
title: current_presentation.title.clone(),
|
||||
path: current_presentation.path.clone(),
|
||||
kind: match current_presentation.kind {
|
||||
PresKind::Pdf { ending_index, .. } => {
|
||||
|
|
@ -671,9 +615,7 @@ fn get_pages(
|
|||
pages
|
||||
.enumerate()
|
||||
.filter_map(|(index, page)| {
|
||||
if !range.contains(&i32::try_from(index).expect(
|
||||
"looking for a pdf index that is way too large",
|
||||
)) {
|
||||
if !range.contains(&(index as i32)) {
|
||||
return None;
|
||||
}
|
||||
let page = page.ok()?;
|
||||
|
|
@ -707,9 +649,7 @@ async fn pick_presentation() -> Result<PathBuf, PresentationError> {
|
|||
error!(?e);
|
||||
PresentationError::DialogClosed
|
||||
})
|
||||
.map(|file| {
|
||||
file.url().to_file_path().expect("Should be a file here")
|
||||
})
|
||||
.map(|file| file.url().to_file_path().unwrap())
|
||||
// rfd::AsyncFileDialog::new()
|
||||
// .set_title("Choose a background...")
|
||||
// .add_filter(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ use cosmic::{
|
|||
menu, mouse_area, responsive, scrollable, text,
|
||||
},
|
||||
};
|
||||
use derive_more::Debug;
|
||||
use iced_video_player::{Position, Video, VideoPlayer, gst_pbutils};
|
||||
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
|
@ -42,7 +41,7 @@ use crate::{
|
|||
},
|
||||
};
|
||||
|
||||
// const REFERENCE_WIDTH: f32 = 1920.0;
|
||||
const REFERENCE_WIDTH: f32 = 1920.0;
|
||||
static DEFAULT_SLIDE: LazyLock<Slide> = LazyLock::new(Slide::default);
|
||||
|
||||
// #[derive(Default, Clone, Debug)]
|
||||
|
|
@ -51,6 +50,7 @@ pub(crate) struct Presenter {
|
|||
pub current_slide: Slide,
|
||||
pub current_item: usize,
|
||||
pub current_slide_index: usize,
|
||||
pub absolute_slide_index: usize,
|
||||
pub total_slides: usize,
|
||||
pub video: Option<Video>,
|
||||
pub video_position: f32,
|
||||
|
|
@ -74,8 +74,7 @@ pub(crate) enum Action {
|
|||
None,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Message {
|
||||
NextSlide,
|
||||
PrevSlide,
|
||||
|
|
@ -84,7 +83,7 @@ pub(crate) enum Message {
|
|||
ClickSlide(usize, usize),
|
||||
EndVideo,
|
||||
StartVideo,
|
||||
// StartAudio,
|
||||
StartAudio,
|
||||
EndAudio,
|
||||
VideoPos(f32),
|
||||
VideoFrame,
|
||||
|
|
@ -96,19 +95,79 @@ pub(crate) enum Message {
|
|||
RightClickSlide(usize, usize),
|
||||
AssignObsScene(usize),
|
||||
UpdateObsScenes(Vec<Scene>),
|
||||
#[debug("AddObsClient")]
|
||||
AddObsClient(Arc<Client>),
|
||||
AssignSlideAction(slide_actions::Action),
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
impl std::fmt::Debug for Message {
|
||||
fn fmt(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NextSlide => write!(f, "NextSlide"),
|
||||
Self::PrevSlide => write!(f, "PrevSlide"),
|
||||
Self::SlideChange(arg0) => {
|
||||
f.debug_tuple("SlideChange").field(arg0).finish()
|
||||
}
|
||||
Self::ActivateSlide(arg0, arg1) => f
|
||||
.debug_tuple("ActivateSlide")
|
||||
.field(arg0)
|
||||
.field(arg1)
|
||||
.finish(),
|
||||
Self::ClickSlide(arg0, arg1) => f
|
||||
.debug_tuple("ClickSlide")
|
||||
.field(arg0)
|
||||
.field(arg1)
|
||||
.finish(),
|
||||
Self::EndVideo => write!(f, "EndVideo"),
|
||||
Self::StartVideo => write!(f, "StartVideo"),
|
||||
Self::StartAudio => write!(f, "StartAudio"),
|
||||
Self::EndAudio => write!(f, "EndAudio"),
|
||||
Self::VideoPos(arg0) => {
|
||||
f.debug_tuple("VideoPos").field(arg0).finish()
|
||||
}
|
||||
Self::VideoFrame => write!(f, "VideoFrame"),
|
||||
Self::MissingPlugin(arg0) => {
|
||||
f.debug_tuple("MissingPlugin").field(arg0).finish()
|
||||
}
|
||||
Self::HoveredSlide(arg0) => {
|
||||
f.debug_tuple("HoveredSlide").field(arg0).finish()
|
||||
}
|
||||
Self::ChangeFont(arg0) => {
|
||||
f.debug_tuple("ChangeFont").field(arg0).finish()
|
||||
}
|
||||
Self::Error(arg0) => {
|
||||
f.debug_tuple("Error").field(arg0).finish()
|
||||
}
|
||||
Self::None => write!(f, "None"),
|
||||
Self::RightClickSlide(arg0, arg1) => f
|
||||
.debug_tuple("RightClickSlide")
|
||||
.field(arg0)
|
||||
.field(arg1)
|
||||
.finish(),
|
||||
Self::AssignObsScene(arg0) => {
|
||||
f.debug_tuple("ObsSceneAssign").field(arg0).finish()
|
||||
}
|
||||
Self::UpdateObsScenes(arg0) => {
|
||||
f.debug_tuple("UpdateObsScenes").field(arg0).finish()
|
||||
}
|
||||
Self::AddObsClient(_) => write!(f, "AddObsClient"),
|
||||
Self::AssignSlideAction(action) => f
|
||||
.debug_tuple("AssignSlideAction")
|
||||
.field(action)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum MenuAction {
|
||||
ObsSceneAssign(usize),
|
||||
ObsStartStream,
|
||||
ObsStopStream,
|
||||
// ObsStartRecord,
|
||||
// ObsStopRecord,
|
||||
ObsStartRecord,
|
||||
ObsStopRecord,
|
||||
}
|
||||
|
||||
impl menu::Action for MenuAction {
|
||||
|
|
@ -129,14 +188,14 @@ impl menu::Action for MenuAction {
|
|||
action: ObsAction::StopStream,
|
||||
},
|
||||
),
|
||||
// Self::ObsStartRecord => todo!(),
|
||||
// Self::ObsStopRecord => todo!(),
|
||||
Self::ObsStartRecord => todo!(),
|
||||
Self::ObsStopRecord => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Presenter {
|
||||
fn create_video(url: &Url) -> Result<Video> {
|
||||
fn create_video(url: Url) -> Result<Video> {
|
||||
// Based on `iced_video_player::Video::new`,
|
||||
// but without a text sink so that the built-in subtitle functionality triggers.
|
||||
use gstreamer as gst;
|
||||
|
|
@ -159,35 +218,16 @@ impl Presenter {
|
|||
|
||||
let video_sink: gst::Element =
|
||||
pipeline.property("video-sink");
|
||||
let pad =
|
||||
video_sink.pads().first().cloned().expect("first pad");
|
||||
let pad = pad
|
||||
.dynamic_cast::<gst::GhostPad>()
|
||||
.map_err(|_| iced_video_player::Error::Cast)
|
||||
.into_diagnostic()?;
|
||||
let pad = video_sink.pads().first().cloned().unwrap();
|
||||
let pad = pad.dynamic_cast::<gst::GhostPad>().unwrap();
|
||||
let bin = pad
|
||||
.parent_element()
|
||||
.ok_or_else(|| {
|
||||
iced_video_player::Error::AppSink(String::from(
|
||||
"Should have a parent element here",
|
||||
))
|
||||
})
|
||||
.into_diagnostic()?
|
||||
.unwrap()
|
||||
.downcast::<gst::Bin>()
|
||||
.map_err(|_| iced_video_player::Error::Cast)
|
||||
.into_diagnostic()?;
|
||||
let video_sink = bin
|
||||
.by_name("lumina_video")
|
||||
.ok_or_else(|| {
|
||||
iced_video_player::Error::AppSink(String::from(
|
||||
"Can't find element lumina_video",
|
||||
))
|
||||
})
|
||||
.into_diagnostic()?;
|
||||
let video_sink = video_sink
|
||||
.downcast::<gst_app::AppSink>()
|
||||
.map_err(|_| iced_video_player::Error::Cast)
|
||||
.into_diagnostic()?;
|
||||
.unwrap();
|
||||
let video_sink = bin.by_name("lumina_video").unwrap();
|
||||
let video_sink =
|
||||
video_sink.downcast::<gst_app::AppSink>().unwrap();
|
||||
let result =
|
||||
Video::from_gst_pipeline(pipeline, video_sink, None);
|
||||
result.into_diagnostic()
|
||||
|
|
@ -195,29 +235,33 @@ impl Presenter {
|
|||
|
||||
pub fn with_items(items: Vec<ServiceItem>) -> Self {
|
||||
let video = {
|
||||
items.first().and_then(|item| {
|
||||
item.slides.first().and_then(|slide| {
|
||||
if let Some(item) = items.first() {
|
||||
if let Some(slide) = item.slides.first() {
|
||||
let path = slide.background().path.clone();
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
let url = Url::from_file_path(path).expect(
|
||||
"There should be a video file here",
|
||||
);
|
||||
match Video::new(&url) {
|
||||
Ok(mut v) => {
|
||||
v.set_paused(true);
|
||||
Some(v)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Had an error creating the video object: {e}, likely the first slide isn't a video"
|
||||
);
|
||||
None
|
||||
if path.exists() {
|
||||
let url = Url::from_file_path(path).unwrap();
|
||||
let result = Video::new(&url);
|
||||
match result {
|
||||
Ok(mut v) => {
|
||||
v.set_paused(true);
|
||||
Some(v)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Had an error creating the video object: {e}, likely the first slide isn't a video"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let total_slides: usize =
|
||||
items.iter().fold(0, |a, item| a + item.slides.len());
|
||||
|
|
@ -237,6 +281,7 @@ impl Presenter {
|
|||
current_slide: slide.unwrap_or(&DEFAULT_SLIDE).clone(),
|
||||
current_item: 0,
|
||||
current_slide_index: 0,
|
||||
absolute_slide_index: 0,
|
||||
total_slides,
|
||||
video,
|
||||
audio,
|
||||
|
|
@ -263,7 +308,6 @@ impl Presenter {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn update(&mut self, message: Message) -> Action {
|
||||
match message {
|
||||
Message::AddObsClient(client) => {
|
||||
|
|
@ -303,16 +347,17 @@ impl Presenter {
|
|||
self.obs_scenes = Some(scenes);
|
||||
}
|
||||
Message::AssignObsScene(scene_index) => {
|
||||
let slide_id = self.context_menu_id.expect("In this match we should always already have a context menu id");
|
||||
let Some(scenes) = &self.obs_scenes else {
|
||||
return Action::None;
|
||||
};
|
||||
let new_scene = &scenes[scene_index];
|
||||
debug!(?scenes, ?new_scene, "updating obs actions");
|
||||
if let Some(map) = self.slide_action_map.as_mut() {
|
||||
if let Some(actions) = map.get_mut(&slide_id) {
|
||||
if let Some(actions) = map.get_mut(
|
||||
&self.context_menu_id.unwrap_or_default(),
|
||||
) {
|
||||
let mut altered_actions = vec![];
|
||||
for action in actions.iter_mut() {
|
||||
actions.iter_mut().for_each(|action| {
|
||||
match action {
|
||||
slide_actions::Action::Obs {
|
||||
action: ObsAction::Scene { .. },
|
||||
|
|
@ -326,7 +371,7 @@ impl Presenter {
|
|||
_ => altered_actions
|
||||
.push(action.to_owned()),
|
||||
}
|
||||
}
|
||||
});
|
||||
*actions = altered_actions;
|
||||
debug!(
|
||||
"updating the obs scene {:?}",
|
||||
|
|
@ -334,7 +379,7 @@ impl Presenter {
|
|||
);
|
||||
} else if map
|
||||
.insert(
|
||||
slide_id,
|
||||
self.context_menu_id.unwrap(),
|
||||
vec![slide_actions::Action::Obs {
|
||||
action: ObsAction::Scene {
|
||||
scene: new_scene.clone(),
|
||||
|
|
@ -356,7 +401,7 @@ impl Presenter {
|
|||
} else {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(
|
||||
slide_id,
|
||||
self.context_menu_id.unwrap(),
|
||||
vec![slide_actions::Action::Obs {
|
||||
action: ObsAction::Scene {
|
||||
scene: new_scene.clone(),
|
||||
|
|
@ -367,16 +412,23 @@ impl Presenter {
|
|||
}
|
||||
}
|
||||
Message::AssignSlideAction(action) => {
|
||||
let slide_id = self.context_menu_id.expect("In this match we should always already have a context menu id");
|
||||
if let Some(map) = self.slide_action_map.as_mut() {
|
||||
if let Some(actions) = map.get_mut(&slide_id) {
|
||||
if let Some(actions) =
|
||||
map.get_mut(&self.context_menu_id.unwrap())
|
||||
{
|
||||
actions.push(action);
|
||||
} else {
|
||||
map.insert(slide_id, vec![action]);
|
||||
map.insert(
|
||||
self.context_menu_id.unwrap(),
|
||||
vec![action],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(slide_id, vec![action]);
|
||||
map.insert(
|
||||
self.context_menu_id.unwrap(),
|
||||
vec![action],
|
||||
);
|
||||
self.slide_action_map = Some(map);
|
||||
}
|
||||
}
|
||||
|
|
@ -404,10 +456,8 @@ impl Presenter {
|
|||
// self.current_slide_index = slide;
|
||||
debug!("cloning slide...");
|
||||
self.current_slide = slide.clone();
|
||||
let font = slide
|
||||
.font()
|
||||
.map_or_else(String::new, |font| font.get_name());
|
||||
let _ = self.update(Message::ChangeFont(font));
|
||||
let _ =
|
||||
self.update(Message::ChangeFont(slide.font()));
|
||||
debug!("changing video now...");
|
||||
if !backgrounds_match {
|
||||
if let Some(video) = &mut self.video {
|
||||
|
|
@ -440,7 +490,6 @@ impl Presenter {
|
|||
|
||||
debug!(target_item);
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let offset = AbsoluteOffset {
|
||||
x: {
|
||||
if target_item > 2 {
|
||||
|
|
@ -466,7 +515,7 @@ impl Presenter {
|
|||
{
|
||||
if let Some(stripped_audio) = new_audio
|
||||
.to_str()
|
||||
.expect("Should be no problem")
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.strip_prefix(r"file://")
|
||||
{
|
||||
|
|
@ -474,8 +523,35 @@ impl Presenter {
|
|||
}
|
||||
debug!("{:?}", new_audio);
|
||||
if new_audio.exists() {
|
||||
self.audio = Some(new_audio);
|
||||
tasks.push(self.start_audio());
|
||||
let old_audio = self.audio.clone();
|
||||
match old_audio {
|
||||
Some(current_audio)
|
||||
if current_audio != *new_audio =>
|
||||
{
|
||||
debug!(
|
||||
?new_audio,
|
||||
?current_audio,
|
||||
"audio needs to change"
|
||||
);
|
||||
self.audio = Some(new_audio);
|
||||
tasks.push(self.start_audio());
|
||||
}
|
||||
Some(current_audio) => {
|
||||
debug!(
|
||||
?new_audio,
|
||||
?current_audio,
|
||||
"Same audio shouldn't change"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
debug!(
|
||||
?new_audio,
|
||||
"could not find audio, need to change"
|
||||
);
|
||||
self.audio = Some(new_audio);
|
||||
tasks.push(self.start_audio());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.audio = None;
|
||||
self.update(Message::EndAudio);
|
||||
|
|
@ -574,7 +650,7 @@ impl Presenter {
|
|||
Message::None
|
||||
})
|
||||
.await
|
||||
.expect("Spawning a task shouldn't fail")
|
||||
.unwrap()
|
||||
},
|
||||
|x| x,
|
||||
));
|
||||
|
|
@ -582,9 +658,9 @@ impl Presenter {
|
|||
Message::HoveredSlide(slide) => {
|
||||
self.hovered_slide = slide;
|
||||
}
|
||||
// Message::StartAudio => {
|
||||
// return Action::Task(self.start_audio());
|
||||
// }
|
||||
Message::StartAudio => {
|
||||
return Action::Task(self.start_audio());
|
||||
}
|
||||
Message::EndAudio => {
|
||||
self.sink.0.stop();
|
||||
}
|
||||
|
|
@ -597,24 +673,13 @@ impl Presenter {
|
|||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
slide_view(
|
||||
&self.current_slide,
|
||||
self.video.as_ref(),
|
||||
false,
|
||||
true,
|
||||
)
|
||||
slide_view(&self.current_slide, &self.video, false, true)
|
||||
}
|
||||
|
||||
pub fn view_preview(&self) -> Element<Message> {
|
||||
slide_view(
|
||||
&self.current_slide,
|
||||
self.video.as_ref(),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
slide_view(&self.current_slide, &self.video, false, false)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn preview_bar(&self) -> Element<Message> {
|
||||
let mut items = vec![];
|
||||
self.service.iter().enumerate().for_each(
|
||||
|
|
@ -631,7 +696,7 @@ impl Presenter {
|
|||
|
||||
let container = slide_view(
|
||||
slide,
|
||||
self.video.as_ref(),
|
||||
&self.video,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
|
@ -670,18 +735,18 @@ impl Presenter {
|
|||
style.shadow = Shadow {
|
||||
color: Color::BLACK,
|
||||
offset: {
|
||||
if is_current_slide
|
||||
|| hovered
|
||||
{
|
||||
if is_current_slide {
|
||||
Vector::new(5.0, 5.0)
|
||||
} else if hovered {
|
||||
Vector::new(5.0, 5.0)
|
||||
} else {
|
||||
Vector::new(0.0, 0.0)
|
||||
}
|
||||
},
|
||||
blur_radius: {
|
||||
if is_current_slide
|
||||
|| hovered
|
||||
{
|
||||
if is_current_slide {
|
||||
10.0
|
||||
} else if hovered {
|
||||
10.0
|
||||
} else {
|
||||
0.0
|
||||
|
|
@ -869,9 +934,8 @@ impl Presenter {
|
|||
BackgroundKind::Video => {
|
||||
let path = &self.current_slide.background().path;
|
||||
if path.exists() {
|
||||
let url = Url::from_file_path(path)
|
||||
.expect("There should be a video file here");
|
||||
let result = Self::create_video(&url);
|
||||
let url = Url::from_file_path(path).unwrap();
|
||||
let result = Self::create_video(url);
|
||||
match result {
|
||||
Ok(mut v) => {
|
||||
v.set_looping(
|
||||
|
|
@ -948,34 +1012,41 @@ impl Presenter {
|
|||
}
|
||||
}
|
||||
|
||||
// #[allow(clippy::unused_async)]
|
||||
// async fn obs_scene_switch(client: Arc<Client>, scene: Scene) {
|
||||
// match client.scenes().set_current_program_scene(&scene.id).await {
|
||||
// Ok(()) => debug!("Set scene to: {:?}", scene),
|
||||
// Err(e) => error!(?e),
|
||||
// }
|
||||
// }
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn obs_scene_switch(client: Arc<Client>, scene: Scene) {
|
||||
match client.scenes().set_current_program_scene(&scene.id).await {
|
||||
Ok(()) => debug!("Set scene to: {:?}", scene),
|
||||
Err(e) => error!(?e),
|
||||
}
|
||||
}
|
||||
|
||||
// This needs to be async so that rodio's audio will work
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn start_audio(sink: Arc<Sink>, audio: PathBuf) {
|
||||
debug!(?audio);
|
||||
let file = BufReader::new(
|
||||
File::open(audio)
|
||||
.expect("There should be an audio file here"),
|
||||
);
|
||||
let file = BufReader::new(File::open(audio).unwrap());
|
||||
debug!(?file);
|
||||
let source = Decoder::new(file)
|
||||
.expect("There should be an audio decoder here");
|
||||
let source = Decoder::new(file).unwrap();
|
||||
sink.append(source);
|
||||
let empty = sink.empty();
|
||||
let paused = sink.is_paused();
|
||||
debug!(empty, paused, "Finished running");
|
||||
}
|
||||
|
||||
fn scale_font(font_size: f32, width: f32) -> f32 {
|
||||
let scale_factor = (REFERENCE_WIDTH / width).sqrt();
|
||||
// debug!(scale_factor);
|
||||
|
||||
if font_size > 0.0 {
|
||||
font_size / scale_factor
|
||||
} else {
|
||||
50.0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slide_view<'a>(
|
||||
slide: &'a Slide,
|
||||
video: Option<&'a Video>,
|
||||
video: &'a Option<Video>,
|
||||
delegate: bool,
|
||||
hide_mouse: bool,
|
||||
) -> Element<'a, Message> {
|
||||
|
|
@ -1045,17 +1116,16 @@ pub(crate) fn slide_view<'a>(
|
|||
}
|
||||
}
|
||||
BackgroundKind::Html => todo!(),
|
||||
}
|
||||
};
|
||||
if let Some(text) = &slide.text_svg
|
||||
&& let Some(handle) = &text.handle
|
||||
{
|
||||
stack = stack.push(
|
||||
image(handle)
|
||||
.content_fit(ContentFit::Contain)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
);
|
||||
}
|
||||
&& let Some(handle) = &text.handle {
|
||||
stack = stack.push(
|
||||
image(handle)
|
||||
.content_fit(ContentFit::ScaleDown)
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink),
|
||||
);
|
||||
};
|
||||
Container::new(stack).center(Length::Fill).into()
|
||||
})
|
||||
.into()
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub const fn service<Message: Clone + 'static>(
|
|||
}
|
||||
|
||||
pub struct Service<'a, Message> {
|
||||
items: &'a Vec<ServiceItem>,
|
||||
service: &'a Vec<ServiceItem>,
|
||||
on_start: Option<Message>,
|
||||
on_cancelled: Option<Message>,
|
||||
on_finish: Option<Message>,
|
||||
|
|
@ -36,9 +36,9 @@ pub struct Service<'a, Message> {
|
|||
|
||||
impl<'a, Message: Clone + 'static> Service<'a, Message> {
|
||||
#[must_use]
|
||||
pub const fn new(service_items: &'a Vec<ServiceItem>) -> Self {
|
||||
pub const fn new(service: &'a Vec<ServiceItem>) -> Self {
|
||||
Self {
|
||||
items: service_items,
|
||||
service,
|
||||
drag_threshold: 8.0,
|
||||
on_start: None,
|
||||
on_cancelled: None,
|
||||
|
|
@ -93,13 +93,11 @@ impl<'a, Message: Clone + 'static> Service<'a, Message> {
|
|||
// );
|
||||
// }
|
||||
|
||||
#[must_use]
|
||||
pub fn on_start(mut self, on_start: Option<Message>) -> Self {
|
||||
self.on_start = on_start;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn on_cancel(
|
||||
mut self,
|
||||
on_cancelled: Option<Message>,
|
||||
|
|
@ -108,7 +106,6 @@ impl<'a, Message: Clone + 'static> Service<'a, Message> {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn on_finish(mut self, on_finish: Option<Message>) -> Self {
|
||||
self.on_finish = on_finish;
|
||||
self
|
||||
|
|
@ -292,7 +289,7 @@ impl<Message: Clone + 'static>
|
|||
_viewport: &Rectangle,
|
||||
) {
|
||||
// let state = tree.state.downcast_mut::<State>();
|
||||
for _item in self.items {}
|
||||
for _item in self.service {}
|
||||
}
|
||||
|
||||
// fn overlay<'b>(
|
||||
|
|
@ -344,7 +341,7 @@ struct State {
|
|||
hovered: bool,
|
||||
left_pressed_position: Option<Point>,
|
||||
is_dragging: bool,
|
||||
_cached_bounds: Rectangle,
|
||||
cached_bounds: Rectangle,
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ use tracing::debug;
|
|||
|
||||
#[derive(Debug, Default)]
|
||||
struct State {
|
||||
_cache: canvas::Cache,
|
||||
cache: canvas::Cache,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SlideEditor {
|
||||
_state: State,
|
||||
_font: Font,
|
||||
state: State,
|
||||
font: Font,
|
||||
program: EditorProgram,
|
||||
}
|
||||
|
||||
|
|
@ -35,11 +35,11 @@ pub enum Message {
|
|||
}
|
||||
|
||||
pub struct Text {
|
||||
_text: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
pub struct Image {
|
||||
_source: PathBuf,
|
||||
source: PathBuf,
|
||||
}
|
||||
|
||||
pub enum SlideWidget {
|
||||
|
|
@ -55,7 +55,7 @@ pub enum SlideError {
|
|||
|
||||
#[derive(Debug, Default)]
|
||||
struct EditorProgram {
|
||||
_mouse_button_pressed: Option<cosmic::iced::mouse::Button>,
|
||||
mouse_button_pressed: Option<cosmic::iced::mouse::Button>,
|
||||
}
|
||||
|
||||
impl SlideEditor {
|
||||
|
|
@ -74,7 +74,6 @@ impl SlideEditor {
|
|||
|
||||
/// Ensure to use the `cosmic::Theme and cosmic::Renderer` here
|
||||
/// or else it will not compile
|
||||
#[allow(clippy::extra_unused_lifetimes)]
|
||||
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||
for EditorProgram
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,22 +1,20 @@
|
|||
use std::{
|
||||
fmt::{Display, Write},
|
||||
fs,
|
||||
fmt::Display,
|
||||
hash::{Hash, Hasher},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use cosmic::{
|
||||
cosmic_theme::palette::{IntoColor, Srgb, rgb::Rgba},
|
||||
cosmic_theme::palette::Srgb,
|
||||
iced::{
|
||||
ContentFit, Length, Size,
|
||||
font::{Style, Weight},
|
||||
},
|
||||
prelude::*,
|
||||
widget::{Image, Space, image::Handle},
|
||||
widget::{Image, image::Handle},
|
||||
};
|
||||
use derive_more::Debug;
|
||||
use miette::{IntoDiagnostic, Result, miette};
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use rapidhash::v3::rapidhash_v3;
|
||||
use resvg::{
|
||||
tiny_skia::{self, Pixmap},
|
||||
|
|
@ -25,9 +23,9 @@ use resvg::{
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{TextAlignment, core::slide::Slide};
|
||||
use crate::TextAlignment;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TextSvg {
|
||||
text: String,
|
||||
font: Font,
|
||||
|
|
@ -35,11 +33,7 @@ pub struct TextSvg {
|
|||
stroke: Option<Stroke>,
|
||||
fill: Color,
|
||||
alignment: TextAlignment,
|
||||
pub path: Option<PathBuf>,
|
||||
#[serde(skip)]
|
||||
pub handle: Option<Handle>,
|
||||
#[serde(skip)]
|
||||
#[debug(skip)]
|
||||
fontdb: Arc<resvg::usvg::fontdb::Database>,
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +46,6 @@ impl PartialEq for TextSvg {
|
|||
&& self.fill == other.fill
|
||||
&& self.alignment == other.alignment
|
||||
&& self.handle == other.handle
|
||||
&& self.path == other.path
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,13 +57,10 @@ impl Hash for TextSvg {
|
|||
self.stroke.hash(state);
|
||||
self.fill.hash(state);
|
||||
self.alignment.hash(state);
|
||||
self.path.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct Font {
|
||||
name: String,
|
||||
weight: Weight,
|
||||
|
|
@ -152,19 +142,16 @@ impl Font {
|
|||
self.style
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn weight(mut self, weight: impl Into<Weight>) -> Self {
|
||||
self.weight = weight.into();
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Into<Style>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||
self.name = name.into();
|
||||
self
|
||||
|
|
@ -184,12 +171,10 @@ impl Hash for Color {
|
|||
}
|
||||
|
||||
impl Color {
|
||||
#[must_use]
|
||||
pub fn to_css_hex_string(&self) -> String {
|
||||
format!("#{:x}", self.0.into_format::<u8>())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_hex_str(color: impl AsRef<str>) -> Self {
|
||||
let color = color.as_ref();
|
||||
let color: Result<Srgb<u8>> = color.parse().into_diagnostic();
|
||||
|
|
@ -203,19 +188,6 @@ impl Color {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Color {
|
||||
fn from(value: Rgba) -> Self {
|
||||
let rgba: Srgb = value.into_color();
|
||||
Self(rgba)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Srgb> for Color {
|
||||
fn from(value: Srgb) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Color {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::from_hex_str(value)
|
||||
|
|
@ -244,7 +216,6 @@ impl Display for Color {
|
|||
}
|
||||
|
||||
impl TextSvg {
|
||||
#[must_use]
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
|
|
@ -254,43 +225,36 @@ impl TextSvg {
|
|||
|
||||
// pub fn build(self)
|
||||
|
||||
#[must_use]
|
||||
pub fn fill(mut self, color: impl Into<Color>) -> Self {
|
||||
self.fill = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn shadow(mut self, shadow: impl Into<Shadow>) -> Self {
|
||||
self.shadow = Some(shadow.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
self.stroke = Some(stroke.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn font(mut self, font: impl Into<Font>) -> Self {
|
||||
self.font = font.into();
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn text(mut self, text: impl AsRef<str>) -> Self {
|
||||
self.text = text.as_ref().to_string();
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fontdb(mut self, fontdb: Arc<fontdb::Database>) -> Self {
|
||||
self.fontdb = fontdb;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn alignment(
|
||||
mut self,
|
||||
alignment: TextAlignment,
|
||||
|
|
@ -299,38 +263,29 @@ impl TextSvg {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn build(
|
||||
mut self,
|
||||
size: Size,
|
||||
mut cache: Option<PathBuf>,
|
||||
) -> Self {
|
||||
pub fn build(mut self) -> Self {
|
||||
// debug!("starting...");
|
||||
|
||||
let mut path = dirs::data_local_dir().unwrap();
|
||||
path.push(PathBuf::from("lumina"));
|
||||
path.push(PathBuf::from("temp"));
|
||||
|
||||
let mut final_svg = String::with_capacity(1024);
|
||||
|
||||
let font_scale = size.height / 1080.0;
|
||||
let font_size = f32::from(self.font.size) * font_scale;
|
||||
let size = Size::new(1920.0, 1080.0);
|
||||
let font_size = f32::from(self.font.size);
|
||||
let total_lines = self.text.lines().count();
|
||||
let half_lines = (total_lines / 2) as f32;
|
||||
let line_spacing = 10.0;
|
||||
let text_and_line_spacing = font_size + line_spacing;
|
||||
|
||||
let center_y = (size.width / 2.0).to_string();
|
||||
let x_width_padded = (size.width - 10.0).to_string();
|
||||
|
||||
let (text_anchor, starting_y_position, text_x_position) =
|
||||
match self.alignment {
|
||||
TextAlignment::TopLeft => ("start", font_size, "10"),
|
||||
TextAlignment::TopCenter => {
|
||||
("middle", font_size, center_y.as_str())
|
||||
}
|
||||
TextAlignment::TopRight => {
|
||||
("end", font_size, x_width_padded.as_str())
|
||||
("middle", font_size, "990")
|
||||
}
|
||||
TextAlignment::TopRight => ("end", font_size, "1910"),
|
||||
TextAlignment::MiddleLeft => {
|
||||
let middle_position = size.height / 2.0;
|
||||
let position = half_lines.mul_add(
|
||||
|
|
@ -345,7 +300,7 @@ impl TextSvg {
|
|||
-text_and_line_spacing,
|
||||
middle_position,
|
||||
);
|
||||
("middle", position, center_y.as_str())
|
||||
("middle", position, "990")
|
||||
}
|
||||
TextAlignment::MiddleRight => {
|
||||
let middle_position = size.height / 2.0;
|
||||
|
|
@ -353,55 +308,38 @@ impl TextSvg {
|
|||
-text_and_line_spacing,
|
||||
middle_position,
|
||||
);
|
||||
("end", position, x_width_padded.as_str())
|
||||
("end", position, "1910")
|
||||
}
|
||||
TextAlignment::BottomLeft => {
|
||||
let position = (total_lines as f32)
|
||||
.mul_add(-text_and_line_spacing, size.height);
|
||||
let position = size.height
|
||||
- (total_lines as f32
|
||||
* text_and_line_spacing);
|
||||
("start", position, "10")
|
||||
}
|
||||
TextAlignment::BottomCenter => {
|
||||
let position = (total_lines as f32)
|
||||
.mul_add(-text_and_line_spacing, size.height);
|
||||
("middle", position, center_y.as_str())
|
||||
let position = size.height
|
||||
- (total_lines as f32
|
||||
* text_and_line_spacing);
|
||||
("middle", position, "990")
|
||||
}
|
||||
TextAlignment::BottomRight => {
|
||||
let position = (total_lines as f32)
|
||||
.mul_add(-text_and_line_spacing, size.height);
|
||||
("end", position, x_width_padded.as_str())
|
||||
let position = size.height
|
||||
- (total_lines as f32
|
||||
* text_and_line_spacing);
|
||||
("end", position, "1910")
|
||||
}
|
||||
};
|
||||
|
||||
let font_style = match self.font.style {
|
||||
Style::Normal => "normal",
|
||||
Style::Italic => "italic",
|
||||
Style::Oblique => "oblique",
|
||||
};
|
||||
|
||||
let font_weight = match self.font.weight {
|
||||
Weight::Thin | Weight::ExtraLight | Weight::Light => {
|
||||
"lighter"
|
||||
}
|
||||
Weight::Normal | Weight::Medium => "normal",
|
||||
Weight::Semibold | Weight::Bold => "bold",
|
||||
Weight::ExtraBold | Weight::Black => "bolder",
|
||||
};
|
||||
|
||||
let _ = write!(
|
||||
final_svg,
|
||||
"<svg width=\"{}\" height=\"{}\" viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\"><defs>",
|
||||
size.width, size.height, size.width, size.height
|
||||
);
|
||||
final_svg.push_str(&format!("<svg width=\"{}\" height=\"{}\" viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\"><defs>", size.width, size.height, size.width, size.height));
|
||||
|
||||
if let Some(shadow) = &self.shadow {
|
||||
let _ = write!(
|
||||
final_svg,
|
||||
final_svg.push_str(&format!(
|
||||
"<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
||||
shadow.offset_x,
|
||||
shadow.offset_y,
|
||||
shadow.spread,
|
||||
shadow.color
|
||||
);
|
||||
));
|
||||
}
|
||||
final_svg.push_str("</defs>");
|
||||
|
||||
|
|
@ -410,42 +348,32 @@ impl TextSvg {
|
|||
// "<style> text { letter-spacing: 0em; } </style>",
|
||||
// );
|
||||
|
||||
let _ = write!(
|
||||
final_svg,
|
||||
"<text x=\"0\" y=\"50%\" transform=\"translate({}, 0)\" dominant-baseline=\"middle\" text-anchor=\"{}\" font-style=\"{}\" font-weight=\"{}\" font-family=\"{}\" font-size=\"{}\" fill=\"{}\" ",
|
||||
text_x_position,
|
||||
text_anchor,
|
||||
font_style,
|
||||
font_weight,
|
||||
self.font.name,
|
||||
font_size,
|
||||
self.fill
|
||||
);
|
||||
final_svg.push_str(&format!("<text x=\"0\" y=\"50%\" transform=\"translate({}, 0)\" dominant-baseline=\"middle\" text-anchor=\"{}\" font-weight=\"bold\" font-family=\"{}\" font-size=\"{}\" fill=\"{}\" ", text_x_position, text_anchor, self.font.name, font_size, self.fill));
|
||||
|
||||
if let Some(stroke) = &self.stroke {
|
||||
let _ = write!(
|
||||
final_svg,
|
||||
final_svg.push_str(&format!(
|
||||
"stroke=\"{}\" stroke-width=\"{}px\" stroke-linejoin=\"arcs\" paint-order=\"stroke\"",
|
||||
stroke.color, stroke.size
|
||||
);
|
||||
));
|
||||
}
|
||||
final_svg.push_str(" style=\"filter:url(#shadow);\">");
|
||||
|
||||
if self.shadow.is_some() {
|
||||
final_svg.push_str(" style=\"filter:url(#shadow);\"");
|
||||
}
|
||||
final_svg.push('>');
|
||||
|
||||
for (index, text) in self.text.lines().enumerate() {
|
||||
let _ = write!(
|
||||
final_svg,
|
||||
"<tspan x=\"0\" y=\"{}\">{}</tspan>",
|
||||
(index as f32).mul_add(
|
||||
text_and_line_spacing,
|
||||
starting_y_position
|
||||
),
|
||||
text
|
||||
);
|
||||
}
|
||||
let text: String = self
|
||||
.text
|
||||
.lines()
|
||||
.enumerate()
|
||||
.map(|(index, text)| {
|
||||
format!(
|
||||
"<tspan x=\"0\" y=\"{}\">{}</tspan>",
|
||||
(index as f32).mul_add(
|
||||
text_and_line_spacing,
|
||||
starting_y_position
|
||||
),
|
||||
text
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
final_svg.push_str(&text);
|
||||
|
||||
final_svg.push_str("</text></svg>");
|
||||
|
||||
|
|
@ -461,72 +389,55 @@ impl TextSvg {
|
|||
// text
|
||||
// ));
|
||||
|
||||
if let Some(path) = cache.as_mut() {
|
||||
let hashed_title = rapidhash_v3(final_svg.as_bytes());
|
||||
path.push(PathBuf::from(hashed_title.to_string()));
|
||||
path.set_extension("png");
|
||||
let hashed_title = rapidhash_v3(final_svg.as_bytes());
|
||||
path.push(PathBuf::from(hashed_title.to_string()));
|
||||
path.set_extension("png");
|
||||
|
||||
if path.exists() {
|
||||
// debug!("cached");
|
||||
let handle = Handle::from_path(&path);
|
||||
self.path = Some(path.clone());
|
||||
self.handle = Some(handle);
|
||||
return self;
|
||||
}
|
||||
if path.exists() {
|
||||
// debug!("cached");
|
||||
let handle = Handle::from_path(path);
|
||||
self.handle = Some(handle);
|
||||
return self;
|
||||
}
|
||||
|
||||
// debug!("text string built...");
|
||||
let Ok(resvg_tree) = Tree::from_data(
|
||||
let resvg_tree = Tree::from_data(
|
||||
final_svg.as_bytes(),
|
||||
&resvg::usvg::Options {
|
||||
fontdb: Arc::clone(&self.fontdb),
|
||||
..Default::default()
|
||||
},
|
||||
) else {
|
||||
error!("Couldn't parse the svg into a tree");
|
||||
return self;
|
||||
};
|
||||
)
|
||||
.expect("Woops mama");
|
||||
// debug!("parsed");
|
||||
let transform = tiny_skia::Transform::default();
|
||||
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
let (size_width, size_height) =
|
||||
(size.width as u32, size.height as u32);
|
||||
|
||||
let Some(mut pixmap) = Pixmap::new(size_width, size_height)
|
||||
else {
|
||||
error!("Couldn't create a new pixmap from size");
|
||||
return self;
|
||||
};
|
||||
let mut pixmap =
|
||||
Pixmap::new(size.width as u32, size.height as u32)
|
||||
.expect("opops");
|
||||
resvg::render(&resvg_tree, transform, &mut pixmap.as_mut());
|
||||
// debug!("rendered");
|
||||
|
||||
if let Some(path) = cache.as_ref()
|
||||
&& let Err(e) = pixmap.save_png(path)
|
||||
{
|
||||
if let Err(e) = pixmap.save_png(&path) {
|
||||
error!(?e, "Couldn't save a copy of the text");
|
||||
}
|
||||
self.path = cache;
|
||||
|
||||
// debug!("saved");
|
||||
// let handle = Handle::from_path(path);
|
||||
let handle =
|
||||
Handle::from_rgba(size_width, size_height, pixmap.take());
|
||||
let handle = Handle::from_rgba(
|
||||
size.width as u32,
|
||||
size.height as u32,
|
||||
pixmap.take(),
|
||||
);
|
||||
self.handle = Some(handle);
|
||||
// debug!("stored");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn view<'a>(&self) -> Element<'a, Message> {
|
||||
self.handle.clone().map_or_else(
|
||||
|| Element::from(Space::new(Length::Fill, Length::Fill)),
|
||||
|handle| {
|
||||
Image::new(handle)
|
||||
.content_fit(ContentFit::Cover)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
},
|
||||
)
|
||||
Image::new(self.handle.clone().unwrap())
|
||||
.content_fit(ContentFit::Cover)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -556,91 +467,21 @@ pub fn color(color: impl AsRef<str>) -> Color {
|
|||
}
|
||||
|
||||
pub fn text_svg_generator(
|
||||
slide: crate::core::slide::Slide,
|
||||
fontdb: &Arc<fontdb::Database>,
|
||||
) -> Result<Slide> {
|
||||
let Some(mut path) = dirs::cache_dir() else {
|
||||
error!("Cannot find the cache dir");
|
||||
return Err(miette!("Cannot find the cache dir"));
|
||||
};
|
||||
path.push("lumina");
|
||||
path.push("text_svg_cache");
|
||||
let _ = fs::create_dir_all(&path);
|
||||
|
||||
text_svg_generator_with_cache(slide, fontdb, Some(path))
|
||||
}
|
||||
|
||||
pub fn text_svg_generator_with_cache(
|
||||
mut slide: crate::core::slide::Slide,
|
||||
fontdb: &Arc<fontdb::Database>,
|
||||
cache: Option<PathBuf>,
|
||||
) -> Result<Slide> {
|
||||
if slide.text().is_empty() {
|
||||
Err(miette!("There is no slide text"))
|
||||
} else {
|
||||
let font = slide.font().unwrap_or_default();
|
||||
slide: &mut crate::core::slide::Slide,
|
||||
fontdb: Arc<fontdb::Database>,
|
||||
) {
|
||||
if !slide.text().is_empty() {
|
||||
let text_svg = TextSvg::new(slide.text())
|
||||
.alignment(slide.text_alignment())
|
||||
.fill(
|
||||
slide.text_color().unwrap_or_else(|| "#fff".into()),
|
||||
);
|
||||
let text_svg = if let Some(stroke) = slide.stroke() {
|
||||
text_svg.stroke(stroke)
|
||||
} else {
|
||||
text_svg
|
||||
};
|
||||
let text_svg = if let Some(shadow) = slide.shadow() {
|
||||
text_svg.shadow(shadow)
|
||||
} else {
|
||||
text_svg
|
||||
};
|
||||
let text_svg = text_svg.font(font).fontdb(Arc::clone(fontdb));
|
||||
// debug!(fill = ?text_svg.fill, font = ?text_svg.font, stroke = ?text_svg.stroke, shadow = ?text_svg.shadow, text = ?text_svg.text);
|
||||
let text_svg =
|
||||
text_svg.build(Size::new(1280.0, 720.0), cache);
|
||||
.fill("#fff")
|
||||
.shadow(shadow(2, 2, 5, "#000000"))
|
||||
.stroke(stroke(3, "#000"))
|
||||
.font(
|
||||
Font::from(slide.font())
|
||||
.size(slide.font_size().try_into().unwrap()),
|
||||
)
|
||||
.fontdb(Arc::clone(&fontdb))
|
||||
.build();
|
||||
slide.text_svg = Some(text_svg);
|
||||
Ok(slide)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::slide::Slide;
|
||||
|
||||
use super::*;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use resvg::usvg::fontdb::Database;
|
||||
use tracing::debug;
|
||||
|
||||
#[test]
|
||||
fn test_generator() {
|
||||
let slide = Slide::default();
|
||||
debug!("test");
|
||||
let mut fontdb = Database::new();
|
||||
fontdb.load_system_fonts();
|
||||
let fontdb = Arc::new(fontdb);
|
||||
(0..400).into_par_iter().for_each(|_| {
|
||||
let slide = slide
|
||||
.clone()
|
||||
.set_font_size(120)
|
||||
.set_font("")
|
||||
.set_shadow(shadow(5, 5, 5, "#000"))
|
||||
.set_stroke(stroke(9, "#000"))
|
||||
.set_text("This is the first slide of text\nAnd we are singing\nTo save the world!");
|
||||
match text_svg_generator_with_cache(
|
||||
slide,
|
||||
&fontdb,
|
||||
None,
|
||||
) {
|
||||
Ok(slide) => {
|
||||
assert!(
|
||||
slide
|
||||
.text_svg
|
||||
.is_some_and(|svg| svg.handle.is_some())
|
||||
)
|
||||
},
|
||||
Err(e) => assert!(false, "There was an issue creating the TextSvg: {e}"),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ impl VideoEditor {
|
|||
self.update_entire_video(&video);
|
||||
}
|
||||
Message::ChangeTitle(title) => {
|
||||
self.title.clone_from(&title);
|
||||
self.title = title.clone();
|
||||
if let Some(video) = &self.core_video {
|
||||
let mut video = video.clone();
|
||||
video.title = title;
|
||||
|
|
@ -89,12 +89,14 @@ impl VideoEditor {
|
|||
let task = Task::perform(
|
||||
pick_video(),
|
||||
move |video_result| {
|
||||
video_result.map_or(Message::None, |video| {
|
||||
if let Ok(video) = video_result {
|
||||
let mut video =
|
||||
videos::Video::from(video);
|
||||
video.id = video_id;
|
||||
Message::UpdateVideoFile(video)
|
||||
})
|
||||
} else {
|
||||
Message::None
|
||||
}
|
||||
},
|
||||
);
|
||||
return Action::Task(task);
|
||||
|
|
@ -109,35 +111,36 @@ impl VideoEditor {
|
|||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
let video_elements = self.video.as_ref().map_or_else(
|
||||
|| container(horizontal_space()),
|
||||
|video| {
|
||||
let play_button = button::icon(if video.paused() {
|
||||
icon::from_name("media-playback-start")
|
||||
} else {
|
||||
icon::from_name("media-playback-pause")
|
||||
})
|
||||
.on_press(Message::PauseVideo);
|
||||
let video_track = progress_bar(
|
||||
0.0..=video.duration().as_secs_f32(),
|
||||
video.position().as_secs_f32(),
|
||||
)
|
||||
.height(cosmic::theme::spacing().space_s)
|
||||
.width(Length::Fill);
|
||||
container(
|
||||
row![play_button, video_track]
|
||||
.align_y(Vertical::Center)
|
||||
.spacing(cosmic::theme::spacing().space_m),
|
||||
)
|
||||
.padding(cosmic::theme::spacing().space_s)
|
||||
.center_x(Length::FillPortion(2))
|
||||
},
|
||||
);
|
||||
let video_elements = if let Some(video) = &self.video {
|
||||
let play_button = button::icon(if video.paused() {
|
||||
icon::from_name("media-playback-start")
|
||||
} else {
|
||||
icon::from_name("media-playback-pause")
|
||||
})
|
||||
.on_press(Message::PauseVideo);
|
||||
let video_track = progress_bar(
|
||||
0.0..=video.duration().as_secs_f32(),
|
||||
video.position().as_secs_f32(),
|
||||
)
|
||||
.height(cosmic::theme::spacing().space_s)
|
||||
.width(Length::Fill);
|
||||
container(
|
||||
row![play_button, video_track]
|
||||
.align_y(Vertical::Center)
|
||||
.spacing(cosmic::theme::spacing().space_m),
|
||||
)
|
||||
.padding(cosmic::theme::spacing().space_s)
|
||||
.center_x(Length::FillPortion(2))
|
||||
} else {
|
||||
container(horizontal_space())
|
||||
};
|
||||
|
||||
let video_player = self.video.as_ref().map_or_else(
|
||||
|| Element::from(Space::new(0, 0)),
|
||||
|video| Element::from(VideoPlayer::new(video)),
|
||||
);
|
||||
let video_player = self
|
||||
.video
|
||||
.as_ref()
|
||||
.map_or(Element::from(Space::new(0, 0)), |video| {
|
||||
Element::from(VideoPlayer::new(video))
|
||||
});
|
||||
|
||||
let video_section = column![video_player, video_elements]
|
||||
.spacing(cosmic::theme::spacing().space_s);
|
||||
|
|
@ -182,13 +185,13 @@ impl VideoEditor {
|
|||
.map(|url| Video::new(&url).expect("Should be here"))
|
||||
else {
|
||||
self.video = None;
|
||||
self.title.clone_from(&video.title);
|
||||
self.title = video.title.clone();
|
||||
self.core_video = Some(video.clone());
|
||||
return;
|
||||
};
|
||||
player_video.set_paused(true);
|
||||
self.video = Some(player_video);
|
||||
self.title.clone_from(&video.title);
|
||||
self.title = video.title.clone();
|
||||
self.core_video = Some(video.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -214,9 +217,7 @@ async fn pick_video() -> Result<PathBuf, VideoError> {
|
|||
error!(?e);
|
||||
VideoError::DialogClosed
|
||||
})
|
||||
.map(|file| {
|
||||
file.url().to_file_path().expect("Should be a file here")
|
||||
})
|
||||
.map(|file| file.url().to_file_path().unwrap())
|
||||
// rfd::AsyncFileDialog::new()
|
||||
// .set_title("Choose a background...")
|
||||
// .add_filter(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::nursery)]
|
||||
#[allow(clippy::pedantic)]
|
||||
pub mod draggable;
|
||||
pub mod slide_text;
|
||||
pub mod verse_editor;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use cosmic::iced_wgpu::Primitive;
|
|||
use cosmic::iced_wgpu::primitive::Renderer as PrimitiveRenderer;
|
||||
|
||||
pub struct SlideText {
|
||||
_text: String,
|
||||
text: String,
|
||||
font_size: f32,
|
||||
}
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ impl SlideText {
|
|||
pub fn new(text: impl AsRef<str>) -> Self {
|
||||
let text = text.as_ref();
|
||||
Self {
|
||||
_text: text.to_string(),
|
||||
text: text.to_string(),
|
||||
font_size: 50.0,
|
||||
}
|
||||
}
|
||||
|
|
@ -88,8 +88,8 @@ where
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TextPrimitive {
|
||||
_text_id: u64,
|
||||
_size: (u32, u32),
|
||||
text_id: u64,
|
||||
size: (u32, u32),
|
||||
}
|
||||
|
||||
impl TextPrimitive {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use cosmic::{
|
||||
Element, Task,
|
||||
cosmic_theme::palette::WithAlpha,
|
||||
iced::{Background, Border},
|
||||
iced::{Background, Border, Color, Point},
|
||||
iced_widget::{column, row},
|
||||
theme,
|
||||
widget::{
|
||||
button, combo_box, container, horizontal_space, icon,
|
||||
text_editor,
|
||||
button, combo_box, container, horizontal_space, icon, text,
|
||||
text_editor, text_input,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -35,17 +35,16 @@ pub enum Action {
|
|||
UpdateVerse((VerseName, String)),
|
||||
UpdateVerseName(String),
|
||||
DeleteVerse(VerseName),
|
||||
ScrollVerses(f32),
|
||||
None,
|
||||
}
|
||||
|
||||
impl VerseEditor {
|
||||
#[must_use]
|
||||
pub fn new(verse: VerseName, lyric: &str) -> Self {
|
||||
pub fn new(verse: VerseName, lyric: String) -> Self {
|
||||
Self {
|
||||
verse_name: verse,
|
||||
lyric: lyric.to_string(),
|
||||
content: text_editor::Content::with_text(lyric),
|
||||
lyric: lyric.clone(),
|
||||
content: text_editor::Content::with_text(&lyric),
|
||||
editing_verse_name: false,
|
||||
verse_name_combo: combo_box::State::new(
|
||||
VerseName::all_names(),
|
||||
|
|
@ -54,27 +53,18 @@ impl VerseEditor {
|
|||
}
|
||||
pub fn update(&mut self, message: Message) -> Action {
|
||||
match message {
|
||||
Message::UpdateLyric(action) => match action {
|
||||
text_editor::Action::Edit(ref _edit) => {
|
||||
self.content.perform(action);
|
||||
let lyrics = self.content.text();
|
||||
self.lyric.clone_from(&lyrics);
|
||||
let verse = self.verse_name;
|
||||
Action::UpdateVerse((verse, lyrics))
|
||||
}
|
||||
text_editor::Action::Scroll { pixels } => {
|
||||
if self.content.line_count() > 6 {
|
||||
self.content.perform(action);
|
||||
Action::None
|
||||
} else {
|
||||
Action::ScrollVerses(pixels)
|
||||
Message::UpdateLyric(action) => {
|
||||
self.content.perform(action.clone());
|
||||
match action {
|
||||
text_editor::Action::Edit(_edit) => {
|
||||
let lyrics = self.content.text();
|
||||
self.lyric = lyrics.clone();
|
||||
let verse = self.verse_name;
|
||||
Action::UpdateVerse((verse, lyrics))
|
||||
}
|
||||
_ => Action::None,
|
||||
}
|
||||
_ => {
|
||||
self.content.perform(action);
|
||||
Action::None
|
||||
}
|
||||
},
|
||||
}
|
||||
Message::UpdateVerseName(verse_name) => {
|
||||
Action::UpdateVerseName(verse_name)
|
||||
}
|
||||
|
|
@ -89,7 +79,7 @@ impl VerseEditor {
|
|||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
let cosmic::cosmic_theme::Spacing {
|
||||
space_xxs: _,
|
||||
space_xxs,
|
||||
space_s,
|
||||
space_m,
|
||||
..
|
||||
|
|
@ -152,8 +142,11 @@ impl VerseEditor {
|
|||
.color(t.cosmic().accent.hover);
|
||||
match s {
|
||||
text_editor::Status::Active => base_style,
|
||||
text_editor::Status::Hovered
|
||||
| text_editor::Status::Focused => {
|
||||
text_editor::Status::Hovered => {
|
||||
base_style.border = hovered_border;
|
||||
base_style
|
||||
}
|
||||
text_editor::Status::Focused => {
|
||||
base_style.border = hovered_border;
|
||||
base_style
|
||||
}
|
||||
|
|
@ -172,4 +165,13 @@ impl VerseEditor {
|
|||
.class(theme::Container::Card)
|
||||
.into()
|
||||
}
|
||||
|
||||
// TODO not done yet. This doesn't work, need to find a way to either reset the
|
||||
// cursor position or not make new widgets
|
||||
pub fn set_cursor_position(&mut self, position: (usize, usize)) {
|
||||
self.content.perform(text_editor::Action::Click(Point::new(
|
||||
position.0 as f32,
|
||||
position.1 as f32,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
test.db
BIN
test.db
Binary file not shown.
|
|
@ -6,15 +6,10 @@
|
|||
kind: Image
|
||||
),
|
||||
text: "This is Frodo",
|
||||
font: Some(Font(
|
||||
name: "Quicksand",
|
||||
weight: Normal,
|
||||
style: Normal,
|
||||
size: 130,
|
||||
)),
|
||||
font_size: 130,
|
||||
stroke: None,
|
||||
shadow: None,
|
||||
font: "Quicksand",
|
||||
font_size: 50,
|
||||
stroke_size: 0,
|
||||
stroke_color: None,
|
||||
text_alignment: MiddleCenter,
|
||||
video_loop: false,
|
||||
video_start_time: 0.0,
|
||||
|
|
@ -29,15 +24,10 @@
|
|||
kind: Video
|
||||
),
|
||||
text: "This is Frodo",
|
||||
font: Some(Font(
|
||||
name: "Quicksand",
|
||||
weight: Normal,
|
||||
style: Normal,
|
||||
size: 130,
|
||||
)),
|
||||
font_size: 130,
|
||||
stroke: None,
|
||||
shadow: None,
|
||||
font: "Quicksand",
|
||||
font_size: 50,
|
||||
stroke_size: 0,
|
||||
stroke_color: None,
|
||||
text_alignment: MiddleCenter,
|
||||
video_loop: false,
|
||||
video_start_time: 0.0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue